I have class(its part of it):
Filter { string CashierId; }
And
ClientQuery { IList<Person> Persons; }
Where Person looks:
Person {
SolvingPersonIn();
SolvingPersonIn(string solvingPersonXnuc = null);
string SolvingPersonXnuc { get; set; }
}
And how I can configure Automapper to map my Filter to ClientQuery?
Something like this:
cashierId = "12345678";
ClientQuery.Persons should be one element with "12345678"
Automapper is an object-object mapper, that means from an object it will convert it to another object. You can apply filter when you are converting data like this in this thread example our to filtering a collection:
Mapper.CreateMap<Customer, CustomerViewModel>()
.ForMember(dest => dest.Orders,
opt => opt.MapFrom(src => src.Orders.Where(o => !o.DeletedDate.HasValue)));
You can for example in you case take only the first one or you test your data beforehand then apply your normal mapping.
if (ClientQuery.Persons.Count(x => x.SolvingPersonXnuc) > 1)
{
// your logic here when you have more than one person with the CashierId
}
// apply your mapping here
You can have a read at the official documentation, there are a lot of examples Automapper documentation
If you only want to add a single CashierId to the List of Persons, you can create and populate a new list:
CreateMap<Filter, ClientQuery>
.ForMember(dest => dest.Persons,
o => o.ResolveUsing(src => {
return new List<Person> { new Person { SolvingPersonXnuc = src.CashierId }};
})
);
Related
I have a model which has a generic properties property looking something like this:
public class GenericProperty
{
public string Name { get; set; }
public object Value { get; set; }
}
Next to that I have a object that has a list with GenericProperties like this:
public class GenericEntity
{
public string Name { get; set; }
public List<GenericProperty> Properties { get; set; }
}
Now im calling a API that deserialize the json to the model above. Next i want to use AutoMapper to construct an actual good looking model so i did the following:
Mapper.Initialize(x =>
{
x.CreateMap<GenericEntity, MyModel>()
.ForMember(d => d.ManagerId, o => o.MapFrom(s => s.Properties.Where(n => n.Name == "ManagerId" ).Select(v => v.Value)))
});
Problem is that this returns the property type and not the actual value:
System.Linq.Enumerable+WhereListIterator`1[MyProject.Models.GenericProperty]
How can i lookup the value of within the model?
By using Where you are selecting all the properties with the name ManagerId. You should instead use Single like in this unit test:
public class TestClass
{
[Test]
public void TestMapper()
{
Mapper.Initialize(x =>
{
x.CreateMap<GenericEntity, MyModel>()
.ForMember(d => d.ManagerId,
o => o.MapFrom(s => s.Properties.Single(n => n.Name == "ManagerId").Value));
});
var ge = new GenericEntity
{
Properties = new List<GenericProperty>
{
new GenericProperty {Name = "ManagerId", Value = "Great"}
}
};
var myModel = Mapper.Map<MyModel>(ge);
Assert.AreEqual("Great", myModel.ManagerId);
}
}
If you can not guarantee that there will be a property with the name ManagerId, you should use SingleOrDefault, and handle the null case.
IMHO, you should limit this kind of AutoMapper configurations to simple cases, since it quickly gets difficult to debug and maintain.
For more complex mappings like this one, I would recommend you to make an explicit mapping method instead, which you can call like an extension method. Another option worth considering if you want to use Automapper are Custom value resolvers.
I have the following classes:
public class Entity
{
public string Name { get; set; }
}
public class SomethingDto
{
public string NameChanged { get; set; }
public void Mapping(Entity something)
{
NameChanged = something.Name;
}
}
I want to use the Mapping Method of the DTO to create the map as the following way:
conf.CreateMap<Entity, SomethingDto>().ForMember(t => t.NameChanged, opt => opt.MapFrom(t => t.Name));
There is a way in AutoMapper to create the maps with custom methods, who works with his projection?
You don't want to do it like that, because that makes the DTO aware of the entity and that would throw out the separation you'd get.
Now in this case, the line ForMember(t => t.NameChanged, opt => opt.MapFrom(t => t.Name)) will work because Name and NameChanged are both of type string. Say you'd like to do something along the lines of mapping identifier of type string with value '20180120-00123456' to two properties on the destination: a DateTime property and a ProductId property. You can do this two ways.
Simple
You would write two mapping functions in the class where you make the mapping and do it along the lines of:
ForMember(t => t.Date, opt => opt.MapFrom(t => RetrieveDate(t.Identifier)))
ForMember(t => t.ProductId, opt => opt.MapFrom(t => RetrieveProductId(t.Identifier)))
Complex
You would make a custom class OrderIdentifier (now I'm assuming the identifier is for an order) with only the Id property as string. Then you'd make two custom type converters, like the article describes.
Is there a way to pass the properties I want to retrieve from the DB in a Select dynamically, I don't know the properties I need beforehand and I don't want to write the conditions in my repository.
I don't want to retrieve all the fields at once, just the ones I need based on some conditions.
For example:
public class Student
{
public string Property1 {get; set;}
public string Property2 {get; set;}
//other properties here
[NotMapped]
public string Selected
{
if(condition)
return Property1;
else
return Property2;
}
}
and in the service layer I have
query.Select(s => new StudentViewModel
{
Value = s.Selected; //this one will get the property we want based on a condition
//other stuff here
//OtherValue = s.OtherProperty
}
).FirstOrDefault();
Selector
An easy but ugly way is to use a Selector:
query.Select(Selector()).FirstOrDefault();
And the Selector can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if (Condition())
return x => new StudentViewModel
{
Name = x.Property1,
OtherName = x.OtherName
};
return x => new StudentViewModel
{
Name = x.Property2,
OtherName = x.OtherName
};
}
As you can see the obviously downside here is that you need to copy/paste all other selected properties. That is why its ugly.
AutoMapper
Configs
You can use AutoMapper with different configurations. First you need to define a standard mapping for all properties that don't need to be dynamic.
public static void AddStandardStudentMap(this IMappingExpression<Student, StudentViewModel> map)
{
map.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherProperty))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherProperty2));
// you can concat .ForMember() for each property you need.
}
Next, you need to define the different configs and add the AddStandardStudentMap method to each invidual mapping.
var config1 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property1))
.AddStandardStudentMap()
);
var config2 = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Property2))
.AddStandardStudentMap()
);
After this, just use your conditions to decide which config do you need
IConfigurationProvider provider;
if(Condition())
provider = config1;
else
provider = config2;
And then instead of .Select() use:
query.ProjectTo<StudentViewModel>(provider).FirstOrDefault();
As we can see this solution is still ugly and has a lot of overhead but its needed in some cases, thats why i stated it here.
Expression
This is a bit similar to the Configs but brings you more flexibility and less writing effort.
First create a config but this time with a Selector
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<Student, StudentViewModel>()
.ForMember(dest => dest.OtherName, opt => opt.MapFrom(src => src.OtherName))
.ForMember(dest => dest.OtherName2, opt => opt.MapFrom(src => src.OtherName2))
// and so on. Map all your properties that are not dynamically.
// and then the Selector
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => Selector()))
);
The Selector method can look like this:
private static Expression<Func<Student, StudentViewModel>> Selector()
{
if(Condition())
return src => src.Property1;
else
return src => src.Property2;
}
And then just use it like the configs solution but without selecting a particular config:
query.ProjectTo<StudentViewModel>(config).FirstOrDefault();
Conclusion
I know this is a lot input and there are even more possibilities to achieve the behaviour that you want, with or without AutoMapper. But i would suggest you (grounded on the information you gave us) to use AutoMapper with Expressions. It should give the flexibility you need and is extensible for further requirements.
public class Person
{
Name { get; set; }
IEnumerable<Address> Addresses { get; set; }
}
public class PersonModel
{
Name { get; set; }
IEnumerable<AddressModel> Addresses { get; set; }
}
If I map Person to PersonModel like so:
Mapper.DynamicMap<Person, PersonModel>(person);
If the Addresses property on Person is null they are mapped on PersonModel as an empty Enumerable instead of null.
How do I get PersonModel to have null Addresses instead of an empty Enumerable?
The simple answer is to use AllowNullCollections:
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.AllowNullCollections = true;
});
or if you use the instance API
new MapperConfiguration(cfg =>
{
cfg.AllowNullCollections = true;
}
In addition to setting AllowNullCollections in the mapper configuration initialization (as noted in this answer), you have the option to set AllowNullCollections in your Profile definition, like this:
public class MyMapper : Profile
{
public MyMapper()
{
// Null collections will be mapped to null collections instead of empty collections.
AllowNullCollections = true;
CreateMap<MySource, MyDestination>();
}
}
So there are probably several ways you can accomplish this with Automapper, and this is just one:
Mapper.CreateMap<Person, PersonMap>()
.AfterMap( (src, dest) => dest.Addresses = dest.Addresses?.Any() ? dest.Addresses : null );
This code uses the new c# ?. operator for null safety, so you might need to remove that and check for null if you can't use that feature in your code.
Another alternative to this is to use a condition, so only map the value when the value is not null.
This may require that the value default to null (as it wont be mapped)
Mapper.CreateMap<Person, PersonModel>()
.ForMember(
dest => dest.Addresses,
opt => opt => opt.Condition(source=> source.Addresses!= null));
You should be able to define a custom resolver for the property you want this behaviour on. So something like:
Mapper.CreateMap<Address, AddressModel>();
Mapper.CreateMap<Person, PersonModel>()
.ForMember(
dest => dest.Addresses,
opt => opt.ResolveUsing(person => person.Addresses.Any() ? person.Addresses.Select(Mapper.Map<Address, AddressModel>) : null));
if you want this as a general rule in your API, you could configure Automapper in the configure services method like this.
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers();
[...]
// configure automapping
services.AddAutoMapper(cfg => cfg.AllowNullCollections = true, typeof(Startup));
}
Or, in the case of using Automapping in a separate DLL (example: for DTO services), I prefer to use a helper function, so the configuration should be done there too:
public static class MappingHelper
{
private static readonly Lazy<IMapper> _lazy = new(() =>
{
var config = new MapperConfiguration(cfg =>
{
// This line ensures that internal properties are also mapped over.
cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.GetMethod.IsAssembly;
cfg.AddProfile<DomainToRepositoryProfile>();
cfg.AddProfile<RepositoryToDomainProfile>();
cfg.AllowNullCollections = true;
});
var mapper = config.CreateMapper();
return mapper;
});
public static IMapper Mapper => _lazy.Value;
}
I have been flattening domain objects into DTOs as shown in the example below:
public class Root
{
public string AParentProperty { get; set; }
public Nested TheNestedClass { get; set; }
}
public class Nested
{
public string ANestedProperty { get; set; }
}
public class Flattened
{
public string AParentProperty { get; set; }
public string ANestedProperty { get; set; }
}
// I put the equivalent of the following in a profile, configured at application start
// as suggested by others:
Mapper.CreateMap<Root, Flattened>()
.ForMember
(
dest => dest.ANestedProperty
, opt => opt.MapFrom(src => src.TheNestedClass.ANestedProperty)
);
// This is in my controller:
Flattened myFlattened = Mapper.Map<Root, Flattened>(myRoot);
I have looked at a number of examples, and so far this seems to be the way to flatten a nested hierarchy. If the child object has a number of properties, however, this approach doesn't save much coding.
I found this example:
http://consultingblogs.emc.com/owainwragg/archive/2010/12/22/automapper-mapping-from-multiple-objects.aspx
but it requires instances of the mapped objects, required by the Map() function, which won't work with a profile as I understand it.
I am new to AutoMapper, so I would like to know if there is a better way to do this.
I much prefer avoiding the older Static methods and do it like this.
Place our mapping definitions into a Profile. We map the Root first, and then apply the mappings of the Nested afterwards. Note the use of the Context.
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Root, Flattened>()
.AfterMap((src, dest, context) => context.Mapper.Map(src.TheNestedClass, dest));
CreateMap<Nested, Flattened>();
}
}
The advantage of defining both the mapping from Root to Flattened and Nested to Flatterned is that you retain full control over the mapping of the properties, such as if the destination property name is different or you want to apply a transformation etc.
An XUnit test:
[Fact]
public void Mapping_root_to_flattened_should_include_nested_properties()
{
// ARRANGE
var myRoot = new Root
{
AParentProperty = "my AParentProperty",
TheNestedClass = new Nested
{
ANestedProperty = "my ANestedProperty"
}
};
// Manually create the mapper using the Profile
var mapper = new MapperConfiguration(cfg => cfg.AddProfile(new MappingProfile())).CreateMapper();
// ACT
var myFlattened = mapper.Map<Root, Flattened>(myRoot);
// ASSERT
Assert.Equal(myRoot.AParentProperty, myFlattened.AParentProperty);
Assert.Equal(myRoot.TheNestedClass.ANestedProperty, myFlattened.ANestedProperty);
}
By adding AutoMapper's serviceCollection.AddAutoMapper() from the AutoMapper.Extensions.Microsoft.DependencyInjection nuget package to your start up, the Profile will be picked up automatically, and you can simply inject IMapper into wherever you are applying the mapping.
In the latest version of AutoMapper, there's a naming convention you can use to avoid multiple .ForMember statements.
In your example, if you update your Flattened class to be:
public class Flattened
{
public string AParentProperty { get; set; }
public string TheNestedClassANestedProperty { get; set; }
}
You can avoid the use of the ForMember statement:
Mapper.CreateMap<Root, Flattened>();
Automapper will (by convention) map Root.TheNestedClass.ANestedProperty to Flattened.TheNestedClassANestedProperty in this case. It looks less ugly when you're using real class names, honest!
2 more possible solutions:
Mapper.CreateMap<Nested, Flattened>()
.ForMember(s=>s.AParentProperty, o=>o.Ignore());
Mapper.CreateMap<Root, Flattened>()
.ForMember(d => d.ANestedProperty, o => o.MapFrom(s => s.TheNestedClass));
An alternative approach would be the below, but it would not pass the Mapper.AssertConfigurationIsValid().
Mapper.CreateMap<Nested, Flattened>()
//.ForMember map your properties here
Mapper.CreateMap<Root, Flattened>()
//.ForMember... map you properties here
.AfterMap((s, d) => Mapper.Map(s.TheNestedClass, d));
Not sure if this adds value to the previous solutions, but you could do it as a two-step mapping. Be careful to map in correct order if there are naming conflicts between the parent and child (last wins).
Mapper.CreateMap<Root, Flattened>();
Mapper.CreateMap<Nested, Flattened>();
var flattened = new Flattened();
Mapper.Map(root, flattened);
Mapper.Map(root.TheNestedClass, flattened);
To improve upon another answer, specify MemberList.Source for both mappings and set the nested property to be ignored. Validation then passes OK.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<SrcNested, DestFlat>(MemberList.Source);
cfg.CreateMap<SrcRoot, DestFlat>(MemberList.Source)
.ForSourceMember(s => s.Nested, x => x.Ignore())
.AfterMap((s, d) => Mapper.Map(s.Nested, d));
});
Mapper.AssertConfigurationIsValid();
var dest = Mapper.Map<SrcRoot, DestFlat>(src);
I wrote extension method to solve similar problem:
public static IMappingExpression<TSource, TDestination> FlattenNested<TSource, TNestedSource, TDestination>(
this IMappingExpression<TSource, TDestination> expression,
Expression<Func<TSource, TNestedSource>> nestedSelector,
IMappingExpression<TNestedSource, TDestination> nestedMappingExpression)
{
var dstProperties = typeof(TDestination).GetProperties().Select(p => p.Name);
var flattenedMappings = nestedMappingExpression.TypeMap.GetPropertyMaps()
.Where(pm => pm.IsMapped() && !pm.IsIgnored())
.ToDictionary(pm => pm.DestinationProperty.Name,
pm => Expression.Lambda(
Expression.MakeMemberAccess(nestedSelector.Body, pm.SourceMember),
nestedSelector.Parameters[0]));
foreach (var property in dstProperties)
{
if (!flattenedMappings.ContainsKey(property))
continue;
expression.ForMember(property, opt => opt.MapFrom((dynamic)flattenedMappings[property]));
}
return expression;
}
So in your case it can be used like this:
var nestedMap = Mapper.CreateMap<Nested, Flattened>()
.IgnoreAllNonExisting();
Mapper.CreateMap<Root, Flattened>()
.FlattenNested(s => s.TheNestedClass, nestedMap);
IgnoreAllNonExisting() is from here.
Though it's not universal solution it should be enough for simple cases.
So,
You don't need to follow flattening convention in destination's properties
Mapper.AssertConfigurationIsValid() will pass
You can use this method in non-static API (a Profile) as well
I created a simple example using AutoMappers new naming convention rules to map from flattened to nested objects, hope this helps
https://dotnetfiddle.net/i55UFK