Mapping Data Entity with Self-Navigating Property to Business Entity - c#

I'm having a lot of trouble with creating my business entities from my data entities.
Github
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?

with ValueInjecter you can use the SmartConventionInjection which will only access the properties if it needs to get the value:
http://valueinjecter.codeplex.com/wikipage?title=SmartConventionInjection&referringTitle=Home
other injections usually get the value too so that you could use it in the matching algorithm
for an example of using valueinjecter with Entity Framework (code first, latest)
have a look at this project: http://prodinner.codeplex.com

Related

Optional loading of EF Core computed properties

I am working on mapping a few database entities for a reporting tool.
At the moment, there are a few computed properties depending on navigation properties for their loading. They've been bound through AutoMapper to ease the process.
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Foo> Foos { get; set; }
public virtual ICollection<Bar> Bars { get; set; }
}
public class CustomerDto
{
public long Id { get; set; }
public string Name { get; set; }
public long TotalNumberOfFoos { get; set; }
public long NumberOfBarsWithCondition { get; set; }
}
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p => p.MapFrom(c => c.Foos.Count))
.ForMember(d => d.NumberOfBarsWithCondition, p => p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count()));
}
}
public class CustomerController : Controller
{
public async Task<List<CustomerDto>> CustomersByName(string name)
{
using (var db = new MyDbContext())
{
return await db.Customers
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
.Where(c => c.Name == name).ToListAsync();
}
}
}
Of course, the queries to retrieve these properties can become quite expensive as the size of the database increases, and they're not always needed in the final report.
The idea is to have an option for the user to choose if they want them included or not in the final report, but I haven't found a way to make the mapping optional at query time.
Is there a way to do this automatically, or am I forced to materialize the list and query these properties myself separately, losing the advantage of having computed properties from the database?
What you need is to utilize the so called AutoMapper Explicit expansion feature. Which should probably be called "explicit property inclusion" (not to be mixed with EF Core Include which is only for navigations), because it works for any destination property, and what it does it to rather include it automatically in the generated projection (Select), include it only when you opt-in explicitly.
So, you need first to configure such properties as ExplicitExpansion(), e.g.
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p =>
{
p.MapFrom(c => c.Foos.Count);
p.ExplicitExpansion();
})
.ForMember(d => d.NumberOfBarsWithCondition, p =>
{
p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count());
p.ExplicitExpansion();
});
Now by default they won't be populated. Use the additional arguments of ProjectTo to pass which ones you want to "expand" (include), e.g.
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider, e => e.TotalNumberOfFoos)

Unable to map from one ViewModel to another

I'm making a list of checkboxes to update a user's roles, and I'm trying to map from this:
public class ApplicationRoleViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string NormalizedName { get; set; }
public string ConcurrencyStamp { get; set; }
public int SortOrder { get; set; }
public string DisplayName { get; set; }
public string Description { get; set; }
public string Icon { get; set; } // Font Awesome-ikoner, f.eks. "fa-user"
}
to this:
public class SelectableRoleViewModel
{
public Guid Id { get; set; }
public string DisplayName { get; set; }
public bool Selected { get; set; }
}
This is my mapping:
CreateMap<ApplicationRoleViewModel, SelectableRoleViewModel>()
.ForMember(dest => dest.Id, s => s.MapFrom(i => i.Id))
.ForMember(dest => dest.DisplayName, s => s.MapFrom(d => d.DisplayName))
.ForMember(dest => dest.Selected, i => i.Ignore());
Mapping it like this in the controller:
ApplicationRole role = await db.Roles.FirstOrDefaultAsync();
SelectableRoleViewModel sr = auto.Map<SelectableRoleViewModel>(role);
gives me the following error message:
AutoMapperMappingException: Missing type map configuration or unsupported mapping.
I am registering AutoMapper in Startup.cs like this:
services.AddAutoMapper(typeof(Startup));
Then, in AutoMapperProfile.cs:
public class AutomapperProfile : Profile
{
public AutomapperProfile()
{
// This is not working:
CreateMap<ApplicationRoleViewModel, SelectableRoleViewModel>()
.ForMember(dest => dest.Selected, i => i.Ignore());
// This is working:
CreateMap<ApplicationUser, ApplicationUserViewModel>();
// Many more mappings, all working
}
}
How can I get it to work?
The code you specified seems to be correct.
I will just suggest to remove the ForMember method for properties with the same names as auto mapper handles it automatically:
CreateMap<ApplicationRoleViewModel, SelectableRoleViewModel>()
.ForMember(dest => dest.Selected, i => i.Ignore());
The problem seems to be because you are not using the mapper right. Where have you registered the mapper? Is the registration happens before the map? Did you do it in the Startup? If you specify more code, it will be easier to help.
UPDATE:
After getting more code & info, the problem was that the map worked on a different object, ApplicationRoleViewModel and not ApplicationRole.
Just to see a difference ;)
public static SelectableRoleViewModel ToSelectable(this ApplicationRoleViewModel model)
{
return new SelectableRoleViewModel
{
Id = model.Id,
DisplayName = model.DisplayName
};
}
// Usage
var selectable = applicationRole.ToSelectable();
Type it once
Perfectly testable
Fully maintainable - supports all kinds of conversion/mapping
Reduce amount of injected dependencies and abstractions (mapper)
No extra dependencies on third party libraries

