I'm having a lot of trouble with creating my business entities from my data entities.
My Data.Entities.User looks as follows:
public class User
{
public User()
{
Messages = new List<Message>();
Followers = new List<User>();
Favorites = new List<Message>();
Notifications = new List<Notification>();
SubscribedTopics = new List<Topic>();
}
public string Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Tag { get; set; }
public string Picture { get; set; }
public ICollection<Message> Messages { get; set; }
public ICollection<User> Followers { get; set; }
public ICollection<Message> Favorites { get; set; }
public ICollection<Notification> Notifications { get; set; }
public ICollection<Topic> SubscribedTopics { get; set; }
}
My Data.Mappers.UserMapper looks like this:
class UserMapper : EntityTypeConfiguration<User>
{
public UserMapper()
{
// Table Mapping
ToTable("Users");
// Primary Key
HasKey(u => u.Id);
Property(u => u.Id)
.IsRequired();
// Properties
Property(u => u.Name)
.IsRequired()
.HasMaxLength(140);
Property(u => u.Email)
.IsRequired()
.HasMaxLength(255)
.IsUnicode(false);
Property(u => u.Tag)
.IsRequired()
.IsUnicode(false)
.HasMaxLength(255)
.HasColumnAnnotation("Index", new IndexAnnotation(new IndexAttribute()));
Property(u => u.Picture)
.IsOptional();
// Relationships
HasMany(u => u.Followers)
.WithMany()
.Map(u => u.MapLeftKey("FollowerID"));
HasMany(u => u.Favorites)
.WithMany()
.Map(u => u.MapLeftKey("MessageID"));
HasMany(u => u.SubscribedTopics)
.WithMany(t => t.Subscribers)
.Map(u =>
{
u.ToTable("TopicSubscribers");
u.MapLeftKey("UserID");
u.MapRightKey("TopicID");
});
}
}
Finally, my Domain.Entities.User like this:
public class User : EntityBase<string>
{
public string Name { get; set; }
public string Email { get; set; }
public string Tag { get; set; }
public string Picture { get; set; }
public IEnumerable<Message> Messages { get; set; }
public IEnumerable<User> Followers { get; set; }
public IEnumerable<Message> Favorites { get; set; }
public IEnumerable<Notification> Notifications { get; set; }
public IEnumerable<Topic> SubscribedTopics { get; set; }
protected override void Validate()
{
if (string.IsNullOrWhiteSpace(Name))
{
AddBrokenRule(new ValidationRule("Name", "Name_Missing"));
}
if (string.IsNullOrWhiteSpace(Email))
{
AddBrokenRule(new ValidationRule("Email", "Email_Missing"));
}
if (string.IsNullOrWhiteSpace(Tag))
{
AddBrokenRule(new ValidationRule("Tag", "Tag_Missing"));
}
System.Uri uriResult;
if (!string.IsNullOrWhiteSpace(Picture) &&
Uri.TryCreate(Picture, UriKind.Absolute, out uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps))
{
AddBrokenRule(new ValidationRule("Picture", "Picture_InvalidURI"));
}
}
}
EntityBase adds the Id parameter, so as far as parameters are concerned, these two classes should be identical.
The part where I run into trouble is mapping the Data Entity to the Domain Entity.
public override IEnumerable<User> GetAll()
{
IEnumerable<User> user = _context.Users.Project()
.To<User>("Followers");
return user;
}
I think what's causing trouble is the circular navigational properties. User1 might have a follower named User2, while at the same time following User2.
So far I have tried both AutoMapper and ValueInjecter, but I have not had any success with either.
- I tried adding "Virtual" to all navigational properties, enabling lazy and proxy loading, but this causes both AutoMapper and ValueInjecter to fail. ValueInjecter due to a already opened datareader and AutoMapper because of a type mismatch.
- I tried explicitly loading navigational properties, but as soon as I Include("Followers") on User, I get a stackoverflow.
- Trying to create a AutoMapperConfiguration where I specify a maxDepth of 1 yields a stackoverflow unless I add opt.ExplicitExpansion to every navigational property.
- If i then try to explicitly expand a navigational property, I get
The type 'ShortStuff.Domain.Entities.User' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be initialized in two places in the same query, but only if the same properties are set in both places and those properties are set in the same order.
Ideally I would want a solution that lets me explicitly control which navigational properties to expand without recursing.
For example, I'd like to do something like:
_context.Users.Include("Followers").NoNavigation().AsEnumerable();
And then I would be able to access User.Followers and have a list of other users, with their navigational properties set to null.
Many thanks!
Full source code of my Repository / Service learning project can be found on Github at https://github.com/Bio2hazard/ShortStuff/tree/master/ShortStuffApi
Edit: I made some progress.
I got things to work by turning off proxy generation & lazy loading, and then using ValueInjector like so:
IEnumerable<Data.Entities.User> userList = _context.Users.Include("Followers").Include("Favorites").Include("Messages").Include("Notifications").Include("SubscribedTopics");
IEnumerable<User> users = userList.Select(u => new User
{
Id = u.Id,
Email = u.Email,
Picture = u.Picture,
Tag = u.Tag,
Name = u.Name,
Followers = u.Followers.Select(uu => new User().InjectFrom<SmartConventionInjection>(uu)).Cast<User>(),
Favorites = u.Favorites.Select(uf => new Message().InjectFrom<SmartConventionInjection>(uf)).Cast<Message>(),
Messages = u.Messages.Select(um => new Message().InjectFrom<SmartConventionInjection>(um)).Cast<Message>(),
Notifications = u.Notifications.Select(un => new Notification().InjectFrom<SmartConventionInjection>(un)).Cast<Notification>(),
SubscribedTopics = u.SubscribedTopics.Select(ut => new Topic().InjectFrom<SmartConventionInjection>(ut)).Cast<Topic>()
});
But that's a ton of code. I could probably create a factory for this, but there has got to be a easier way, right?