Skip to content

Commit 5dd4326

Browse files
Merge pull request #125 from Tasteful/efcore-bug-13
Avoid circular reference when mapping unmapped types
2 parents 79126e3 + 724c1bc commit 5dd4326

File tree

3 files changed

+280
-10
lines changed

3 files changed

+280
-10
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.ComponentModel.DataAnnotations.Schema;
4+
using System.Data.Entity;
5+
using System.Data.SqlServerCe;
6+
using System.Linq;
7+
using AutoMapper.EntityFramework;
8+
using AutoMapper.EquivalencyExpression;
9+
using FluentAssertions;
10+
using Xunit;
11+
12+
namespace AutoMapper.Collection.EntityFramework.Tests
13+
{
14+
public class EntityFramworkUnmappedTypes
15+
{
16+
private void ConfigureMapper(IMapperConfigurationExpression cfg)
17+
{
18+
cfg.AddCollectionMappers();
19+
cfg.SetGeneratePropertyMaps<GenerateEntityFrameworkPrimaryKeyPropertyMaps<DB>>();
20+
}
21+
22+
protected IMapper CreateMapper(Action<IMapperConfigurationExpression> cfg)
23+
{
24+
var map = new MapperConfiguration(cfg);
25+
map.CompileMappings();
26+
27+
var mapper = map.CreateMapper();
28+
mapper.ConfigurationProvider.AssertConfigurationIsValid();
29+
return mapper;
30+
}
31+
32+
33+
[Fact]
34+
public void Should_not_throw_exception_for_nonexisting_types()
35+
{
36+
var mapper = CreateMapper(ConfigureMapper);
37+
38+
var originalModel = new System
39+
{
40+
Name = "My First System",
41+
Contacts = new List<Contact>
42+
{
43+
new Contact
44+
{
45+
Name = "John",
46+
Emails = new List<Email>()
47+
{
48+
new Email
49+
{
50+
Address = "[email protected]"
51+
}
52+
}
53+
}
54+
}
55+
};
56+
57+
var originalEmail = originalModel.Contacts.Single().Emails.Single();
58+
59+
var assertModel = mapper.Map<SystemViewModel>(originalModel);
60+
assertModel.Name.Should().Be(originalModel.Name);
61+
assertModel.Contacts.Single().Name.Should().Be(originalModel.Contacts.Single().Name);
62+
assertModel.Contacts.Single().Emails.Single().Address.Should().Be(originalModel.Contacts.Single().Emails.Single().Address);
63+
64+
assertModel.Contacts.Single().Emails.Add(new EmailViewModel { Address = "[email protected]" });
65+
66+
mapper.Map(assertModel, originalModel);
67+
// This tests if equality was found and mapped to pre-existing object and not defaulting to AM and clearing and regenerating the list
68+
originalModel.Contacts.Single().Emails.First().Should().Be(originalEmail);
69+
}
70+
71+
public class DB : DbContext
72+
{
73+
public DB()
74+
: base(new SqlCeConnection("Data Source=MyDatabase.sdf;Persist Security Info=False;"), contextOwnsConnection: true)
75+
{
76+
}
77+
78+
public DbSet<System> Systems { get; set; }
79+
public DbSet<Contact> Contacts { get; set; }
80+
public DbSet<Email> Emails { get; set; }
81+
}
82+
83+
84+
public class System
85+
{
86+
public Guid Id { get; set; } = Guid.NewGuid();
87+
public string Name { get; set; }
88+
89+
public ICollection<Contact> Contacts { get; set; }
90+
}
91+
92+
public class Contact
93+
{
94+
public Guid Id { get; set; } = Guid.NewGuid();
95+
public Guid SystemId { get; set; }
96+
public string Name { get; set; }
97+
98+
[ForeignKey("SystemId")]
99+
public System System { get; set; }
100+
101+
public ICollection<Email> Emails { get; set; }
102+
}
103+
104+
public class Email
105+
{
106+
public Guid Id { get; set; } = Guid.NewGuid();
107+
108+
public Guid ContactId { get; set; }
109+
public string Address { get; set; }
110+
111+
[ForeignKey("ContactId")]
112+
public Contact Contact { get; set; }
113+
}
114+
115+
public class SystemViewModel
116+
{
117+
public Guid Id { get; set; }
118+
public string Name { get; set; }
119+
120+
public ICollection<ContactViewModel> Contacts { get; set; }
121+
}
122+
123+
public class ContactViewModel
124+
{
125+
public Guid Id { get; set; }
126+
public Guid SystemId { get; set; }
127+
public string Name { get; set; }
128+
129+
public SystemViewModel System { get; set; }
130+
131+
public ICollection<EmailViewModel> Emails { get; set; }
132+
}
133+
134+
public class EmailViewModel
135+
{
136+
public Guid Id { get; set; }
137+
public Guid ContactId { get; set; }
138+
public string Address { get; set; }
139+
140+
public ContactViewModel Contact { get; set; }
141+
}
142+
}
143+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using AutoMapper.EquivalencyExpression;
5+
using FluentAssertions;
6+
using Xunit;
7+
8+
namespace AutoMapper.Collection
9+
{
10+
public class NotExistingTests
11+
{
12+
[Fact]
13+
public void Should_not_throw_exception_for_nonexisting_types()
14+
{
15+
var configuration = new MapperConfiguration(x =>
16+
{
17+
x.AddCollectionMappers();
18+
});
19+
IMapper mapper = new Mapper(configuration);
20+
21+
var originalModel = new System
22+
{
23+
Name = "My First System",
24+
Contacts = new List<Contact>
25+
{
26+
new Contact
27+
{
28+
Name = "John",
29+
Emails = new List<Email>()
30+
{
31+
new Email
32+
{
33+
Address = "[email protected]"
34+
}
35+
}
36+
}
37+
}
38+
};
39+
40+
var originalEmail = originalModel.Contacts.Single().Emails.Single();
41+
42+
var assertModel = mapper.Map<SystemViewModel>(originalModel);
43+
assertModel.Name.Should().Be(originalModel.Name);
44+
assertModel.Contacts.Single().Name.Should().Be(originalModel.Contacts.Single().Name);
45+
assertModel.Contacts.Single().Emails.Single().Address.Should().Be(originalModel.Contacts.Single().Emails.Single().Address);
46+
47+
assertModel.Contacts.Single().Emails.Add(new EmailViewModel { Address = "[email protected]" });
48+
49+
mapper.Map(assertModel, originalModel);
50+
}
51+
52+
public class System
53+
{
54+
public Guid Id { get; set; } = Guid.NewGuid();
55+
public string Name { get; set; }
56+
57+
public ICollection<Contact> Contacts { get; set; }
58+
}
59+
60+
public class Contact
61+
{
62+
public Guid Id { get; set; } = Guid.NewGuid();
63+
public Guid SystemId { get; set; }
64+
public string Name { get; set; }
65+
66+
public System System { get; set; }
67+
68+
public ICollection<Email> Emails { get; set; }
69+
}
70+
71+
public class Email
72+
{
73+
public Guid Id { get; set; } = Guid.NewGuid();
74+
75+
public Guid ContactId { get; set; }
76+
public string Address { get; set; }
77+
78+
public Contact Contact { get; set; }
79+
}
80+
81+
public class SystemViewModel
82+
{
83+
public Guid Id { get; set; }
84+
public string Name { get; set; }
85+
86+
public ICollection<ContactViewModel> Contacts { get; set; }
87+
}
88+
89+
public class ContactViewModel
90+
{
91+
public Guid Id { get; set; }
92+
public Guid SystemId { get; set; }
93+
public string Name { get; set; }
94+
95+
public SystemViewModel System { get; set; }
96+
97+
public ICollection<EmailViewModel> Emails { get; set; }
98+
}
99+
100+
public class EmailViewModel
101+
{
102+
public Guid Id { get; set; }
103+
public Guid ContactId { get; set; }
104+
public string Address { get; set; }
105+
106+
public ContactViewModel Contact { get; set; }
107+
}
108+
}
109+
}

src/AutoMapper.Collection/Mappers/EquivalentExpressionAddRemoveCollectionMapper.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System.Collections.Concurrent;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Linq.Expressions;
45
using System.Reflection;
@@ -10,7 +11,7 @@ namespace AutoMapper.Mappers
1011
{
1112
public class EquivalentExpressionAddRemoveCollectionMapper : IConfigurationObjectMapper
1213
{
13-
private readonly CollectionMapper CollectionMapper = new CollectionMapper();
14+
private readonly CollectionMapper _collectionMapper = new CollectionMapper();
1415

1516
public IConfigurationProvider ConfigurationProvider { get; set; }
1617

@@ -62,13 +63,13 @@ public static TDestination Map<TSource, TSourceItem, TDestination, TDestinationI
6263
return destination;
6364
}
6465

65-
private static readonly MethodInfo MapMethodInfo = typeof(EquivalentExpressionAddRemoveCollectionMapper).GetRuntimeMethods().First(_ => _.IsStatic);
66+
private static readonly MethodInfo _mapMethodInfo = typeof(EquivalentExpressionAddRemoveCollectionMapper).GetRuntimeMethods().Single(x => x.IsStatic && x.Name == nameof(Map));
67+
private static readonly ConcurrentDictionary<TypePair, IObjectMapper> _objectMapperCache = new ConcurrentDictionary<TypePair, IObjectMapper>();
6668

6769
public bool IsMatch(TypePair typePair)
6870
{
6971
return typePair.SourceType.IsEnumerableType()
70-
&& typePair.DestinationType.IsCollectionType()
71-
&& this.GetEquivalentExpression(TypeHelper.GetElementType(typePair.SourceType), TypeHelper.GetElementType(typePair.DestinationType)) != null;
72+
&& typePair.DestinationType.IsCollectionType();
7273
}
7374

7475
public Expression MapExpression(IConfigurationProvider configurationProvider, ProfileMap profileMap, IMemberMap memberMap,
@@ -77,15 +78,32 @@ public Expression MapExpression(IConfigurationProvider configurationProvider, Pr
7778
var sourceType = TypeHelper.GetElementType(sourceExpression.Type);
7879
var destType = TypeHelper.GetElementType(destExpression.Type);
7980

80-
var method = MapMethodInfo.MakeGenericMethod(sourceExpression.Type, sourceType, destExpression.Type, destType);
8181
var equivalencyExpression = this.GetEquivalentExpression(sourceType, destType);
82+
if (equivalencyExpression == null)
83+
{
84+
var typePair = new TypePair(sourceExpression.Type, destExpression.Type);
85+
return _objectMapperCache.GetOrAdd(typePair, _ =>
86+
{
87+
var mappers = new List<IObjectMapper>(configurationProvider.GetMappers());
88+
for (var i = mappers.IndexOf(this) + 1; i < mappers.Count; i++)
89+
{
90+
var mapper = mappers[i];
91+
if (mapper.IsMatch(typePair))
92+
{
93+
return mapper;
94+
}
95+
}
96+
return _collectionMapper;
97+
})
98+
.MapExpression(configurationProvider, profileMap, memberMap, sourceExpression, destExpression, contextExpression);
99+
}
82100

83-
var equivalencyExpressionConst = Constant(equivalencyExpression);
84-
var map = Call(null, method, sourceExpression, destExpression, contextExpression, equivalencyExpressionConst);
101+
var method = _mapMethodInfo.MakeGenericMethod(sourceExpression.Type, sourceType, destExpression.Type, destType);
102+
var map = Call(null, method, sourceExpression, destExpression, contextExpression, Constant(equivalencyExpression));
85103

86104
var notNull = NotEqual(destExpression, Constant(null));
87-
var collectionMap = CollectionMapper.MapExpression(configurationProvider, profileMap, memberMap, sourceExpression, destExpression, contextExpression);
88-
return Condition(notNull, map, Convert(collectionMap, destExpression.Type));
105+
var collectionMapperExpression = _collectionMapper.MapExpression(configurationProvider, profileMap, memberMap, sourceExpression, destExpression, contextExpression);
106+
return Condition(notNull, map, Convert(collectionMapperExpression, destExpression.Type));
89107
}
90108
}
91109
}

0 commit comments

Comments
 (0)