Prevent AutoMapper Projections From Forcing Related Data Loading - entity-framework

Is there a way to configure AutoMapper to adhere to the .Include style loading instructions for Entity Framework?
I've disabled lazy loading for my context, and I want to conditionally load related data for particular entities. Ideally, I'd like to do this by using an include syntax. Something like:
if(loadAddreses)
{
query = query.Include(e => e.Addresses);
}
if(loadEmails)
{
query = query.Include(e => e.Emails);
}
The problem is, AutoMapper is seeing that the model I'm projecting to includes Addresses and E-mails, and is generating SQL that loads all that data regardless of what I've asked EF to include. In other words:
var model = query.Project.To<MyModel>();
If MyModel has an Addresses collection, it will load addresses, regardless of my Include statements.
Short of changing my model so that I have one that doesn't have an Addresses or Emails property, is there a way to fix this? I suppose I could change my mapping, but mappings are usually static and don't change after they're initially created.

This was kind of tricky to tease out, but see how this works for you. Note that I'm using version 3.3.0-ci1027 of AutoMapper (at the time of writing this was a pre-release).
Assume my data model looks like this:
public class Address
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int AddressId { get; set; }
public string Text { get; set; }
}
public class Email
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int EmailId { get; set; }
public string Text { get; set; }
}
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual ICollection<Address> Addresses { get; set; }
public virtual ICollection<Email> Emails { get; set; }
public User()
{
this.Addresses = new List<Address>();
this.Emails = new List<Email>();
}
}
My view models are not specified but they just contain the same properties as the entities.
My mapping from User to UserViewModel looks like this:
Mapper.CreateMap<User, UserViewModel>()
.ForMember(x => x.Emails, opt => opt.ExplicitExpansion())
.ForMember(x => x.Addresses, opt => opt.ExplicitExpansion());
And my projection looks like this:
var viewModels = context.Set<User>().Project()
.To<UserViewModel>(new { }, u => u.Emails).ToList();
With that mapping and projection, only the Emails collection is loaded. The important parts to this are the opt => opt.ExplicitExpansion() call in the mapping - which prevents a navigation property being followed unless explicitly expanded during projection, and the overloaded To method. This overload allows you to specify parameters (which I've left as an empty object), and the members you wish to expand (in this case just the Emails).
The one thing I'm not sure of at this stage is the precise mechanism to extract the details from the Include statements so you can in turn pass them into the To method, but hopefully this gives you something to work with.

Related

EF6:How to include subproperty with Select so that single instance is created. Avoid "same primary key" error

I'm trying to fetch (in disconnected way) an entity with its all related entities and then trying to update the entity. But I'm getting the following error:
Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
public class Person
{
public int PersonId { get; set; }
public string Personname { get; set }
public ICollection Addresses { get; set; }
}
public class Address
{
public int AddressId { get; set; }
public int PersonId { get; set; }
public string Line1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public Person Person { get; set; }
public ICollection<Feature> Features { get; set; }
}
// Many to Many: Represented in database as AddressFeature (e.g Air Conditioning, Central Heating; User could select multiple features of a single address)
public class Feature
{
public int FeatureId { get; set; }
public string Featurename { get; set; }
public ICollection<Address> Addresses { get; set; } // Many-To-Many with Addresses
}
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
var person = dbContext.People.AsNoTracking().Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}
public void UpdateCandidate(Person newPerson)
{
Person existingPerson = GetPerson(person.Id); // Loading the existing candidate from database with ASNOTRACKING
dbContext.People.Attach(existingPerson); // This line is giving error
.....
.....
.....
}
Error:
Additional information: Attaching an entity of type 'Feature' failed because another entity of the same type already has the same primary key value.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Kindly suggest.
It seems like (I may be wrong) GetCandidate is assigning every Feature within Person.Addresses a new instance. So, how could I modify the GetCandidate to make sure that the same instance (for same values) is bing assisgned to Person.Addresses --> Features.
Since you are using a short lived DbContext for retrieving the data, all you need is to remove AsNoTracking(), thus allowing EF to use the context cache and consolidate the Feature entities. EF tracking serves different purposes. One is to allow consolidating the entity instances with the same PK which you are interested in this case, and the second is to detect the modifications in case you modify the entities and call SaveChanges(), which apparently you are not interested when using the context simply to retrieve the data. When you disable the tracking for a query, EF cannot use the cache, thus generates separate object instances.
What you really not want is to let EF create proxies which hold reference to the context used to obtain them and will cause issues when trying to attach to another context. I don't see virtual navigation properties in your models, so most likely EF will not create proxies, but in order to be absolutely sure, I would turn ProxyCreationEnabled off:
public Person GetCandidate(int id)
{
using (MyDbContext dbContext = new MyDbContext())
{
dbContext.Configuration.ProxyCreationEnabled = false;
var person = dbContext.People.Where(x => x.PersonId == id);
person = person.Include(prop => prop.Addresses.Select(x => x.Country)).Include(prop => prop.Addresses.Select(x => x.Features));
return person.FirstOrDefault();
}
}

