Optional loading of EF Core computed properties - c#

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)

Related

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

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

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

Mapping Data Entity with Self-Navigating Property to Business Entity

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

Automapper Many to one map configuration

I want to map 3 different classes into a single DTO, each property have the same name on the source and the destination, the classes are the following:
User
Candidate
Portfolio
this is the DTO and how I want to map my objects:
public class CandidateTextInfo
{
public string ProfilePicture { get; set; } //-->User
public ObjectId UserId { get; set; } //-->User
public string Name { get; set; } //--> Candidate
public string Headline { get; set; } //--> Candidate
public Gender Gender { get; set; } //--> Candidate
public byte Rating { get; set; } //--> Candidate
public bool IsCompany { get; set; } //--> Candidate
public string[] Tags { get; set; } //--> Portafolio
public string[] Categories { get; set; } //--> Portafolio
public string ExecutiveSummary { get; set; } //--> Portafolio
public HourlyRate HourlyRate{ get; set; } //--> Candidate
}
I've been looking in SO and I found this solution but I don't get the method ConstructUsing
so how can I do to have a many to one mapping, is that possible, if not any workaround?
It depends greatly on the relationships between your objects. If you have a 1:1 relationship between your objects (e.g. if User has properties User.Candidate and User.Portfolio) then the mapping is easy:-
CreateMap<User, CandidateTextInfo>()
.ForMember(d => d.ProfilePicture, o => o.MapFrom(s => s.ProfilePicture)
// ...
.ForMember(d => d.Name, o => o.MapFrom(s => s.Candidate.Name)
// And so on...
If you don't have a one-to-one mapping, you need to arrange things a little bit yourself:-
public class CandidateTextInfoSource
{
public CandidateTextInfoSource(User user,
Candidate candidate,
Portafolio portafolio)
{
this.User = user;
this.Candidate = candidate;
this.Portafolio = portafolio;
}
public User User { get; set; }
public Candidate Candidate { get; set; }
public Portafolio Portafolio { get; set; }
}
// ...
CreateMap<CandidateTextInfoSource, CandidateTextInfo>()
.ForMember(d => d.ProfilePicture, o => o.MapFrom(s => s.User.ProfilePicture)
// ...
.ForMember(d => d.Name, o => o.MapFrom(s => s.Candidate.Name)
// And so on...
You can then use whatever means you require to create your CandidateTextInfoSource depending on the relationship between your objects. For example, if I assume that a User has a collection User.Candidates, and a Candidate has a property Candidate.Portfolio:-
CreateMap<User, IEnuemerable<CandidateTextInfoSource>>()
.ConstructUsing(
x => x.Candidates
.Select(y => Mapper.Map<CandidateTextInfo>(new CandidateTextInfoSource(x, y, y.Portfolio)))
.ToList());
I appreciate that this answer is very late, but if you further specify the relationship between your objects, I can help you create a more specific mapping.
Automapper's ConstructUsing is useful to build one property from custom code. In your case it is not really necessary. You just need to create the maps from your objects to your DTO. Then map each object instance to the same DTO instance.
However since Automapper wants each property of the destination object to be defined in order to ensure that the destination is fully specified you will need to configure each mapping with the properties not existing in the source object as ignored
CreateMap<Candidate, CandidateTextInfo>()
.ForMember(x=> x.ProfilePicture, opt => opt.Ignore())
.ForMember(...
// repeat for all destination properties not existing in source properties
If this is too much boilerplate code, many solutions are explored on stack overflow, among which this one looks promising: AutoMapper: "Ignore the rest"? (look at Robert Schroeder's answer)

Categories

Resources