EF core + Automapper ID-only related entity collection, how? - c#

I have a simple problem - I would like one of the RESTful endpoints serve a resource DTO (auto-mapped) with its related resources as their IDs only. However there does not seem to be any way to implement it without loading the whole(and heavy) related entities. Consider following (DB first) example model:
public partial class Blog
{
public int Id { get; set; }
public string Url { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public partial class Post // some heavy entity
{
public int Id { get; set; }
public string Content { get; set; }
// other properties
}
and its corresponding DTO
// api/v1/blogs serves collection of following type
public class BlogSlimDto
{
public int Id { get; set; }
public string Url { get; set; }
public int[] PostIds { get; set; }
}
a straightforward soltion would be to fetch all the related Posts from database and discard all data except for the IDs, but that can be inefficient or even unfeasible depending on related Post entity size:
var result = ctx.Blogs.Include(blog => blog.Posts) //fecth everything and discard it on next line
.Select(blog => _mapper.Map<BlogSlimDto>(blog));
// simply use a profile that discards Posts but keeps their Ids, e.g.
// .forMember(dto => dto.PostIds, opt => opt.MapFrom(db.Posts.Select(p => p.Id)))
there is similar question which offers a solution using anonymous types, however this does not play well with Automapper at all:
var result = ctx.Blogs.Select(blog => new {
blog.Id,
blog.Url,
PostIds = blog.Posts.Select(b => b.Id),
}).Select(ablog => _mapper.Map<BlogSlimDto>(ablog)); //throws, no mapping and such mapping cannot be defined
The code above will throw during runtime because there no Automapper mapping defined. Even worse, it cannnot be defined because there is no support for anonymous types in Automapper. Moreover, solutions with one-by-one 'manual' property assignment tend to be difficult to maintain.
Is there an alternative solution that would allow EF query without fetching whole related entities while allowing the result to be auto-mapped to the BlogSlimDto?

You can use the queryable extensions:
https://docs.automapper.org/en/stable/Queryable-Extensions.html
var configuration = new MapperConfiguration(cfg =>
cfg.CreateMap<OrderLine, OrderLineDTO>()
.ForMember(dto => dto.Item, conf => conf.MapFrom(ol => ol.Item.Name)));
public List<OrderLineDTO> GetLinesForOrder(int orderId)
{
using (var context = new orderEntities())
{
return context.OrderLines.Where(ol => ol.OrderId == orderId)
.ProjectTo<OrderLineDTO>().ToList();
}
}
Replacing the Item and OrderLine with your Post and Blogs

Related

EF core load data from child object using FK

I have a class of Agent
public class Agent
{
public string Id { get; set; }
public string Name { get; set; }
public List<AgentUser> Users { get; set; }
}
public class User
{
public string Id { get; set; }
public string Email { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class AgentUser
{
public string Id { get; set; }
public string AgentId { get; set; }
public Agent Agent { get; set; }
public string UserId { get; set; }
public User User { get; set; }
}
the relationship is 1-to-many. on the AgentUsers table, it only contains UserId. I want to know how to load the user details(email, firstname, lastName) when I call the Agents table.
This is my query, but it only contains UserId.
var result= await _context.Agents
.Include(au => au.Users)
.Where(x => x.Id.Equals(request.Id))
.AsNoTracking()
.FirstOrDefaultAsync();
Yes I can do a JOIN but How can I get the details from User table in the most efficient way using the FK only?
Thanks!
If you want to use the joining entity in the relationships like your example then the other option when reading data is to use projection rather than loading the entire entity graph:
var result= await _context.Agents
.Where(a => a.Id.Equals(request.Id))
.Select(a => new AgentSummaryViewModel
{
AgentId = a.Id,
// other details from agent...
Users = a.Users.Select(u => new UserSummaryViewModel
{
UserId = u.User.Id,
Email = u.User.EMail,
// ...
}).ToList()
})
.SingleOrDefaultAsync();
This can be simplified using Automapper and it's ProjectTo() method against an EF IQueryable once a mapper is configured with how to build an Agent and User ViewModel. Projection has the added advantage of just loading as much data from the entity graph as your consumer (view, etc.) needs, building efficient queries. There is no need to eager load related entities or worry about tracking references since the entities themselves are not being loaded.
If you want to load that Agent and their Users for editing purposes then:
var agent = await _context.Agents
.Include(a => a.Users)
.ThenInclude(u => u.User)
.Where(a => a.Id.Equals(request.Id))
.SingleOrDefaultAsync();
Alternatively, with Many-to-Many relationships, if the joining table can be represented with just the two FKs as a composite PK then EF can map this table by convention without needing to declare the AgentUser as an entity. You only need to define the AgentUser if you want other details recorded and accessible about the relationship. So for example if you want to use a soft-delete system with an IsActive flag or such and removing an association between agent and user resulted in that record being made inactive rather than deleted, you would need an AgentUser entity.
So if you can simplify your AgentUser table to just:
AgentId [PK, FK(Agents)]
UserId [PK, FK(Users)]
... then your Entity model can be simplified to:
public class Agent
{
public string Id { get; set; }
public string Name { get; set; }
public List<User> Users { get; set; }
}
The entity mapping would need to be added explicityly such as with OnModelCreating or via an IEntityTypeConfiguration implementation that is registered so that:
modelBuilder.Entity<Agent>()
.HasMany(x => x.Users)
.WithMany(x => Agents);
Unfortunately with EF Core, many-to-many relationships mapped this way require navigation properties on both sides. (Agent has Users, and User has Agents)
EF should be able to work out the linking table by convention, but if you want more control over the table name etc. you can explicitly tell it what to use. This is where it can get a little tricky, but you can either do it by defining a joining entity, or without one, using a "shadow" entity. (Essentially a dictionary)
modelBuilder.Entity<Agent>()
.HasMany(x => x.Users)
.WithMany(x => Agents);
.UsingEntity<Dictionary<string, object>>(
"AgentUsers",
l => l.HasOne<Agent>().WithMany().HasForeignKey("AgentId"),
r => r.HasOne<User>().WithMany().HasForeignKey("UserId"),
j =>
{
j.HasKey("AgentId", "UserId");
j.ToTable("AgentUsers"); // Or "UserAgents", or "AgentUserLinks" etc.
});
Edit: A simple example with Automapper...
var config = new MapperConfiguration(cfg => {
cfg.CreateMap<Agent, AgentSummaryViewModel>();
cfg.CreateMap<User, UserSummaryViewModel>();
});
var result= await _context.Agents
.Where(a => a.Id.Equals(request.Id))
.ProjectTo<AgentSummaryViewModel>()
.SingleOrDefaultAsync();
"config" above can be extracted from a global Mapper set up with all of your mapper configurations, created ad-hoc as needed, or what I typically do is derive from mapping configuration expressions built as static methods in the view models themselves:
var config = new MapperConfiguration(AgentSummaryViewModel.BuildMapperConfig());
... then in the AgentSummaryViewModel:
public static MapperConfigurationExpression BuildConfig(MapperConfigurationExpression config = null)
{
if (config == null)
config = new MapperConfigurationExpression();
config = UserSummaryViewModel.BuildMapperConfig(config);
config.CreateMap<Agent, AgentSummaryViewModel>()
.ForMember(x => x.Users, opt => opt.MapFrom(src => src.Users.Select(u => u.User));
// append any other custom mappings such as renames or flatting from related entities, etc.
return config;
}
The UserSummaryViewModel would have a similar BuildMapperConfig() method to set up converting a User to UserSummaryViewModel. The resulting mapper configuration expression can chain as many needed related view models as you need, located under their respective view models.

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)

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; }

Entity Framework Code First IQueryable

I am using Entity Framework Code First and ran into a small road block. I have a class "Person" defined as such:
public class Person
{
public Guid Id { get; set; }
public virtual ICollection<History> History { get; set; }
}
and a class "History" defined as such:
public class History
{
public Guid Id { get; set; }
public virtual Person Owner { get; set; }
public DateTime OnDate { get; set; }
}
However, when I call:
IEnumerable<History> results = person.History
.OrderBy(h => h.OnDate)
.Take(50)
.ToArray();
It appears to pull all of the history for the person, then order it and such in memory. Any recommendations on what I'm missing?
Thanks in advance!
Because you are querying an IEnumerable (ie: LINQ to Objects) not IQueryable (ie: LINQ to Entities) given by EF.
Instead you should use
IEnumerable<History> results = context.History.Where(h => h.Person.Id = "sfssd").OrderBy(h => h.OnDate).Take(50)
This question and the accepted answer are both a bit old. Code like this would, as the original question points, load the entire history for the person from the database - not good!
var results = person
.History
.OrderBy(h => h.OnDate)
.Take(50)
.ToArray();
With EF 6 there is an easy solution. Without rearranging your query, you can have it work the IQueryable way by making use of the DbContext.Entry method, the DbEntryEntity.Collection method, and the DbCollectionEntry.Query method.
var results = dbContext
.Entry(person)
.Collection(p => p.History)
.Query()
.OrderBy(h => h.OnDate)
.Take(50)
.ToArray();

Categories

Resources