I am running into an error when using AutoMapper with an EntityFramework class model. The error is: "The ObjectContext instance has been disposed and can no longer be used for operations that require a connection." I know that this is due to navigational properties being lazy loaded unless I specifically eager load them with Include()
when querying the database.
Here is some of the code that I'm currently working with to give a better idea of what I have, and then I will explain my question in more detail.
public partial class AddressInfo //EF Model
{
public int Id { get; set; }
public int PersonalInfoId { get; set; }
public Nullable<int> AddressTypeId { get; set; }
public string Address { get; set; }
public Nullable<int> ServiceCityId { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public virtual ListItem AddressType { get; set; }
public virtual PersonalInfo PersonalInfo { get; set; }
public virtual ServiceCity ServiceCity { get; set; }
}
public class AddressInfoProfile : Profile
{
protected override void Configure()
{
CreateMap<AddressInfo, AddressInfo>();
}
}
public class AddressesViewModel : ViewModel, IBasePersonalInfoViewModel //These aren't really important to the example
{
private IMapper _mapper;
public override IMapper Mapper => _mapper ??
(_mapper = new MapperConfiguration(cfg => { cfg.AddProfile<AddressInfoProfile>(); }).CreateMapper());
public AddressInfo SelectedAddressInfo { get; set; }
private void EditAddress()
{
if (SelectedAddressInfo == null) return;
try
{
var addressClone = Mapper.Map<AddressInfo, AddressInfo>(SelectedAddressInfo); //Error is thrown here
//Do stuff with cloned address
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
}
My question is how can I leverage AutoMapper to be smart enough to determine if it is an unpopulated navigation property, then it will skip cloning that property? I can't just not map all virtual properties, because sometimes they are included, and sometimes I do need them. I also know that if I know up front which properties will be included/excluded, then I can do something like:
public class AddressesViewModel : ViewModel, IBasePersonalInfoViewModel //These aren't really important to the example
{
private IMapper _mapper;
public override IMapper Mapper => _mapper ??
(_mapper = new MapperConfiguration(cfg => { cfg.AddProfile( new AddressInfoProfile(x => x.ServiceCity, x => x.AddressType)); }).CreateMapper());
public AddressInfo SelectedAddressInfo { get; set; }
private void EditAddress()
{
if (SelectedAddressInfo == null) return;
try
{
//Error won't be thrown here because SelectedAddressInfo.PersonalInfo will be populated and the others will be ignored
var addressClone = Mapper.Map<AddressInfo, AddressInfo>(SelectedAddressInfo);
//Do stuff with cloned address
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
}
public class AddressInfoProfile : Profile
{
private readonly Expression<Func<AddressInfo, object>>[] _ignoreExpressions;
public AddressInfoProfile(params Expression<Func<AddressInfo, object>>[] ignoresExpressions)
{
_ignoreExpressions = ignoresExpressions;
}
protected override void Configure()
{
CreateMap<AddressInfo, AddressInfo>().IgnoreAll(_ignoreExpressions);
}
}
public static class Extensions
{
public static IMappingExpression<TSource, TDestination> IgnoreAll<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> map,
IEnumerable<Expression<Func<TDestination, object>>> selectors)
{
foreach (var expression in selectors)
{
map.ForMember(expression, config => config.Ignore());
}
return map;
}
}
Ultimately if I have to I will use this approach, but it would be VERY nice to not have to specify all properties to ignore in each circumstance. It would be a lot easier to make auto mapper realize that if a reference property isn't loaded, then it shouldn't try to map it. Any ideas on how this can be achieved?
Edit: I can't just decorate the navigation properties with an attribute and have it ignore all of them with that attribute because there are some certain circumstances where the properties are populated and I need to use them.
One possible solution that I'm investigating is making use of a custom value resolver and doing a try/catch when trying to get the value. If an error is caught specifying that the db context has been disposed, then just ignore it. I'll make a mockup example of what I am talking about, but keep in mind that I haven't gotten it to work yet. Suggestions on how I can make it work would be much appreciated:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NavigationPropertyAttribute : Attribute
{
}
public partial class AddressInfo //EF Model (I modified the .tt file to have it generate the attributes on the navigation properties
{
public int Id { get; set; }
public int PersonalInfoId { get; set; }
public Nullable<int> AddressTypeId { get; set; }
public string Address { get; set; }
public Nullable<int> ServiceCityId { get; set; }
public string State { get; set; }
public string Zip { get; set; }
[NavigationProperty]
public virtual ListItem AddressType { get; set; }
[NavigationProperty]
public virtual PersonalInfo PersonalInfo { get; set; }
[NavigationProperty]
public virtual ServiceCity ServiceCity { get; set; }
}
public class AddressInfoProfile : Profile
{
protected override void Configure()
{
CreateMap<AddressInfo, AddressInfo>()
.TryCatchNavigationProperties();
}
}
public static class Extensions
{
public static IMappingExpression<TSource, TDestination> TryCatchNavigationProperties<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> map)
{
var sourceType = typeof(TSource);
foreach (PropertyInfo p in sourceType.GetProperties().Where(x => x.GetCustomAttributes<NavigationPropertyAttribute>().Any()))
map.ForMember(p.Name, x => x.ResolveUsing(typeof(SkipMapIfNullResolver)).FromMember(p.Name));
return map;
}
}
public class AddressesViewModel : ViewModel, IBasePersonalInfoViewModel //These aren't really important to the example
{
private IMapper _mapper;
public override IMapper Mapper => _mapper ??
(_mapper = new MapperConfiguration(cfg => { cfg.AddProfile<AddressInfoProfile>(); }).CreateMapper());
public AddressInfo SelectedAddressInfo { get; set; }
private void EditAddress()
{
if (SelectedAddressInfo == null) return;
try
{
var addressClone = Mapper.Map<AddressInfo, AddressInfo>(SelectedAddressInfo); //Error is thrown here
//Do stuff with cloned address
}
catch (Exception ex)
{
Errors.Add(ex.Message);
}
}
}