How apply condition for multiple properties - c#

Hello I am trying to use AutoMapper for a mapping. Let's imagine I have a domain class like that and a DTO one with the exact same structure.
public class Address
{
public int State { get; set; }
public string StateCode { get; set; }
public int City { get; set; }
public string Street { get; set; }
public string ZipCode { get; set; }
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Phone { get; set; }
public Address Address { get; set; }
...
}
I want to construct a mapping that allows me only set the properties Phone and Address (and others, but for simplicity let's assume is only these two) conditionally depending on the value of the property age. Moreover, if the condition of the age property is met I want conditionally set the property StateCode only if the property State is not null. The logic is described below.
var personDto = new personDto();
personDto.Id = person.Id;
personDto.Name = person.Name;
personDto.Age = person.Age;
if(person.Age > 18)
{
personDto.Phone = person.Phone;
personDto.Address = new AddressDto();
personDto.Address.State = person.Address.State;
if (person.Address.State != null)
{
personDto.Address.StateCode = person.Address.StateCode;
}
personDto.Address.Street = person.Address.Street;
personDto.Address.ZipCode = person.Address.ZipCode;
}
I am trying to apply the same logic using Auto mapper, but I did not find in the documentation a way to do this.
CreateMap<Person, PersonDto>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.Id))
.ForMember(d => d.Age , o => o.MapFrom(s => s.Age))
.ForMember(d => d.Name , o => o.MapFrom(s => s.Name ))
.ForMember(d => d.Phone , o =>
{
o.Condition(s => s.Age > 18); // I do not want repeat same condition for every property
o.MapFrom(s => s.Phone );
})
.ForMember(d => d.Address, o =>
{
o.Condition(s => s.Age > 18);
o.MapFrom(s => s.Address); // how can I nest the if condition of the State property here?
})
...
How can I apply the same nested logic here on Auto mapper? The whole point is how I can configure the mapping of several properties respecting the same condition and configure the nested condition as shown previously.

Related

How to assign null for the object if object members have no value - automapper c#

I am using automapper in c#.
class A
{
public int Value { get; set; }
public string Code { get; set; }
public B? Details { get; set; }
}
class B
{
public int Id { get; set;}
public string Name { get; set; }
}
class C
{
public int Value { get; set; }
public string Code { get; set; }
public int? DetailId { get; set; }
public string? DetailName { get; set; }
}
And in automapper I used like below:
CreateMap<C, A>()
.ForPath(o => o.Details.Id, b => b.MapFrom(z => z.DetailId))
.ForPath(o => o.Details.Name, b => b.MapFrom(z => z.DetailName))
.ReverseMap();
When I using like the above mapping, I get like output as
"details ": {
"id": 0,
"name": ""
}
I need to get Details value as null instead of an object type if it's members have no value. i.e) DetailId and DetailName have no value. How to get this?
"details" : null
You can user Conditional Mapping
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<C, B>()
.ForMember(o => o.Id, b => b.MapFrom(z => z.DetailId))
.ForMember(o => o.Name, b => b.MapFrom(z => z.DetailName));
cfg.CreateMap<C, A>()
.ForMember(o => o.Details, b => b.MapFrom((c, a, obj, context) => !string.IsNullOrEmpty(c.DetailName) ? context.Mapper.Map<B>(c) : null))
.ReverseMap();
});
You could do this with an AutoMapper after map action.
Something like this:
CreateMap<C, A>()
.ForPath(o => o.Details.Id, b => b.MapFrom(z => z.DetailId))
.ForPath(o => o.Details.Name, b => b.MapFrom(z => z.DetailName))
.AfterMap((src, dest) =>
{
dest.Details = src.DetailId.HasValue && src.DetailName != null
? dest.Details
: null;
})
.ReverseMap());
You can use IValueResolver interface to achieve what you require. Documentation: http://docs.automapper.org/en/stable/Custom-value-resolvers.html
Also there's a similar question: Automapper Mapping Multiple Properties to Single Property
Configuration:
CreateMap<C, A>()
.ForMember(o => o.Details, b => b.MapFrom<DetailsValueResolver>())
.ReverseMap();
Implementation:
// Note: this does not cover ReverseMap() when you would try to convert A to C
public class DetailsValueResolver : IValueResolver<C, A, B>
{
// Runs every time you map C to A
public B Resolve(C source, A destination, B destMember, ResolutionContext context)
{
// Covers cases where you can get null or empty DetailName, as well as null or zero DetailId
if (!string.IsNullOrEmpty(source.DetailName) && source.DetailId > 0)
{
return new B { Id = (int)source.DetailId, Name = source.DetailName };
}
return null;
}
}
You also can omit explicitly setting strings and classes as nullable types with ? as you do here:
public B? Details { get; set; }
public string? DetailName { get; set; }
Because string type and any class is null by default.

How to map a simple poco into a complex object hierachy using automapper?