Entity Framework Core update column with foreign key reference

This is the simplified version of the table structure I have:
[Table("PolicyMapping")]
public class PolicyMapping
{
[Key]
public Guid Id { get; set; }
public Policy PolicyA { get; set; }
public Policy PolicyB { get; set; }
public Lookup_Bank Bank { get; set; }
}
[Table("Policy")]
public class Policy
{
[Key]
public Guid Id { get; set; }
public string PolicyNm { get; set; }
public string Description { get; set; }
}
[Table("Lookup_Bank")]
public class Lookup_Bank
{
[Key]
public Guid Id { get; set; }
public string Name { get; set; }
public string Code { get; set; }
}
I am working on the edit screen for policy mapping where you can have the same values for PolicyA and PolicyB attributes.
After using automapper for DTO to the entity, here is my entity object looks like:
var policyMapping = new PolicyMapping
{
Id = "b27fb632-330b-46be-a649-2e2463d58626",
PolicyA = new Policy
{
Id = "a4f1cf6f-034d-4727-ab8f-49e95b2c9d23",
PolicyNm = null,
Description = null
},
PolicyB = new Policy
{
Id = "a4f1cf6f-034d-4727-ab8f-49e95b2c9d23",
PolicyNm = null,
Description = null
},
Bank = new Lookup_Bank()
{
Id = "98ed2bae-631b-490c-8ddf-3e02232d4231",
Name = null,
Code = null
}
}
I am mapping only selected id value of dropdown to entity id using automapper. Values are present for Code, Description and other attributes in the database. It's just not getting populated after automapper.
dbContext.PolicyMapping.Attach(policyMapping);
dbContext.SaveChanges();
This is the error, I am getting
The instance of entity type Policy cannot be tracked because another instance with the same key value for {'a4f1cf6f-034d-4727-ab8f-49e95b2c9d23'} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
The reason for the error maybe because I am attaching two different entities with the same Id. I am still not sure how can I make it work in the most efficient way?
Solution 1: (not efficient)
var fromdatabase = dbContext.PolicyMapping.Include(x => x.PolicyA)
.Include(x => x.Bank)
.Include(x => x.PolicyB)
.FirstOrDefault(x => x.Id == policyMapping.Id);
fromdatabase.PolicyA = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyA.Id);
fromdatabase.PolicyB = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyB.Id);
dbContext.PolicyMapping.Attach(fromdatabase);
dbContext.SaveChanges();
This is working. But I would like to avoid a trip to the database just to fetch the entire entity.
Edit: Based on Xing's answer
#Xing, pointed out to change the Model structure by adding navigational properties and some changes in OnModelCreating method. (This method is currently blank in my code)
However, I went through a couple of articles (This & This) related to EF Core Code First approach, none of them are saying about navigational properties and all.
I am wondering how they are updating the column in this scenario?
If you just would like to update the Id of the navigation properties, you could add foreign key for them and update it.
1.Model:
public class PolicyMapping
{
[Key]
public Guid Id { get; set; }
public Guid PolicyAId { get; set; }
public Policy PolicyA { get; set; }
public Guid PolicyBId { get; set; }
public Policy PolicyB { get; set; }
public Guid BankId { get; set; }
public Lookup_Bank Bank { get; set; }
}
2.DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.PolicyA)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.PolicyAId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.PolicyB)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.PolicyBId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<PolicyMapping>()
.HasOne(x => x.Bank)
.WithOne()
.HasForeignKey<PolicyMapping>(p => p.BankId)
.OnDelete(DeleteBehavior.Restrict);
}
3.Add migrations.
4.Controller:
var policyMapping = new PolicyMapping
{
Id = new Guid("b27fb632-330b-46be-a649-2e2463d58626"),
PolicyAId = new Guid("a4f1cf6f-034d-4727-ab8f-49e95b2c9d23"),
PolicyBId = new Guid("a4f1cf6f-034d-4727-ab8f-49e95b2c9d23"),
BankId = new Guid("98ed2bae-631b-490c-8ddf-3e02232d4231")
};
dbContext.PolicyMapping.Attach(policyMapping);
dbContext.Entry(policyMapping).Property("PolicyAId").IsModified = true;
dbContext.Entry(policyMapping).Property("PolicyBId").IsModified = true;
dbContext.Entry(policyMapping).Property("BankId").IsModified = true;
dbContext.SaveChanges();
Then you could retrieve PolicyA or PolicyB from their foreign key PolicyAId or PolicyBId
var policyA = dbContext.Policy.FirstOrDefault(x => x.Id == policyMapping.PolicyAId);

