I have a problem when using AutoMapper and EF Core together to map navigation properties from the model to the DTO. My EF classes are:
public class Meal
{
public DateTime Day { get; set; }
public MealType MealType { get; set; }
public int MealId { get; set; }
}
public class MealType
{
public string Name { get; set; }
public int MealTypeId { get; set; }
}
And the corresponding DTO classes:
public class ExistingMealDto
{
public DateTime Day { get; set; }
public ExistingMealTypeDto MealType { get; set; }
public int MealId { get; set; }
public string MealTypeName { get; set; }
}
public class ExistingMealTypeDto
{
public string Name { get; set; }
public int MealTypeId { get; set; }
}
This is my AutoMapper mapping:
config.CreateMap<DataLayer.EfClasses.MealType, ExistingMealTypeDto>();
config.CreateMap<DataLayer.EfClasses.Meal, ExistingMealDto>()
.ForMember(x => x.MealType, x => x.MapFrom(x=>x.MealType))
.ForMember(x => x.MealTypeName, x => x.MapFrom(y => y.MealType.Name));
I'm loading the data within a generic method that looks like this:
public IEnumerable<TDtoOut> GetAllAsDto<TIn, TDtoOut>()
where TIn : class
{
var allEntities = DbContext.Set<TIn>();
return Mapper.Map<IEnumerable<TDtoOut>>(allEntities);
}
When calling this code, all Meal instances are loaded from the database and MealId and Day are filled correctly. However, MealType is null and therefore ExistingMealDto.MealType is null as well. I can work around this problem by explicitly calling DbContext.MealTypes.ToList(), but since the method should be generic for TIn, this is not a production solution.
How can I solve this issue? Thanks!
For getting the related data in generic method , you can judge the Type of the passed type. The following is a test demo , you could refer to:
public IEnumerable<TIn> GetAllAsDto<TIn>()
where TIn : class
{
Type typeParameterType = typeof(TIn);
if (typeParameterType == typeof(User))
{
var Entities = _context.Set<User>().Include(u=>u.Orders);
return (IEnumerable<TIn>)Entities;
}
else
{
var allEntities = _context.Set<TIn>();
return allEntities;
}
}
public void Test()
{
var data = GetAllAsDto<User>();
var data1 = GetAllAsDto<Status>();
}
Result
Related
I've got three classes:
public class UserReport : Entity
{
public string Name { get; set; }
public string Email { get; set; }
public List<string> Departments { get; set; }
public List<string> Titles { get; set; }
}
public abstract class Entity
{
public Guid Id { get; set; }
public DateTime DateCreated { get; set; }
public DateTime DateLastModified { get; set; }
public string CreatedBy { get; set; }
public string LastModifiedBy { get; set; }
public bool Active { get; set; }
protected Entity()
{
DateCreated = DateTime.Now;
DateLastModified = DateTime.Now;
Id = Guid.NewGuid();
}
}
public class UserModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Departments { get; set; }
public string Titles { get; set; }
}
With my automapper configs set as:
CreateMap<List<string>, string>().ConvertUsing(strings => {
if (strings != null)
return string.Join("\n", strings);
return "";
});
CreateMap<UserReport, UserModel>();
When trying to call from a generic method using the Automapper Ef Extensions:
IQueryable<TModel> modelQueryable = _reportService.GetReportBaseQuery().ProjectToQueryable<TModel>();
I get this error
Missing map from System.String to System.Char. Create using Mapper.CreateMap.
GetReportBaseQuery() returns an IQueryable<TReport>, so the UserReport in this instance. I don't see any char properties, why is this coming up?
Just for testing I tried to make one:
CreateMap<String, Char>().ConvertUsing(x => x.FirstOrDefault());
And then it says:
Argument types do not match
Further research shows that this works:
Mapper.Map<List<TReport>, List<TModel>>(_reportService.GetReportBaseQuery().ToList());
But I can't use that since I need it to be a queryable returned. So something is different when I try to do an EF projection, not sure what that is though. Writing a select statement from one to the other is easy enough, but that's not generic and re-usable.
The solution is to explicitly set mapping for the Departments and Titles fields:
CreateMap<UserReport, UserModel>()
.ForMember(x => x.Departments, o => o.MapFrom(s => string.Join("\n", s.Departments)))
.ForMember(x => x.Titles, o => o.MapFrom(s => string.Join("\n", s.Titles)));
But, this does not explain, why this situation occurs.
As DOTang pointed out:
LINQ to Entities does not recognize the method 'System.String Join(System.String, System.Collections.Generic.IEnumerable1[System.String])' method, and this method cannot be translated into a store expression.
The AutoMapper extension seems to try to map the following thing:
IEnumerable<string> => IEnumerable<char>
I have created classes using EF Code First that have collections of each other.
Entities:
public class Field
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<AppUser> Teachers { get; set; }
public Field()
{
Teachers = new List<AppUser>();
}
}
public class AppUser
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public virtual List<Field> Fields { get; set; }
public AppUser()
{
Fields = new List<FieldDTO>();
}
}
DTOs:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<AppUserDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<AppUserDTO>();
}
}
public class AppUserDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
Mappings:
Mapper.CreateMap<Field, FieldDTO>();
Mapper.CreateMap<FieldDTO, Field>();
Mapper.CreateMap<AppUserDTO, AppUser>();
Mapper.CreateMap<AppUser, AppUserDTO>();
And I am getting StackOverflowException when calling this code (Context is my dbContext):
protected override IQueryable<FieldDTO> GetQueryable()
{
IQueryable<Field> query = Context.Fields;
return query.ProjectTo<FieldDTO>();//exception thrown here
}
I guess this happens because it loops in Lists calling each other endlessly. But I do not understand why this happens. Are my mappings wrong?
You have self-referencing entities AND self-referencing DTOs. Generally speaking self-referencing DTOs are a bad idea. Especially when doing a projection - EF does not know how to join together and join together and join together a hierarchy of items.
You have two choices.
First, you can force a specific depth of hierarchy by explicitly modeling your DTOs with a hierarchy in mind:
public class FieldDTO
{
public int Id { get; set; }
public string Name { get; set; }
public List<TeacherDTO> Teachers { get; set; }
public FieldDTO()
{
Teachers = new List<TeacherDTO>();
}
}
public class TeacherDTO
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string UserName => Email;
}
public class AppUserDTO : TeacherDTO
{
public List<FieldDTO> Fields { get; set; }
public AppUserDTO()
{
Fields = new List<FieldDTO>();
}
}
This is the preferred way, as it's the most obvious and explicit.
The less obvious, less explicit way is to configure AutoMapper to have a maximum depth it will go to traverse hierarchical relationships:
CreateMap<AppUser, AppUserDTO>().MaxDepth(3);
I prefer to go #1 because it's the most easily understood, but #2 works as well.
Other option is using PreserveReferences() method.
CreateMap<AppUser, AppUserDTO>().PreserveReferences();
I use this generic method:
public static TTarget Convert<TSource, TTarget>(TSource sourceItem)
{
if (null == sourceItem)
{
return default(TTarget);
}
var deserializeSettings = new JsonSerializerSettings { ObjectCreationHandling = ObjectCreationHandling.Replace, ReferenceLoopHandling = ReferenceLoopHandling.Ignore };
var serializedObject = JsonConvert.SerializeObject(sourceItem, deserializeSettings);
return JsonConvert.DeserializeObject<TTarget>(serializedObject);
}
...
MapperConfiguration(cfg =>
{
cfg.ForAllMaps((map, exp) => exp.MaxDepth(1));
...
When you giving 1 navigation_property to 2nd entity and visa-versa it go in an infinite loop state. So, the compiler automatically throws a Stackoverflow exception.
So, to avoid that, you just need to remove one navigation_property from any of the entities.
I'm trying to flatten a nested object into a DTO object in .NET 3.5. Most of what I've seen so far is to use AutoMapper to do this (using v1.x since I need to use .NET 3.5, unfortunately):
Here's what a snippet of my class structures look like:
public class RootObject
{
[JsonProperty("BaseSupplier")]
public BaseSupplier BaseSupplier { get; set; }
[JsonProperty("BaseOrderShipmentLineitem")]
public IList<BaseOrderShipmentLineitem> BaseOrderShipmentLineitem { get; set; }
}
public class BaseSupplier
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
}
public class BaseOrderShipmentLineitem
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("qty_delivered")]
public int QtyDelivered { get; set; }
[JsonProperty("BaseOrderLineitem")]
public BaseOrderLineitem BaseOrderLineitem { get; set; }
}
public class BaseOrderLineitem
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("product_sku")]
public string ProductSku { get; set; }
}
public class ShipmentDetailsDTO
{
public int BaseOrderShipmentLineitemId { get; set; }
public string BaseSupplierName { get; set; }
public string Sku { get; set; }
}
I've been trying something like this:
Mapper.CreateMap<BaseOrderLineitem, ShipmentDetailsDTO>()
.ForMember(d => d.Sku, opts => opts.MapFrom(s => s.ProductSku));
Mapper.CreateMap<BaseOrderShipmentLineitem, ShipmentDetailsDTO>();
Mapper.CreateMap<RootObject, ShipmentDetailsDTO>()
.ForMember(d => d.Sku, opts => opts.MapFrom(s => Mapper.Map<IEnumerable<BaseOrderLineitem>, IEnumerable<ShipmentDetailsDTO>>(s.BaseOrderShipmentLineitem.SelectMany(q => q.BaseOrderLineitem)).FirstOrDefault().Sku))
;
var model = Mapper.Map<IEnumerable<RootObject>, IEnumerable<ShipmentDetailsDTO>>(obj);
With that above code I'm getting an error on this bit s.BaseOrderShipmentLineitem.SelectMany(q => q.BaseOrderLineitem):
Cannot implicitly convert type 'IEnumerable<?>' to 'IEnumerable<BaseOrderLineitem>'. An explicit conversion exists (are you missing a cast?)
I'm not sure if it's something simple I'm just overlooking or not.
A far easier way is to write a simple extension method using some LINQ to do this projection yourself. It's easier and more transparent:
public static class MyConversionExtensions
{
public static IEnumerable<ShipmentDetailsDTO> ToShipmentDetails(this RootObject root)
{
return root.BaseOrderShipmentLineitem.Select(x => new ShipmentDetailsDTO() {
BaseOrderShipmentLineitemId = x.BaseOrderLineitem.Id,
BaseSupplierName = root.BaseSupplier.Name,
Sku = x.BaseOrderLineitem.ProductSku
});
}
}
Usage:
var shipmentDetails = myRootObject.ToShipmentDetails();
I am having two classes :
source.Employee and destination.Employee .
I am getting just two name in my function destination property name i.e destination.TestEnum1 and source property name i.e source.TestEnum1.
I want to create expression dynamically as i mentioned below.
var mapExpr = Mapper.CreateMap<Soure.Employee, destination.Employee>().ForMember(destination => destination.TestEnum1, opt => opt.MapFrom(source => (destination.MyEnum2)source.TestEnum1));
The expression is just
destination => destination.TestEnum1, opt => opt.MapFrom(source => (destination.MyEnum2)source.TestEnum1)
I am creating it to map Enum in Project().To();.
As
Mapper.CreateMap<Soure.MyEnum1, destination.MyEnum2>()
gives exception unable to map MyEnum2 to int 32.
Source employee :
namespace Soure
{
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public Department dept1 { get; set; }
public int age { get; set; }
public MyEnum1 TestEnum1 { get; set; }
}
public enum MyEnum1
{
red = 1,
yellow = 2
}
}
destination employee class :
namespace destination
{
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public int age { get; set; }
public MyEnum2 TestEnum1 { get; set; }
public Departments dept1 { get; set; }
}
public enum MyEnum2
{
red = 1,
yellow = 2
}
}
You might think of using Custom Type Converters for this, found here.
Here I made a sample application for you (simplified a bit) where I implemented a Enum1to2TypeConverter inheriting from the generic interface ITypeConverter:
public class Employee1
{
public int Id { get; set; }
public string Name { get; set; }
public MyEnum1 TestEnum { get; set; }
}
public enum MyEnum1
{
red = 1,
yellow = 2
}
public class Employee2
{
public int Id { get; set; }
public string Name { get; set; }
public MyEnum2 TestEnum { get; set; }
}
public enum MyEnum2
{
red = 1,
yellow = 2
}
public class Enum1to2TypeConverter : ITypeConverter<MyEnum1, MyEnum2>
{
public MyEnum2 Convert(ResolutionContext context)
{
return (MyEnum2)(context.SourceValue);
}
}
public class Test
{
public void Example()
{
Mapper.CreateMap<MyEnum1, MyEnum2>().ConvertUsing(new Enum1to2TypeConverter());
Mapper.CreateMap<Employee1, Employee2>();
Mapper.AssertConfigurationIsValid();
var source = new Employee1
{
Id = 1,
Name = "Employee1-Name",
TestEnum = MyEnum1.yellow
};
Employee2 result = Mapper.Map<Employee1, Employee2>(source);
//Check content of result here!
}
}
class Program
{
private static void Main(string[] args)
{
(new Test()).Example();
}
}
As the enums in your example share the same underlying int types, automapper will handle this automatically as follows:
Mapper.CreateMap<Foo, Bar>()
.ForMember(dest => dest.MyEnum2, opt => opt.MapFrom(src => src.MyEnum1))
;
var source = new Foo { MyEnum1 = MyEnum1.yellow };
var destination = Mapper.Map<Bar>(source);
Console.WriteLine(destination.MyEnum2);
Working Fiddle
You can check out ConvertProjectionUsing, which is the ConvertUsing for LINQ.
Mapper.CreateMap<MyEnum1, MyEnum2>().ProjectUsing(src => ???);
What should you put inside that ???, I'm not sure, it will depend on your query provider. EF may or may not be able to take that projection and convert it to SQL. You might be able to cast the source to int:
Mapper.CreateMap<MyEnum1, MyEnum2>().ProjectUsing(src => (MyEnum2)(int)src);
That is completely dependent on your query provider, though.
I am having an issue where automapper is trying to map a property that I do not want it to map. I can fix this issue by renaming the property to something else but I want to use the name and I want to know how to fix issues like this.
My code looks like this.
public abstract class Task
{
public int Id { get; set; }
public int ProjectId { get; set; }
public virtual Project Project { get; set; }
public string Title { get; set; }
}
public class BasicTask : Task
{
}
public abstract class Project
{
public virtual int Id { get; set; }
public virtual string Title { get; set; }
public virtual ICollection<Task> Tasks { get; set; }
[NotMapped]
public abstract string ProjectTypeDescription { get; }
}
public class BasicProject : Project
{
public override string ProjectTypeDescription
{
get { return "Basic Project"; }
}
}
public abstract class TaskViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public abstract string TaskType { get; }
//This is the property giving me issues
public ProjectDetails Project { get; set; }
public class ProjectDetails
{
public int Id { get; set; }
public string Title { get; set; }
}
}
public class BasicTaskViewModel : TaskViewModel
{
public override string TaskType
{
get { return "Basic"; }
}
}
public abstract class ProjectViewModel
{
public int Id { get; set; }
public string Title { get; set; }
public SelectList ProjectTypes { get; set; }
public abstract string ProjectType { get; }
public IEnumerable<TaskViewModel> Tasks { get; set; }
}
public class BasicProjectViewModel : ProjectViewModel
{
public override string ProjectType
{
get { return "Basic Project"; }
}
}
My mapping for Tasks looks like (I removed the project mapping since I can replicate the problem without those mappings)
Mapper.CreateMap<Task, TaskViewModel>()
.ForMember(dest => dest.Project, opt => opt.Ignore())
.Include<BasicTask, BasicTaskViewModel>();
Mapper.CreateMap<BasicTask, BasicTaskViewModel>();
Mapper.CreateMap<TaskViewModel, Task>()
.Include<BasicTaskViewModel, BasicTask>();
Mapper.CreateMap<BasicTaskViewModel, BasicTask>();
Mapper.CreateMap<Project, TaskViewModel.ProjectDetails>();
I am use an extenstion method for the mapping
public static TResult MapTo<TResult>(this object self)
{
return (TResult)Mapper.Map(self, self.GetType(), typeof(TResult));
}
An example of using this that fails is
TaskViewModel vm = new BasicTaskViewModel()
{
Id = 1,
Project = new TaskViewModel.ProjectDetails()
{
Id = 1,
Title = "Some Title",
}
};
Task m = vm.MapTo<Task>();
Sorry for the very long post I just don't know where the problem is.
If I rename Project in the taskviewmodel to something else it works and doesn't map TaskViewModel.ProjectDetails to Project which is what I want.
I have tried adding ForSourceMember(src => src.Project, opt => opt.Ignore()) in every spot that I can
When I run this I get the follwoing
Missing type map configuration or unsupported mapping.
Mapping types:
ProjectDetails -> Project
amtest.ViewModel.TaskViewModel+ProjectDetails -> amtest.Models.Project
Destination path:
BasicTask.Project.Project
Source value:
amtest.ViewModel.TaskViewModel+ProjectDetails
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: AutoMapper.AutoMapperMappingException: Missing type map configuration or unsupported mapping.
Mapping types:
ProjectDetails -> Project
amtest.ViewModel.TaskViewModel+ProjectDetails -> amtest.Models.Project
Destination path:
BasicTask.Project.Project
Source value:
amtest.ViewModel.TaskViewModel+ProjectDetails
Does it help when you add the ignore also to the mapping of the child class?
Mapper.CreateMap<BasicTaskViewModel, BasicTask>()
.ForMember(dest => dest.Project, opt => opt.Ignore());