I am trying to perform the following Automapper mapping for an OrderBy:
Expression<Func<ServerObject, object>> serverQueryable = x => x.TestEnumKVP.Value;
Mapper.Map<Expression<Func<ServerObject, object>>, Expression<Func<DatabaseObject, object>>(serverQueryable)
I want to map the ServerObject expression to a DatabaseObject expression
ServerObject defined as:
public class ServerObject
{
public KeyValuePairEx TestEnumKVP { get; set; }
}
KeyValuePairEx is a wrapper for the Enumeration which stores the Int16 value and the string value:
public enum TestEnum : Int16 { Test1, Test2, Test3 }
public class KeyValuePairEx
{
internal KeyValuePairEx(TestEnum key, string value) { }
public TestEnum Key { get; set; }
public string Value { get; set; }
}
DatabaseObject defined as:
public class DatabaseObject
{
public string TestEnumId { get; set; }
}
The Mapping I have is:
AutoMapper.Mapper.Initialize(config =>
{
config.CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP.Value, opt => opt.MapFrom(src => src.TestEnumId));
});
The mapping fails with:
'Expression 'dest => dest.TestEnumKVP.Value' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead.'
I need ServerObject.TestEnumKVP.Value to Map to DatabaseObject.TestEnumId. I am aware that Expression mappings are reversed - hence why the Map is from DatabaseObject to ServerObject. I have spent many hours on this and am at a loss as to how to get the mapping to work!
NB. I am using AutoMapper 6.1.1
Any help would be appreciated!
Thank you Lucian, I followed the github link and the solution offered by Blaise has worked. See below:
CreateMap<DatabaseObject, ServerObject>().ForMember(dest => dest.TestEnumKVP, opt => opt.MapFrom(src => src));
CreateMap<DatabaseObject, KeyValuePairEx>().ForMember(dest => dest.Value, opt => opt.MapFrom(src => src.TestEnumId));
I was starting to look for at workarounds so delighted it was possible and that the solution was so clean and concise.
Thanks again!
The error and the solution are right there in the message. Forget about all the expression stuff. The ForMember is broken. Try ForPath instead.
Expression mapping now supports ForPath. See https://github.com/AutoMapper/AutoMapper/issues/2293.
Related
I have the following two classes:
I have two classes
public class SourceClass
{
public Guid Id { get; set; }
public string Provider { get; set; }
}
public class DestinationClass
{
public Guid Id { get; set; }
public List<string> Providers { get; set; }
}
I have the following mappings for my automapper
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Providers,
memberOptions => memberOptions.MapFrom(src => new List<string> {src.Provider ?? ""}));
CreateMap<DestinationClass, SourceClass>().ForMember(SourceClass => SourceClass.Provider,
memberOptions => memberOptions.MapFrom(src => src.Providers.FirstOrDefault()));
I wrote some unit tests and can confirm the following behaviour:
When Providers in my destination class is null, it maps to null, which is great. However, I'd like to change my mapping so that if Providers is an empty list, it maps to an empty list and similarly, if Provider is null, I'd want it to map to an empty list instead of a list with an empty string.
Does anyone know how I can go about doing this? I've tried this for my mapping from SourceClass to DestinationClass:
CreateMap<SourceClass, DestinationClass>()
.ForMember(destinationMember => destinationMember.Providers,
memberOptions => memberOptions.MapFrom(src => !string.IsNullOrEmpty(src.Provider) ? new List<string> {src.Provider} : new List<string>()));
but for going the other way, an empty list is mapping to null, instead of an empty list. (I think because of FirstOrDefault() ). Does anyone know how I can work around this?
Tested this mapping with AutoMapper 6.1.1
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<SourceClass, DestinationClass>()
.ForMember(dest => dest.Providers,
opt => opt.MapFrom(src => string.IsNullOrEmpty(src.Provider) ? new List<string>()
: new List<string> { src.Provider }));
cfg.CreateMap<DestinationClass, SourceClass>()
.ForMember(dest => dest.Provider,
opt => opt.MapFrom(src => src.Providers == null ? ""
: src.Providers.FirstOrDefault() ?? ""));
});
If you really want it "inline" that will work. I still think it'd be easier to read if you moved it out into a method you could call, or if you could upgrade AutoMapper to leverage the built in converter mapping. Hopefully this gives you enough information to start.
Yep. I think its because its FirstOrDefaults( ) .. why not use the same ternary operator or an if statement to place your conditions for mapping ?
I am trying to map only 2 out of 4 properties from an object to the destination type. In my case DeletedBy and DeletedDate, where as DeletedDate will simply be set to the current UTC date.
public class DeleteCommand : IRequest
{
public string CodePath { get; set; }
[JsonIgnore]
public Guid? DeletedBy { get; set; }
[IgnoreMap]
public DeleteMode DeleteMode { get; set; } = DeleteMode.Soft;
}
This is my current configuration:
CreateMap<DeleteCommand, Asset>(MemberList.Source)
.ForMember(x => x.DeletedDate, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(x => x.DeletedBy, opt => opt.MapFrom(src => src.DeletedBy));
Running a unit test against this specific mapper configuration gives me 2 errors for a missing mapping:
[Fact]
public void MapperConfigShouldBeValid()
{
_config.AssertConfigurationIsValid();
}
Unmapped properties:
DeletedDate
DeleteMode
This is confusing me, since the Date is explicitly defined and the DeleteMode is set to be ignored by default. If possible I want to avoid to create another dto to be mapped from a first dto and then to the entity, to be soft-deleted, by setting the audit fields.
Things I've tried so far:
IgnoreMapAttribute as shown above
ForSourceMember(), seems to not support an Ignore method for a source property.
This can be solved by removing MemberList.Source from argument list of CreateMap() and ignoring all remaining unmapped destination members.
CreateMap<DeleteCommand, Asset>()
.ForMember(x => x.DeletedDate, opt => opt.MapFrom(src => DateTime.UtcNow))
.ForMember(x => x.DeletedBy, opt => opt.MapFrom(src => src.DeletedBy))
.ForAllOtherMembers(x => x.Ignore())
Same could be achieved by having CreateMap(MemberList.None). This doesn't even require explicitly ignoring all other destination members.
Removing DeletedDate as a property solved 50% of my issues, since I don't need it on the source any more.
The other one was updating the map with ForSourceMember(x => x.DeleteMode, y => x.DoNotValidate())
This then also works in a quick unit test:
[Fact]
public void DeleteMapShouldSetAuditFields()
{
var asset = new Asset();
var cmd = new DeleteCommand
{
DeletedBy = Guid.NewGuid()
};
_mapper.Map(cmd, asset);
Assert.NotNull(asset.DeletedBy);
Assert.NotNull(asset.DeletedDate);
Assert.Equal(cmd.DeletedBy, asset.DeletedBy);
}
I need to map a model object coming from API to my actual entity object on DbContext. It is used when creating a new machine object using a POST action.
As always, I created a simple map for the source/destination objects.
In this case we consider the source object as the API model and the destination object as the entity. Also the model has just a subset of properties of the entity.
Source/destination types
// Destination (entity on DbContext)
public class Machine
{
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string MnmConfiguration { get; set; }
public MachineBootStatus Status { get; set; }
public long MachineDriverId { get; set; }
public MachineDriver MachineDriver { get; set; }
public string DriverConfiguration { get; set; }
public string DriverStatus { get; set; }
}
// Source (from API controller)
public class MachineCreateModel
{
public string Name { get; set; }
public string Description { get; set; }
public string MnmConfiguration { get; set; }
public long MachineDriverId { get; set; }
}
Mapping configuration
public class DomainProfile : Profile
{
public DomainProfile()
{
//CreateMap<MachineCreateModel, Machine>();
// Update 2019/01/30 with proposed solution
CreateMap<MachineCreateModel, Machine>(MemberList.Source);
}
}
I'm using Unity DI container and the configuration of AutoMapper is this:
container = new UnityContainer();
// ... some other configurations...
container.RegisterType<IMapper, Mapper>(new InjectionConstructor(new MapperConfiguration(cfg => cfg.AddProfile<DomainProfile>())));
Version
Using AutoMapper v8.0.0.
Expected behavior
I expect to obtain a simple automatic mapping without errors, since my source model is just a subset of properties of the destination model, with same names.
Actual behavior
I get this error about unmapped properties when I hit this line of code:
Machine entity = Mapper.Map<Machine>(request.Machine);
[14:08:34.363 8 2e62361a INF] Creating new machine: TEST M1
[14:08:36.205 8 bd577466 ERR] An unhandled exception has occurred while executing the request.
AutoMapper.AutoMapperConfigurationException:
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
=================================================================================================
AutoMapper created this type map for you, but your types cannot be mapped using the current configuration.
MachineCreateModel -> Machine (Destination member list)
MyApplication.Dcs.Application.Models.MachineCreateModel -> MyApplication.Dcs.Domain.Entities.Machine (Destination member list)
Unmapped properties:
Id
Status
MachineDriver
DriverConfiguration
DriverStatus
at AutoMapper.ConfigurationValidator.AssertConfigurationIsValid(IEnumerable`1 typeMaps)
at lambda_method(Closure , MachineCreateModel , Machine , ResolutionContext )
at lambda_method(Closure , Object , Object , ResolutionContext )
at AutoMapper.Mapper.AutoMapper.IMapper.Map[TDestination](Object source)
at MyApplication.Dcs.Application.Commands.MachineCreateCommandHandler.Handle(MachineCreateCommand request, CancellationToken cancellationToken) ..Commands\MachineCreateCommand.cs:line 28
Note
In my solution I've many projects and 3 of them are making use of AutoMapper (same version for all). There're 3 different DomainProfile.cs files (1 for each project) with the needed mappings.
In the other 2 DomainProfile classes I've some manual mappings (see example below) because I need to "translate" an object with italian property names to another one with english property names. So there're many lines for each object mapping, such as:
CreateMap<ArticleCreateUpdateModel, Articoli>()
.ForMember(d => d.Categoria, opt => opt.MapFrom(src => src.Category))
.ForMember(d => d.CodiceArticolo, opt => opt.MapFrom(src => src.Code))
.ForMember(d => d.Descrizione, opt => opt.MapFrom(src => src.Description))
.ForMember(d => d.Famiglia, opt => opt.MapFrom(src => src.Family))
.ForMember(d => d.Note, opt => opt.MapFrom(src => src.Note))
...
I don't know if the usage of those manual members mapping on one or more DomainProfile class, obliges me in some way to always explain all the subsequent mappings, even if they should be simple like those of this example.
By default, AutoMapper validates the destination properties. As there are neither matching properties nor ForMember constructs for a bunch of properties in your destination type you get the exception above.
Try to validate on the source properties instead:
CreateMap<ArticleCreateUpdateModel, Articoli>(MemberList.Source)
.ForMember(d => d.Categoria, opt => opt.MapFrom(src => src.Category))
// ...
Remark:
On the other hand, I have to mention that this is the typical case when AutoMapper is an overkill. Apart from trivial cases I would never use it anymore.
I had to use it in a project for more than a year but actually it is only good for making simple things more complicated than necessary. Some FromDto and ToDto [extension] methods are just simpler, faster, easier to debug and more reactive to code changes. Mapping between different class layouts or property names often results practically as much code (or even more with tons of lambdas) as simply writing the mapping manually. See also this link.
I'm trying to project from my Order model to my OrderDTO model. Order has an enum. The problem is that projection doesn't work if I try to to get the Description attribute from the Enum. Here it's my code:
OrderStatus.cs:
public enum OrderStatus {
[Description("Paid")]
Paid,
[Description("Processing")]
InProcess,
[Description("Delivered")]
Sent
}
Order.cs:
public class Order {
public int Id { get; set; }
public List<OrderLine> OrderLines { get; set; }
public OrderStatus Status { get; set; }
}
OrderDTO.cs:
public class OrderDTO {
public int Id { get; set; }
public List<OrderLineDTO> OrderLines { get; set; }
public string Status { get; set; }
}
With this following configuration in my AutoMapper.cs:
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status,
opt => opt.MapFrom(src => src.Status.ToString())
);
Projection works, but I get an OrderDTO object like this:
- Id: 1
- OrderLines: List<OrderLines>
- Sent //I want "Delivered"!
I don't want Status property to be "Sent", I want it to be as its associated Description attribute, in this case, "Delivered".
I have tried two solutions and none of them have worked:
Using ResolveUsing AutoMapper function as explained here, but, as it's stated here:
ResolveUsing is not supported for projections, see the wiki on LINQ projections for supported operations.
Using a static method to return the Description attribute in String by Reflection.
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status,
opt => opt.MapFrom(src => EnumHelper<OrderStatus>.GetEnumDescription(src.Status.ToString()))
);
But this gives me the following error:
LINQ to Entities does not recognize the method 'System.String
GetEnumDescription(System.String)' method, and this method cannot be
translated into a store expression.
Then, how can I achieve this?
You can add an extension method like this one (borrowed the logic from this post):
public static class ExtensionMethods
{
static public string GetDescription(this OrderStatus This)
{
var type = typeof(OrderStatus);
var memInfo = type.GetMember(This.ToString());
var attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
return ((DescriptionAttribute)attributes[0]).Description;
}
}
Then access it in your map:
cfg =>
{
cfg.CreateMap<Order, OrderDTO>()
.ForMember
(
dest => dest.Status,
opt => opt.MapFrom
(
src => src.Status.GetDescription()
)
);
}
This results in what you are asking for:
Console.WriteLine(dto.Status); //"Delivered", not "sent"
See a working example on DotNetFiddle
Edit1: Don’t think you can add a local look up function like that to LINQ to entities. It would only work in LINQ to objects. The solution you should pursue perhaps is a domain table in the database that allows you to join to it and return the column that you want so that you don’t have to do anything with AutoMapper.
You can achieve an expression based enum description mapping by building up an expression that evaluates to a string containing a condition statement (such as switch/if/case depending on how the provider implements it) with the enum descriptions as the results.
Because the enum descriptions can be extracted ahead of time we can obtain them and use them as a constant for the result of the condition expression.
Note: I've used the above extension method GetDescription() but you can use whatever flavour of attribute extraction you need.
public static Expression<Func<TEntity, string>> CreateEnumDescriptionExpression<TEntity, TEnum>(
Expression<Func<TEntity, TEnum>> propertyExpression)
where TEntity : class
where TEnum : struct
{
// Get all of the possible enum values for the given enum type
var enumValues = Enum.GetValues(typeof(TEnum)).Cast<Enum>();
// Build up a condition expression based on each enum value
Expression resultExpression = Expression.Constant(string.Empty);
foreach (var enumValue in enumValues)
{
resultExpression = Expression.Condition(
Expression.Equal(propertyExpression.Body, Expression.Constant(enumValue)),
// GetDescription() can be replaced with whatever extension
// to get you the needed enum attribute.
Expression.Constant(enumValue.GetDescription()),
resultExpression);
}
return Expression.Lambda<Func<TEntity, string>>(
resultExpression, propertyExpression.Parameters);
}
Then your Automapper mapping becomes:
cfg.CreateMap<Order, OrderDTO>().ForMember(
dest => dest.Status, opts => opts.MapFrom(
CreateEnumDescriptionExpression<Order, OrderStatus>(src => src.Status)));
When this is evaluated at runtime using Entity Framework with SQL server provider, the resulting SQL will be something like:
SELECT
-- various fields such as Id
CASE WHEN (2 = [Extent1].[Status]) THEN N'Delivered'
WHEN (1 = [Extent1].[Status]) THEN N'Processing'
WHEN (0 = [Extent1].[Status]) THEN N'Paid' ELSE N'' END AS [C1]
FROM [Orders] as [Extent1]
This should also work for other Entity Framework DB providers.
For example, suppose I have the following...
public class TheSource
{
public string WrittenDate { get; set; }
}
public class TheDestination
{
public string CreateDate { get; set; }
public DateTime WrittenDate { get; set;}
}
and I have the mapping as such...
Mapper.CreateMap<TheSource, TheDestination>()
.ForMember(dest => dest.CreateDate, opt => opt.MapFrom(src => src.WrittenDate));
Question: Is the Automapper trying to map the TheSource.WrittenDate to TheDestination.WrittenDate instead of TheDestination.CreateDate as I specified in the .ForMember?
-- I ask this because I am getting an AutoMapper DateTime exception from the CreateMap line above.
Is the Automapper trying to map the TheSource.WrittenDate to TheDestination.WrittenDate instead of TheDestination.CreateDate as I specified in the .ForMember?
Not instead of TheDestination.CreateDate.
Automapper will map src.WrittenDate to dest.CreateDate because you specified that explicitly.
And it will map src.WrittenDate to dest.WrittenDate because by convention, if you don't specify otherwise, properties with the same name will be mapped to each other when you create the map.
To override this behavior, you can explcitly tell Automapper to ignore dest.WrittenDate like this:
Mapper.CreateMap<TheSource, TheDestination>()
.ForMember(dest => dest.CreateDate, opt => opt.MapFrom(src => src.WrittenDate))
.ForMember(dest => dest.WrittenDate, opt => opt.Ignore());