From 9d78365ecb77a4d5d6d85a4e742538ff3cbfc404 Mon Sep 17 00:00:00 2001 From: Kevin Petit Date: Thu, 15 Jun 2023 00:59:21 +0200 Subject: [PATCH 1/4] Add reproduction test --- .../ProjectAbstractWithInheritance.cs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/UnitTests/Projection/ProjectAbstractWithInheritance.cs diff --git a/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs b/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs new file mode 100644 index 0000000000..2c4607fe43 --- /dev/null +++ b/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs @@ -0,0 +1,72 @@ +namespace AutoMapper.UnitTests.Projection; + +public class ProjectAbstractWithInheritance : AutoMapperSpecBase +{ + class StepGroup + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Steps { get; set; } = new HashSet(); + } + abstract class Step + { + public int Id { get; set; } + public string Name { get; set; } + } + class CheckingStep : Step { } + class InstructionStep : Step { } + + class StepGroupModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Steps { get; set; } = new HashSet(); + } + abstract class StepModel + { + public int Id { get; set; } + public string Name { get; set; } + } + class CheckingStepModel : StepModel { } + class InstructionStepModel : StepModel { } + + protected override MapperConfiguration CreateConfiguration() + { + return new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + }); + } + + [Fact] + public void ProjectCollectionWithElementInheritingAbstractClassWithoutException() + { + var stepGroup = new StepGroup + { + Id = 1, + Name = "StepGroup", + Steps = new List + { + new InstructionStep + { + Id = 1, + Name = "InstructionStep" + }, + new CheckingStep + { + Id = 2, + Name = "CheckingStep" + } + } + }; + + var query = new[] { stepGroup }.AsQueryable(); + + Should.NotThrow(() => query.ProjectTo(Configuration).SingleOrDefault()); + } +} \ No newline at end of file From 9f030a8552c43fcfd942faff3214a1aef2a077bd Mon Sep 17 00:00:00 2001 From: Kevin Petit Date: Thu, 15 Jun 2023 01:01:37 +0200 Subject: [PATCH 2/4] If the destination type of the projection is abstract use the default value instead. --- src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index 590a25de2e..130ce42566 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -64,7 +64,10 @@ public QueryExpressions CreateProjection(in ProjectionRequest request, LetProper QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps) { var instanceParameter = Parameter(request.SourceType, "dto" + request.SourceType.Name); - var projection = CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps); + var projection = !typeMap.DestinationType.IsAbstract + ? CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps) + : Default(typeMap.DestinationType); + foreach(var derivedMap in polymorphicMaps) { var sourceType = derivedMap.SourceType; From 65bfcea0a3bb38d8556c23c1500c5025d8320af5 Mon Sep 17 00:00:00 2001 From: Kevin Petit Date: Fri, 16 Jun 2023 01:11:58 +0200 Subject: [PATCH 3/4] Add integration test. --- .../ProjectToAbstractTypeWithInheritance.cs | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs diff --git a/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs new file mode 100644 index 0000000000..5c3d962ed4 --- /dev/null +++ b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs @@ -0,0 +1,103 @@ +namespace AutoMapper.IntegrationTests.Inheritance; + +public class ProjectToAbstractTypeWithInheritance : IntegrationTest +{ + public class StepGroup + { + public int Id { get; set; } + public string Name { get; set; } + public virtual ICollection Steps { get; set; } = new HashSet(); + } + public abstract class Step + { + public int Id { get; set; } + public string Name { get; set; } + public int StepGroupId { get; set; } + public virtual StepGroup StepGroup { get; set; } + } + public class CheckingStep : Step { } + public class InstructionStep : Step { } + + public class StepGroupModel + { + public int Id { get; set; } + public string Name { get; set; } + public ICollection Steps { get; set; } = new HashSet(); + } + public abstract class StepModel + { + public int Id { get; set; } + public string Name { get; set; } + } + public class CheckingStepModel : StepModel { } + public class InstructionStepModel : StepModel { } + + public class Context : LocalDbContext + { + public DbSet StepGroups { get; set; } + + public DbSet Steps { get; set; } + + public DbSet CheckingSteps { get; set; } + + public DbSet InstructionSteps { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.StepGroup).WithMany(p => p.Steps) + .HasForeignKey(d => d.StepGroupId); + }); + } + } + + protected override MapperConfiguration CreateConfiguration() + { + return new MapperConfiguration(cfg => + { + cfg.CreateMap(); + cfg.CreateMap(); + cfg.CreateMap() + .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); + }); + } + + public class DatabaseInitializer : DropCreateDatabaseAlways + { + protected override void Seed(Context context) + { + context.StepGroups.Add(new StepGroup + { + Name = "StepGroup", + Steps = new List + { + new InstructionStep + { + Name = "InstructionStep" + }, + new CheckingStep + { + Name = "CheckingStep" + } + } + }); + + base.Seed(context); + } + } + + [Fact] + public void ProjectCollectionWithElementInheritingAbstractClass() + { + using (var context = new Context()) + { + var stepGroups = ProjectTo(context.StepGroups).First(); + + stepGroups.ShouldNotBeNull(); + stepGroups.Steps.ShouldNotBeEmpty(); + } + } +} \ No newline at end of file From 48fd7b32a757bb23790e331496c7507e9014ae2d Mon Sep 17 00:00:00 2001 From: Lucian Bargaoanu Date: Fri, 16 Jun 2023 11:24:59 +0300 Subject: [PATCH 4/4] exclude included abstract types --- .../QueryableExtensions/ProjectionBuilder.cs | 10 +-- .../ProjectToAbstractTypeWithInheritance.cs | 19 ++--- .../ProjectAbstractWithInheritance.cs | 72 ------------------- 3 files changed, 15 insertions(+), 86 deletions(-) delete mode 100644 src/UnitTests/Projection/ProjectAbstractWithInheritance.cs diff --git a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs index 130ce42566..fd08dd7c41 100644 --- a/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs +++ b/src/AutoMapper/QueryableExtensions/ProjectionBuilder.cs @@ -54,7 +54,8 @@ private QueryExpressions CreateProjection(ProjectionRequest request) private (TypeMap, TypeMap[]) GetPolymorphicMaps(in ProjectionRequest request) { var typeMap = _configuration.ResolveTypeMap(request.SourceType, request.DestinationType) ?? throw TypeMap.MissingMapException(request.SourceType, request.DestinationType); - return (typeMap, _configuration.GetIncludedTypeMaps(typeMap.IncludedDerivedTypes.Where(tp => tp.SourceType != typeMap.SourceType).DistinctBy(tp => tp.SourceType).ToArray())); + return (typeMap, _configuration.GetIncludedTypeMaps(typeMap.IncludedDerivedTypes + .Where(tp => tp.SourceType != typeMap.SourceType && !tp.DestinationType.IsAbstract).DistinctBy(tp => tp.SourceType).ToArray())); } public QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps) { @@ -64,10 +65,9 @@ public QueryExpressions CreateProjection(in ProjectionRequest request, LetProper QueryExpressions CreateProjection(in ProjectionRequest request, LetPropertyMaps letPropertyMaps, TypeMap typeMap, TypeMap[] polymorphicMaps) { var instanceParameter = Parameter(request.SourceType, "dto" + request.SourceType.Name); - var projection = !typeMap.DestinationType.IsAbstract - ? CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps) - : Default(typeMap.DestinationType); - + var destinationType = typeMap.DestinationType; + var projection = polymorphicMaps.Length > 0 && destinationType.IsAbstract ? + Default(destinationType) : CreateProjectionCore(request, instanceParameter, typeMap, letPropertyMaps); foreach(var derivedMap in polymorphicMaps) { var sourceType = derivedMap.SourceType; diff --git a/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs index 5c3d962ed4..4c8535a29e 100644 --- a/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs +++ b/src/IntegrationTests/Inheritance/ProjectToAbstractTypeWithInheritance.cs @@ -6,7 +6,7 @@ public class StepGroup { public int Id { get; set; } public string Name { get; set; } - public virtual ICollection Steps { get; set; } = new HashSet(); + public virtual List Steps { get; set; } = new(); } public abstract class Step { @@ -17,12 +17,13 @@ public abstract class Step } public class CheckingStep : Step { } public class InstructionStep : Step { } + public abstract class AbstractStep : Step { } public class StepGroupModel { public int Id { get; set; } public string Name { get; set; } - public ICollection Steps { get; set; } = new HashSet(); + public List Steps { get; set; } = new(); } public abstract class StepModel { @@ -31,6 +32,7 @@ public abstract class StepModel } public class CheckingStepModel : StepModel { } public class InstructionStepModel : StepModel { } + public abstract class AbstractStepModel : StepModel { } public class Context : LocalDbContext { @@ -62,6 +64,8 @@ protected override MapperConfiguration CreateConfiguration() .IncludeBase(); cfg.CreateMap() .IncludeBase(); + cfg.CreateMap() + .IncludeBase(); }); } @@ -92,12 +96,9 @@ protected override void Seed(Context context) [Fact] public void ProjectCollectionWithElementInheritingAbstractClass() { - using (var context = new Context()) - { - var stepGroups = ProjectTo(context.StepGroups).First(); - - stepGroups.ShouldNotBeNull(); - stepGroups.Steps.ShouldNotBeEmpty(); - } + using var context = new Context(); + var steps = ProjectTo(context.StepGroups).Single().Steps; + steps[0].ShouldBeOfType().Name.ShouldBe("CheckingStep"); + steps[1].ShouldBeOfType().Name.ShouldBe("InstructionStep"); } } \ No newline at end of file diff --git a/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs b/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs deleted file mode 100644 index 2c4607fe43..0000000000 --- a/src/UnitTests/Projection/ProjectAbstractWithInheritance.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace AutoMapper.UnitTests.Projection; - -public class ProjectAbstractWithInheritance : AutoMapperSpecBase -{ - class StepGroup - { - public int Id { get; set; } - public string Name { get; set; } - public ICollection Steps { get; set; } = new HashSet(); - } - abstract class Step - { - public int Id { get; set; } - public string Name { get; set; } - } - class CheckingStep : Step { } - class InstructionStep : Step { } - - class StepGroupModel - { - public int Id { get; set; } - public string Name { get; set; } - public ICollection Steps { get; set; } = new HashSet(); - } - abstract class StepModel - { - public int Id { get; set; } - public string Name { get; set; } - } - class CheckingStepModel : StepModel { } - class InstructionStepModel : StepModel { } - - protected override MapperConfiguration CreateConfiguration() - { - return new MapperConfiguration(cfg => - { - cfg.CreateMap(); - cfg.CreateMap(); - cfg.CreateMap() - .IncludeBase(); - cfg.CreateMap() - .IncludeBase(); - }); - } - - [Fact] - public void ProjectCollectionWithElementInheritingAbstractClassWithoutException() - { - var stepGroup = new StepGroup - { - Id = 1, - Name = "StepGroup", - Steps = new List - { - new InstructionStep - { - Id = 1, - Name = "InstructionStep" - }, - new CheckingStep - { - Id = 2, - Name = "CheckingStep" - } - } - }; - - var query = new[] { stepGroup }.AsQueryable(); - - Should.NotThrow(() => query.ProjectTo(Configuration).SingleOrDefault()); - } -} \ No newline at end of file