5

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.

RMD
  • 3,421
  • 7
  • 39
  • 85
  • What about having different mappings for each of the scenarios you are dealing with? I realise this may not be ideal, but Automapper is going to generate the expression trees based on what you are projecting to, so if you don't ignore something, it will be included in the database query. – nick_w Oct 04 '14 at 10:41
  • This is used in a general purpose query object pattern I call the Query Specification Patten. The primary goal of this question is to see if I can make the pattern "safer". – RMD Oct 04 '14 at 23:45
  • By "safer", are you meaning that you would like to avoid the situation where Automapper loads related collections you do not want loaded? – nick_w Oct 05 '14 at 02:03
  • Yes. I want to allow projections, but only those that are a subset of the data I've chosen to expose. – RMD Oct 06 '14 at 03:43
  • So to go back to my original comment, would you be open to the suggestion of using multiple mappings so that you can cover each of your scenarios? – nick_w Oct 06 '14 at 04:03
  • I need to be able to configure general rules as the mappings are configured by the consumer of the query. Basically, the author of the query object defines a Query Model, which is supposed to represent a superset of all data available. The consumer of the query can then create their own View Model which, ideally, would be a subset of the Query Model. I'd like a way to enforce this. So yes, the consumers define their own View Models and I can tell the developers not to do certain stuff, but I'd rather do this at the framework level. – RMD Oct 06 '14 at 13:24

1 Answers1

8

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.

nick_w
  • 14,758
  • 3
  • 51
  • 71
  • Why are you using Set?.. on my end it worked just by disabling lazy loading, configuring the mapping for explicit expansion and doing dc.User.Include("Emails").Project().To(). – Faris Zacina Oct 07 '14 at 11:45
  • @TheZenCoder The use of Set doesn't make any difference in this case. Are you sure that code works? My understanding is that the To method generates the Select statement. – nick_w Oct 07 '14 at 12:03
  • Yes it works. i Tried it on my box earlier today with the 3.3.0-ci1027 version of automapper. – Faris Zacina Oct 07 '14 at 13:53