Many to many in database but 1-to-1 in entity framework

I started working on an ongoing project and it has a many-to-many relationship in the database and in some parts of the code too, but I realized that even the relationship being many-to-many in the model there is always only one line linking the two entities (confirmed with the author). This is what I mean: The two entities are task and task list and a task only belongs to a task list. Models below:
public class ProjectTask
{
public long Id { get; set; }
// other non related properties
}
public class ProjectTaskList
{
public long Id { get; set; }
public DateTime? DateEnd { get; set; }
// other non related properties
}
// link between task list and task
public class ProjectTaskListTask
{
public long ProjectTaskId { get; set; }
public ProjectTask ProjectTask { get; set; }
public long ProjectTaskListId { get; set; }
public ProjectTaskList ProjectTaskList { get; set; }
public int Order { get; set; }
}
And its configuration in the OnModelCreating method of the context class:
modelBuilder.Entity<ProjectTaskListTask>()
.HasKey(a => new { a.ProjectTaskId, a.ProjectTaskListId });
modelBuilder.Entity<ProjectTaskListTask>()
.HasOne(u => u.ProjectTaskList)
.WithMany(u => u.Tasks)
.IsRequired()
.OnDelete(DeleteBehavior.Restrict);
My problem is: In some parts of my code I need to know the Task List of a task, I need to use it in Where queries to do some validations, like : Tasks.Where(p => p.TaskList.DateEnd == null).
How can I add a Not Mapped property to the ProjectTask entity so I could do that? I'm using Entity Framework Core 2.
Thanks for any help
Without changing the underlying data structure, could you query ProjectTaskListTask? Something along the lines...?
ProjectTaskListTask
.Include(p => p.ProjectTaskList)
.Include(p => p.ProjectTask)
.Where(p => p.ProjectTaskList.DateEnd == null)
.Select(p => p.ProjectTask);

Entity Framework Many to Many Relationship returning Null

I developed and uploaded a web service to Azure using Entity Framework 6.1.3 with MVC design pattern.
So let's imagine I have a Workshop that can have many Clients and a Client that can have many Workshops.
So far my results have been null, empty values and some times correct values but without the relationship (no clients inside my workshop, and the other way around).
This is what I have at this point:
public class Workshop
{
public Workshop()
{
this.Clients = new HashSet<Client>();
this.ModuleSets = new HashSet<ModuleSet>();
}
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Client> Clients { get; set; }
public virtual ICollection<ModuleSet> ModuleSets { get; set; }
}
public class Client
{
public Client()
{
this.Workshops = new HashSet<Workshop>();
this.Vehicles = new HashSet<Vehicle>();
}
public int Id { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Workshop> Workshops { get; set; }
public virtual ICollection<Vehicle> Vehicles { get; set; }
}
Yes I have more relations going on at the same time.
Since that alone was not giving me anything, I added some Fluent Api, like this:
modelBuilder.Entity<Workshop>().
HasMany(c => c.Clients).
WithMany(p => p.Workshops).
Map(
m =>
{
m.MapLeftKey("Workshop_Id");
m.MapRightKey("Client_Id");
m.ToTable("WorkshopClients");
});
The names that are shown are the ones that are in the table WorkshopClients (auto generated by entity framework).
I also read this article to make sure I was doing the right thing when it came to Fluent API.
How to define Many-to-Many relationship through Fluent API Entity Framework?
And this is my simple request on the client:
var request = new RestRequest("api/Workshops") { Method = Method.GET };
var workshopList = await api.ExecuteAsync<List<Workshop>>(request);
API/Workshops method:
// GET: api/Workshops
public IQueryable<Workshop> GetWorkshops()
{
return db.Workshops;
}
It looks like you are not using lazy loading or that part is lost when you pass the data over the API. Make sure you tell your API to include the child objects:
public IQueryable<Workshop> GetWorkshops()
{
return db.Workshops
.Include(w => w.Clients);
}
Note: You may need to add using System.Data.Entity; to use the lambda version of Include, otherwise you can use the string version.
I would recommend keeping your mappings in a separate mapping file, but if you are going to do it in the OnModelCreating method then this should do what you need.
modelBuilder.Entity<Workshop>()
.HasRequired(c => c.Clients) //or HasOptional depending on your setup
.WithMany(d => d.Workshop)
.HasForeignKey(d => new { d.ID });
modelBuilder.Entity<Clients>()
.HasRequired(c => c.Workshop) //same
.WithMany(d => d.Clients)
.HasForeignKey(d => new { d.ID });
Also in both entities add this to your ID properties:
[Key]
public int Id { get; set; }

Categories

Resources