I have a simple poco that need to be mapped to an object supplied by a third party that uses a complex object hierarchy. I would like to use AutoMapper if possible but I am unsure how to set it up correctly.
I have supplied a simplified example below to show what I am trying to do.
My poco:
public class Person
{
public string FirstName { get; set; }
public string Lastname { get; set; }
public string FullName { get { return Firstname + " " + Lastname; } }
public string AddressLine1 { get; set; }
public string AddressLine2 { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
public string Reference { get; set; }
}
Third Party objects
public class People
{
public Person[] Person { get; set; }
}
public class Person
{
public Names Names { get; set; }
public Address Address { get; set; }
public ReasonsForRequest[] Reasons { get; set; }
}
public class Names
{
public string Fullname { get; set; }
}
public class Address
{
public string AddressLine[] { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string Postcode { get; set; }
}
public class ReasonsForRequest
{
public StructuredReasons StructuredReasons { get; set; }
}
public class StructuredReasons
{
public Reference Ref { get; set; }
}
public class Reference
{
public string Ref { get; set; }
}
The issue I am having is Registering the mappings to get the reference mapped. How can I register a mappig to an object that just contains an object when I need to map to the inner object and to a parent object?
[For clarity, I'm going to call your Person POCO PersonDTO, since the 3rd party code also has a class called Person.]
There are a couple of ways of doing this. One, which I've used in the past, involves setting up a mapping from PersonDTO to Names, another from PersonDTO to Address, and another from PersonDTO to Reasons. Finally, you add a mapping from PersonDTO to Person. It looks like this (I've left out Reasons, but you get the idea):
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<PersonDTO, Names>()
.ForMember(d => d.Fullname, o => o.MapFrom(s => s.FullName));
cfg.CreateMap<PersonDTO, Address>()
.ForMember(d => d.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }));
cfg.CreateMap<PersonDTO, Person>()
.ForMember(d => d.Names, o => o.MapFrom(s => s))
.ForMember(d => d.Address, o => o.MapFrom(s => s));
});
var mapper = config.CreateMapper();
var myPerson = new PersonDTO() {
FirstName = "Bob",
LastName = "Gold",
AddressLine1 = "123 Main Street",
AddressLine2 = "Apt. 2"
};
var theirPerson = mapper.Map<Person>(myPerson);
But a recent version of AutoMapper added a ForPath() method which makes all of this simpler by letting you access inner objects. Now the code collapses to this:
var config = new MapperConfiguration(cfg =>
cfg.CreateMap<PersonDTO, Person>()
.ForPath(d => d.Names.Fullname, o => o.MapFrom(s => s.FullName))
.ForPath(d => d.Address.AddressLine,
o => o.MapFrom(s => new[] { s.AddressLine1, s.AddressLine2 }))
);
var mapper = config.CreateMapper();
Edit: I left out one aspect which may change the balance between these two methods. In the first method, with multiple maps defined, you get for free any fields with matching names. For example, you don't need to explicitly map PesronDTO.City to Address.City (and if you change the case on PersonDTO.FullName to be PersonDTO.Fullname, you'd get that for free as well). But in the second method, you have to explicitly map all nested fields, even if the names match. In your case, I think the first method would probably work better for you, because you'd have to map only 3 fields explicitly and would get the other 4 for free. For the second method, you'd have to do a ForPath() for all 7 fields.
By default it tries to match the properties of the SourceType to those of the DestinationType. But for your scenario you'll have add some specifications using the ForMember extension method, like this:
Mapper.CreateMap<Foo, FooDTO>()
.ForMember(e => e.Bars, o => o.ExplicitExpansion());
There's an existing thread on this one here:
AutoMapper define mapping level

C# - Automapper - Map element with condition

I have two classes:
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Quantity { get; set; }
};
public class Customer_
{
public string FirstNam { get; set; }
public string LastNam { get; set; }
public int Quantity { get; set; }
}
And a mapping between the two with Automapper library :
Customer[] data = new Customer[3];
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Customer, Customer_>()
.ForMember(dest => dest.FirstNam, opt => opt.MapFrom(src => src.FirstName))
.ForMember(dest => dest.LastNam, opt => opt.MapFrom(src => src.LastName));
});
IMapper mapper = config.CreateMapper();
Customer_[] desti = mapper.Map<Customer[], Customer_[]>(data);
It works, but how to map only the elements of my array data that meet a condition ? For example : map only the element who have a Quantity > x
Thank's
Typically I'd do this before you map into AutoMapper:
Customer_[] desti = mapper.Map<Customer[], Customer_[]>(
data.Where(c => c.Quantity > 10).ToArray());
Don't try to put this sort of intelligence into AutoMapper, it's confusing and will lead to bugs.

Getting a Dictionary from a Query with GroupBy