AutoMapper - Trying to pass variables from WEB API Controller to Automapper configuration at runtime

I am working on a large project that uses AutoMapper to map entities to DTO's - this works perfectly for simple mappings but I've come unstuck at the moment trying to get some custom mappings to work.
This is a simplified example of what I am trying to do. I have a Product, Price and Store class:
public class Product
{
public Guid Id { get; set; }
public ICollection<Price> Prices { get; set; }
}
public class Price
{
public Guid Id { get; set; }
public Guid ProductId { get; set; }
public Guid StoreId { get; set; }
public decimal Value { get; set; }
public virtual Product Product { get; set; }
public virtual Store Store { get; set; }
}
Each class also has a DTO. When I access a ProductDTO class by default I will get all prices for all stores. However, when a particular store logs in to the application I want to limit the prices they see to their store only.
I would like AutoMapper to be able to handle this so that when my WEB API Controller calls into the Repository to get a ProductDTO, the list of Prices are automatically filtered to include ONLY the currently logged in stores' prices.
I have set up my AutoMapper configuration in the Startup class of my WEB API project. I know I could set up a custom mapping like so:
Mapper.Configuration.CreateMap<Product, ProductDto>()
.ForMember(pd => pd.Prices,
opt => opt.MapFrom(p => p.Prices.Where(pp => pp.StoreId == storeId));
but I only know what the currently logged in store ( the storeId ) is once the request comes into the WEB API Controller. So, is it possible to set up my configuration in a way that I can update the configuration during runtime or is there a better way of tackling this altogether ? This is a very simple example - there are hundreds of similar scenarios in this project I am working on ( not limited to filtering by Store ID ) so I would like to implement something as generic as possible.
You need to use parameterized LINQ query support in AutoMapper:
int storeId = 0;
Mapper.Configuration.CreateMap<Product, ProductDto>()
.ForMember(pd => pd.Prices,
opt => opt.MapFrom(p => p.Prices.Where(pp => pp.StoreId == storeId));
Then in your controller:
dbContext.Products.ProjectTo<ProductDto>(new { storeId = currentLoggedInStoreId });
AutoMapper will pass that parameter based on it matching "storeId" to your "Where" expression's variable at runtime.

EF code first related entities not loading at all

I haven't been able to find someone else with this issue specifically so here goes.
I have a simple model where one entity simply references another as a parent-child or one-to-many relationship defined like this:
public class Parent
{
public int ID { get; private set; }
public string Name { get; private set; }
}
public class Child
{
public int ID { get; private set; }
public string Name { get; private set; }
public virtual Parent Parent { get; private set; }
}
I am creating speicific mapping files for each, which work great for all the normal properties except for the related entity. It is always coming up null. No matter whether i have the virtual/private accessors on the property it will not load UNLESS i pull a copy of the parent separately from the context first. My mapping looks like this:
HasRequired(t => t.Parent).WithMany().Map(t => t.MapKey("ParentID")).WillCascadeOnDelete();
Is there anything I am doing wrong with this? I cannot for the life of me figure this out. Just so I cover all the bases, I am loading the entity like this:
Context.Set<Child>().FirstOrDefault(x => x.ID == 1);
And lastly here are some constraints I have:
I cannot have the foreign keys in my model as properties.
I cannot have a collection of children from the parent.
I finally figured it out. After much trial and error I noticed that having a parameterless constructor marked as internal, EF cannot create its dynamic proxy class of your type and therefore disables all lazy loading. I have two contructors, one for EF to hydrate objects, and another with parameters requires for callers to create my entity. Once I changed the signature to protected internal it started working. So I changed this:
internal Child() {}
to
protected internal Child() {}
May be you hasn't enable lazy loading .Try this,
Context.Set<Child>().FirstOrDefault(x => x.ID == 1).Include(c=>c.Parent);

Including Hierarchy in Entity Framework Query

I have an object hierarchy that looks something like this:
public class Book
{
public virtual List<Page> Pages { get; set; }
public virtual List<Paragraph> Paragraphs { get; set; }
}
public class Page
{
public virtual List<Paragraph> Paragraphs { get; set; }
}
I want to load the complete object hierarchy and am going about that like this:
Book book = (from b in context.Books.Include("Pages").Include("Paragraphs")
.Include("Pages.Paragraphs") where CONDITION).SingleOrDefault();
I find that book.Pages and book.Paragraphs are loaded, but book.Pages[i].Paragraphs is null.
Examining the database, the data looks correct (association columns are all correctly populated).
I also tried the lamda syntax, but do not see how that could work when the parameter is a collection rather than an entity, e.g. one can do something like this:
.Include(s => s.Paragraphs.Select(p => p.Id == 1)
but I do not see how one could use the lamda syntax to specify that the Paragraphs collection for each Page in book.Pages should be loaded.
Am I missing something, or is this a limitation of Entity Framework? If it's a limitation, how can I work around it?
In the actual code (not the code boiled down for the purpose of asking a targeted question, I was missing a virtual keyword. EF didn't complain about an inability to load the additional data requested through .Include("Pages.Paragraphs"), it just silently ignored that request.
public class Page
{
public /* was missing: virtual*/ List<Paragraph> Paragraphs { get; set; }
}

Entity Framework Code First - Restoring collections of the same type

I'm using Entity Framework Code First. The class i'm trying to create contains two collections (of the same type). I'm having problem recovering my respective collections.
My classes look like this:
public class Destination
{
public int DestinationId { get; set; }
public string Name { get; set; }
public List<Lodging> Lodgings { get; set; }
public List<Lodging> Lodgings2 { get; set; }
}
public class Lodging
{
public int LodgingId { get; set; }
public string Name { get; set; }
public Destination Destination { get; set; }
}
I created a new Destination, then I reopened (closed & opened) the database connection. When I retrieve the destination, my collections (dest.Lodgings and dest.Lodgings2) are null. How do I restore the respective collections? If my class only has one collection of a particular type, I could do the following:
var lodgings = context.Lodgings.Where(l => l.Destination.DestinationId == destId).ToList();
I can see that the relationships are maintained in the database schema (Destination_DestinationId1 and Destination_DestinationId2) but I don't seem to be able to get to them.
Any suggestion would be appreciated.
In addition to using Include (as you've discovered) (which loads the related data from the db at the same time the destination is retrieved) you can also retreive the lodgings after the fact. So if you query for the destination and then you want the lodgings, that's possible. One way is called explicit loading where you will use a Load method. The other is with lazy loading, which requires that your classes be set up a particular way and just the mere mention of the Lodgings property will trigger the call to the database to retrieve them.
there's a great blog post on the Ef team blog about the various ways to load related data with DbContext : http://blogs.msdn.com/b/adonet/archive/2011/01/31/using-dbcontext-in-ef-feature-ctp5-part-6-loading-related-entities.aspx
hth
Julie