I'm trying to replace an existing localization mechanism by using AutoMapper => ForAllPropertyMaps.
Database entities have properties marked with an Localize attribute, so the idea is that when mapping from Entity => Dto to intercept the entity property that is marked with the Localize attribute and translate it to the corresponding language before setting the value to the Dto property.
Below is my prof of concept, but in the case of a custom mapping like the one below:
CreateMap<Entities.Product, Dto.Product>().ForMember(a => a.OfferName, opt =>
{
opt.PreCondition(m => m.ProductOffer != null);
opt.MapFrom(a => a.ProductOffer.Name);
})
I receive the ProductOffer as SourceMember instead of the ProductOffer.Name, and I didn't find any solution to identify the Name property value, so my question is, it possible to handle this types of scenarios in a generic way?
public class ProfileBase : Profile
{
public ProfileBase()
{
ForAllPropertyMaps(Condition, Configuration);
}
private static bool Condition(PropertyMap pm)
{
return pm.SourceMember != null &&
pm.SourceType == typeof(string) &&
pm.DestinationType == typeof(string) &&
pm.SourceMember.CustomAttributes.Any(a => a.AttributeType == typeof(LocalizedAttribute));
}
private static void Configuration(PropertyMap pm, IMemberConfigurationExpression opt)
{
opt.ConvertUsing(new LocalizedValueConverter());
}
}
public class LocalizedValueConverter : IValueConverter<string, string>
{
internal const string Language = "LANGUAGE";
public string Convert(string sourceMember, ResolutionContext context)
{
var language = context.Options.Items[Language]?.ToString();
if (language == null)
{
return sourceMember;
}
return GetLocalizedValue(sourceMember, language); // Custom logic for translating the sourceMember value
}
}
CreateMap<Entities.Product, Dto.Product>().ForMember(a => a.OfferName, opt =>
{
opt.PreCondition(m => m.ProductOffer != null);
opt.MapFrom(a => a.ProductOffer.Name);
})
namespace Dto
{
public class Product
{
public int Id { get; set; }
public int ProductOfferId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string OfferName { get; set; }
}
}
namespace Entities
{
public class Product
{
public int Id { get; set; }
public int ProductOfferId { get; set; }
[Localized]
public string Name { get; set; }
[Localized]
public string Description { get; set; }
public virtual ProductOffer ProductOffer { get; set; }
}
public class ProductOffer
{
public int Id { get; set; }
[Localized]
public string Name { get; set; }
[Localized]
public string Description { get; set; }
}
}
Related
With universal PredicateBuilder it is possible to build predicates without link to DbSet. For examle I have classes
public class Сountry
{
public int Id { get; set; }
public bool IsSchengen { get; set; }
}
public class Institute
{
public int Id { get; set; }
public int CountryId { get; set; }
public bool IsNational { get; set; }
public string Title { get; set; }
}
Build predicate with PredicateBuilder:
public static Expression<Func<Institute, bool>> BuilPredicate(bool isNational)
{
var predicate = new PredicateBuilder<Institute>()
.And(ao => ao.IsNational == isNational);
return predicate.Build();
}
Now I need to filter Institutes by Сountries:
public static Expression<Func<Institute, bool>> BuilPredicate(
bool isNational,
bool countryIsSchengen,
DbContext db) // <== don't want to pass link to DbContext
{
var predicate = new PredicateBuilder<Institute>()
.And(ii => ii.IsNational == isNational);
// filter by countries
// question: how to make the same without DbContext
var countyIds = db.Set<Сountry>()
.Where(cc => cc.IsSchengen == countryIsSchengen)
.Select(cc => cc.Id);
predicate.And(ii => countyIds.Contains(ii.CountryId));
return predicate.Build();
}
Is it possible to create such predicate without link to DbContext?
And without navigation Country property:
public class Institute
{
public int Id { get; set; }
// without this
// public Country Country { get; set; }
public int CountryId { get; set; }
public bool IsNational { get; set; }
public string Title { get; set; }
}
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
The error message that I'm receiving at runtime is:
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type
For no matching constructor, add a no-arg ctor, add optional arguments, or map all of the constructor parameters
List'1 -> MobileRoot (Destination member list)
System.Collections.Generic.List'1[[Strata.CS.Jazz.Biz.Dashboard.MobileInfo, Strata.CS.Jazz.Biz, Version=2019.10.0.0, Culture=neutral, PublicKeyToken=null]] -> Strata.Jazz.Web.Controllers.MobileController+MobileRoot (Destination member list)
Unmapped properties:
Users
From what I can tell from the error message is that AutoMapper needs to know how to handle the ForMember Users create in the MobileRoot, and then propagate that for each of the subsequent lists down the chain. Can anyone tell me how to do this efficiently using AutoMapper? I know how to do this with Linq using GroupBy and Select, so it is my thought that this should be do-able with AutoMapper.
The query I have returns this class:
public class MobileInfo
{
public string NameFull { get; set; }
public string EmailAddress { get; set; }
public string SolutionName { get; set; }
public int SortOrder { get; set; }
public string Name { get; set; }
public bool IsLegacy { get; set; }
public string Description { get; set; }
public string WidgetName { get; set; }
public int Row { get; set; }
public int Col { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string WidgetClassName { get; set; }
public string Data { get; set; }
}
I would like to use Automapper with profiles to have it return this:
internal class MobileRoot
{
public IEnumerable<MobileUser> Users { get; set; }
}
internal class MobileUser
{
public string NameFull { get; set; }
public string EmailAddress { get; set; }
public IEnumerable<MobileSolution> Solutions { get; set; }
}
internal class MobileSolution
{
public string Solution { get; set; } // MobileInfo.SolutionName
public int SortOrder { get; set; }
public IEnumerable<MobileDashboard> Dashboards { get; set; }
}
internal class MobileDashboard
{
public string Dashboard { get; set; } // MobileInfo.Name
public bool IsLegacy { get; set; }
public string Description { get; set; }
public IEnumerable<MobileWidget> Widgets { get; set; }
}
internal class MobileWidget
{
public string Widget { get; set; } // MobileInfo.WidgetName
public int Row { get; set; }
public int Col { get; set; }
public int Height { get; set; }
public int Width { get; set; }
public string WidgetClassName { get; set; }
public string Data { get; set; }
}
The Profiles I have defined so far are:
public class ProfileMobileRoot : Profile
{
public ProfileMobileRoot()
{
CreateMap<MobileInfo, MobileRoot>();
}
}
public class ProfileMobileUser : Profile
{
public ProfileMobileUser()
{
CreateMap<MobileInfo, MobileUser>();
}
}
public class ProfileMobileSolution : Profile
{
public ProfileMobileSolution()
{
CreateMap<MobileInfo, MobileSolution>();
}
}
public class ProfileMobileDashboard : Profile
{
public ProfileMobileDashboard()
{
CreateMap<MobileInfo, MobileRoot>();
}
}
public class ProfileMobileWidget : Profile
{
public ProfileMobileWidget()
{
CreateMap<MobileInfo, MobileWidget>();
}
}
You can do something like below. It's a little late so my solution isn't so sophisticated... but it works ;)
public class ProfileMobileRoot : Profile
{
public ProfileMobileRoot()
{
CreateMap<MobileInfo, MobileWidget>()
.ForMember(x=>x.Name, opt=>opt.MapFrom(x=>x.WidgetName));
CreateMap<IEnumerable<MobileInfo>, IEnumerable<MobileDashboard>>()
.ConvertUsing<DashboardConverter>();
CreateMap<IEnumerable<MobileInfo>, IEnumerable<MobileSolution>>()
.ConvertUsing<SolutionConverter>();
CreateMap<IEnumerable<MobileInfo>, IEnumerable<MobileUser>>()
.ConvertUsing<UserConverter>();
CreateMap<IEnumerable<MobileInfo>, MobileRoot>()
.ForMember(x => x.Users, opt => opt.MapFrom(x => x.ToList()));
}
}
class UserConverter : ITypeConverter<IEnumerable<MobileInfo>, IEnumerable<MobileUser>>
{
public IEnumerable<MobileUser> Convert(IEnumerable<MobileInfo> source, IEnumerable<MobileUser> destination, ResolutionContext context)
{
var groups = source.GroupBy(x => new { x.NameFull, x.EmailAddress});
foreach (var v in groups)
{
yield return new MobileUser()
{
EmailAddress = v.Key.EmailAddress,
NameFull = v.Key.NameFull,
Solutions = context.Mapper.Map<IEnumerable<MobileSolution>>(source.Where(x =>
v.Key.NameFull == x.NameFull && v.Key.EmailAddress== x.EmailAddress)).ToList()
};
}
}
}
class SolutionConverter : ITypeConverter<IEnumerable<MobileInfo>, IEnumerable<MobileSolution>>
{
public IEnumerable<MobileSolution> Convert(IEnumerable<MobileInfo> source, IEnumerable<MobileSolution> destination, ResolutionContext context)
{
var groups = source.GroupBy(x => new { x.SolutionName, x.SortOrder});
foreach (var v in groups)
{
yield return new MobileSolution()
{
Solution = v.Key.SolutionName,
SortOrder = v.Key.SortOrder,
Dashboards= context.Mapper.Map<IEnumerable<MobileDashboard>>(source.Where(x =>
v.Key.SolutionName== x.SolutionName&& v.Key.SortOrder== x.SortOrder)).ToList()
};
}
}
}
class DashboardConverter : ITypeConverter<IEnumerable<MobileInfo>, IEnumerable<MobileDashboard>>
{
public IEnumerable<MobileDashboard> Convert(IEnumerable<MobileInfo> source, IEnumerable<MobileDashboard> destination, ResolutionContext context)
{
var groups = source.GroupBy(x => new {x.Name, x.IsLegacy, x.Description});
foreach (var v in groups)
{
yield return new MobileDashboard()
{
Dashboard = v.Key.Name,
Description = v.Key.Description,
IsLegacy = v.Key.IsLegacy,
Widgets = context.Mapper.Map<IEnumerable<MobileWidget>>(source.Where(x =>
v.Key.IsLegacy == x.IsLegacy && v.Key.Name == x.Name && v.Key.Description == x.Description))
};
}
}
}
I have the following Entity-Models
public class Blog
{
public int Id { get; set;}
public string Title { get; set; }
public string Body { get; set; }
[ForeignKey("Category")]
public int? CategoryId { get; set; }
public virtual Category Category { get; set; }
public virtual ICollection<Comment> Comments { get; set; }
}
public class Category
{
public int Id { get; set;}
public string Name { get; set; }
}
public class Comment
{
public int Id { get; set;}
public string Title { get; set; }
public string Body { get; set; }
[ForeignKey("Blog")]
public int BlogId { get; set; }
public virtual Blog Blog { get; set; }
}
Then I have the following view-model in which I like to tell AutoMapper to map the Blog object into the BlogViewModel notice the CategoryName property will need to come from Blog.Category.Name and each Comment in the Blog.Comments need to be converter to CommentViewModel using the organic convention.
I currently set the mapping at run time using reflection for any class that implements the ICustomMap interface. Please read the comment in the code over the Transfer(IMapper mapper) method.
public class BlogViewModel : ICustomMapFrom
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string MyCatName { get; set; }
public IEnumerable<CommentViewModel> Comments { get; set; }
// **IMPORTANT NOTE**
// This method is called using reflection when the on Application_Start() method.
// If IMapper is the wrong Interface to pass, I can change
// the implementation of ICustomMap
// I assumed that `IMapper` is what is needed to add configuration at runtime.
public void Transfer(IConfigurationProvider config)
{
// How to I do the custom mapping for my MyCatName and Comments?
// I need to use the config to create the complex mapping
// AutoMapper.Mapper.Map(typeof(Blog), typeof(BlogViewModel));
}
}
Finally here is my CommentViewModel
public class CommentViewModel : IMapFrom<Comment>
{
public int Id { get; set; }
public string Title { get; set; }
public string Body { get; set; }
}
How can I tell AutoMapper how to map the CategoryName and the Comments?
Updated
Here is how I would create the mapping. I would have the following 3 interfaces
public interface IMap
{
}
public interface IMapFrom<T> : IMap
{
}
public interface ICustomMapFrom : IMap
{
void Map(IConfigurationProvider config);
}
Then in the Global.cs file
I would execute the Run method on startup. Basically this method will scan assemblies and register the classes that I would want to register using the interfaces.
public class ConfigureAutoMapper
{
public void Run()
{
var types = AssemblyHelpers.GetInternalAssemblies()
.SelectMany(x => x.GetTypes())
.Where(x => x.IsClass && !x.IsAbstract && !x.IsInterface && typeof(IMap).IsAssignableFrom(x))
.ToList();
RegisterStandardMappings(types);
RegisterCustomMappings(types);
}
private static void RegisterStandardMappings(IEnumerable<Type> types)
{
foreach (Type type in types)
{
if(type.IsGenericType && typeof(IMapFrom<>).IsAssignableFrom(type))
{
AutoMapper.Mapper.Map(type.GetGenericArguments()[0], type);
}
}
}
private static void RegisterCustomMappings(IEnumerable<Type> types)
{
foreach (Type type in types)
{
if (typeof(ICustomMapFrom).IsAssignableFrom(type))
{
ICustomMapFrom map = (ICustomMapFrom)Activator.CreateInstance(type);
var t = AutoMapper.Mapper.Configuration;
map.Map(Mapper.Configuration);
}
}
}
}
I wrote an NUnit test which sets up AutoMapper with your classes. AutoMapper supports the mapping of CategoryName out of the box.
[TestFixture]
public class TestClass
{
[Test]
public void Test1()
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Blog, BlogViewModel>();
});
config.AssertConfigurationIsValid();
var blog = new Blog()
{
Body = "Blog body",
Category = new Category { Name = "My Category" },
Comments = new List<Comment>() {
new Comment { Body = "Comment body 1" },
new Comment { Body = "Comment body 2" }
}
};
var mapper = config.CreateMapper();
var result = mapper.Map<Blog, BlogViewModel>(blog);
Assert.AreEqual(blog.Body, "Blog body");
Assert.AreEqual(blog.Category.Name, result.CategoryName);
List<CommentViewModel> comments = result.Comments.ToList();
Assert.That(comments.Any(c => c.Body == "Comment body 1"));
Assert.That(comments.Any(c => c.Body == "Comment body 2"));
}
}
i have this domain objects:
public class Societa : EquatableObject<Societa>
{
public virtual int IdSocieta { get; set; }
public virtual string NomeSocieta { get; set; }
}
public class Attivita {
public virtual int IdAttivita { get; set; }
public virtual IEnumerable<ProcessoEsaminato> Processi
}
public class ProcessoEsaminato {
public virtual ProcessoSocieta ProcessoCoperto { get; set; }
public virtual int Anno { get; set; }
}
public class ProcessoSocieta {
public override int Id { get; set; }
public virtual Societa SocietaDiretta { get; set; }
public virtual Societa SocietaService { get; set; }
}
public class Processo {
public virtual int Id { get; set; }
public virtual string NomeProcesso { get; set; }
public virtual IEnumerable<ProcessoSocieta> SocietaAttivate
}
i nedd to extract from db with QueryOver or LinqToNHibernate every Process of an Attivita with NomeProcesso, SocietaDiretta.NomeSocieta and SocietaService.NomeSocieta
So i think:
i have to start from Processo and get those that in their SocietaAttivate has one that is in Processi collection of Attivita, looking at ProcessoCoperto property of every element of this collection
i try this:
public IEnumerable<object> ProcessiPerAttivita (Attivita att) {
ProcessoSocieta ps = null;
var elencoPS = att.Processi.Select(p => p.ProcessoCoperto).ToList<ProcessoSocieta>();
return _session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.Where(x => x.SocietaAttivate.IsIn(elencoPS))
.List();
}
but Where(x => x.SocietaAttivate.IsIn(elencoPS)) is not what i nedd, since it wants only a list of id. so first question is how can i do this?
Second question is how can i select only fields i need from different object, coming from different level of aggregation?
EDIT:
now i try
_session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.Where(x => x.SocietaAttivate.Any(p => elencoPS.Contains(p)) != null)
.List();
but i get variable 'x' of type 'ProcessoSocieta' referenced from scope '', but it is not defined
Try this:
public IEnumerable<Processo> ProcessiPerAttivita (Attivita att) {
ProcessoSocieta ps = null;
var elencoPS = att.Processi.Select(p => p.ProcessoCoperto).ToList<ProcessoSocieta>();
return _session.QueryOver<Processo>()
.JoinAlias(processo => processo.SocietaAttivate, () => ps)
.WhereRestrictionOn(processo => ps.Id).IsIn(elencoPS.Select(el => el.Id).ToList())
.List<Processo>();
}
You must use the 'ps' alias!
Edit: you can use
.List<Processo>(); and return an IEnumerable<Processo>