Given two persisted entities
public class Header
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual List<Detail> Details { get; set; }
}
and
public class Detail
{
public virtual int Id { get; set; }
public virtual Header MyHeader { get; set; }
public virtual string Text { get; set; }
public virtual object LotsOfPropertiesIDontNeed { get; set; }
}
I want to populate a new object
public class MiniHeader
{
public string Name { get; set; }
public Dictionary<int, string> DetailTexts { get; set; }
}
with only the name from the Header, and with a dictionary relating detail IDs to the associated texts. Note that Detail also has LotsOfPropertiesIDontNeed, which I would prefer not to pull
across the wire or even request from SQL Server.
With the code
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.Select(g => new MiniHeader()
{
Name = g.Key.Name,
DetailTexts = g.ToDictionary(d => d.Id, d => d.Text)
});
I get the expected
LINQ to Entities does not recognize the method 'System.Collections.Generic.Dictionary`2[System.Int32,System.String]
since .ToDictionary cannot execute on the database side. I can make it work like this:
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.AsEnumerable()
.Select(g => new MiniHeader()
{
Name = g.Key.Name,
DetailTexts = g.ToDictionary(d => d.Id, d => d.Text)
});
but I presume that LotsOfPropertiesIDontNeed will be requested of SQL Server and pulled across the wire.
Is there a way to make this work, without pulling the unnecessary fields?
You can project your results to an anonymous type and then apply AsEnumerable and later project to your class like:
IEnumerable<MiniHeader> mini =
ctx.Details.Include(d => d.MyHeader)
.GroupBy(d => d.MyHeader)
.Select(g => new
{
Name = g.Key.Name,
Details = g.Select(i => new { i.Id, i.Text }),
})
.AsEnumerable()
.Select(e => new MiniHeader()
{
Name = e.Name,
DetailTexts = e.Details.ToDictionary(d => d.Id, d => d.Text)
});
This will let you get only those field that you need and later you can use ToDictionary on an in-memory collection.

AutoMapper Conditional Entity Mapping

I have a db entity which stores the Order Addresses like this...
And I have the BLL classes like this...
public class DeliveryAddress
{
public string Id { get; set; }
public string PersonyName { get; set; }
public string CompanyName { get; set; }
public List<string> AddressLines { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
and another class like this...
public class InvoiceAddress
{
public string Id { get; set; }
public string PersonyName { get; set; }
public string CompanyName { get; set; }
public List<string> AddressLines { get; set; }
public string Zip { get; set; }
public string City { get; set; }
public string CountryCode { get; set; }
}
and I want to map the EF entity to the above classes on the basis of AddressType column. Can anybody explain me how to do that ?
UPDATE
I want to map to OR.DeliveryAddress if the addressType is "Delivery" and to OR.InvoiceAddress if the addressType is "Invoice"
So far, I have been able to do this, but I don't know how to apply condition on the entity mapping level...
Mapper.CreateMap<OrderAddress, OR.DeliveryAddress>()
.ForMember(d => d.City, o => o.MapFrom(s => s.city))
.ForMember(d => d.CompanyName, o => o.UseValue(string.Empty))
.ForMember(d => d.CountryCode, o => o.MapFrom(s => s.countryCode))
.ForMember(d => d.Id, o => o.MapFrom(s => s.id))
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name))
.ForMember(d => d.Zip, o => o.MapFrom(s => s.zip));
UPDATE 2
After discussion with #Yuliam Here is the Fiddle that I could come up with for my problem...
You can create a customer mapper to object. And also you don't have to specify each property using ForMember because if the difference is only upper case / lower case (unless for PersonName), by default AutoMapper is case insensitive when mapping the property name.
Create a custom mapper to object.
public class AddressConverter : ITypeConverter<OrderAddress, object>
{
public object Convert(ResolutionContext context)
{
var o = context.SourceValue as OrderAddress;
if (o == null) return null;
if (o.addressType == "Delivery") return Mapper.Map<OR.DeliveryAddress>(o);
if (o.addressType == "Invoice") return Mapper.Map<OR.InvoiceAddress>(o);
return null;
}
}
Then define the mapper.
Mapper.CreateMap<OrderAddress, OR.DeliveryAddress>()
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name));
Mapper.CreateMap<OrderAddress, OR.InvoiceAddress>()
.ForMember(d => d.PersonyName, o => o.MapFrom(s => s.name));
Mapper.CreateMap<OrderAddress, object>().ConvertUsing<AddressConverter>();
Usage.
var orderAddressDto = Mapper.Map<object>(orderAddress);
The actual orderAddressDto type will be based on the addressType. If you have an interface or base class for OR.DeliveryAddress and OR.InvoiceAddress that would be more strongly type. Then replace the object type with the interface / base class.
You may want to try taking a look at ResolveUsing
Semi-pseudo code, as I don't know what your entire Domain Model looks like:
Mapper.CreateMap<OrderObject, OrderDto>()
.ForMember(x => x.Address, opt => opt.ResolveUsing(oo => oo.Type == Invoice ? oo.InvoiceAddress : oo.DeliveryAddress));
I'm assuming here that you have an actual Order entity, which you're trying to make to an 'OrderDto' which only contains one address field.

Categories

Resources