Pass different AutoMapper context per nested mapping - automapper

I know we can set the Context items when we call Map(), and it will be available to every map operation. Is there a way to change those context items during mapping?
Suppose I have these source types:
public class OuterSource {
public string TimeZone { get; set; }
public string Name { get; set; }
public InnerSource[] InnerArray { get; set; }
}
public class InnerSource {
public DateTime Created { get; set; }
public string Message { get; set; }
}
and these destination types:
public class OuterDest {
public string Name { get; set; }
public InnerDest[] InnerArray { get; set; }
}
public class InnerDest {
public DateTime Created { get; set; }
public string Message { get; set; }
}
The only difference is that InnerSource.Created is in UTC and I want to map it to the local time zone. However the time zone is in OuterSource, not InnerSource.
Normally, I would set up my mappers like so:
CreateMap<OuterSource, OuterDest>();
CreateMap<InnerSource, InnerDest>();
But that wouldn't work because when it comes to mapping InnerSource to InnerDest it does not have access to OuterSource.TimeZone.
So I'm currently forced to set my mapping like so:
CreateMap<OuterSource, OuterDest>()
.ForMember(dest => dest.InnerArray, opt => opt.ResolveUsing(
//loop through source.InnerArray and do the datetime
//conversion manually
));
I consider that a code smell. What I would love to do is to pass the timezone to the nested mapping somehow. I would appreciate any pointers towards that direction.

Related

How do I get automapper to map enums that are part of a list of child objects

I've got the majority of my automapper maps working but I'm facing a problem trying to translate an enum from the value to the string when part of list of child objects. I have the enum to string converter working when at the top level but it seems when I am converting from RecipeStep to RecipeStepResource it isn't using the map defined for Ingredient to IngredientResource and therefore the conversion from enum to string isn't being called.
I've looked around but can't seem to find a similar example to work from and am having trouble deciphering the automapper help on this which says it should automatically pick up the map defined, which it doesn't seem to be, unsure if this is because the Ingredient items are part of a list. Major code snippets below, any help appreciated.
Model:
public class RecipeStep
{
[Required]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
public string Description { get; set; }
public IList<Ingredient> Ingredients { get; set; }
public Timer Timer { get; set; }
public int RecipeID { get; set; }
[JsonIgnore]
[IgnoreDataMember]
[ForeignKey("RecipeID")]
public Recipe Recipe { get; set; }
}
public class Ingredient
{
[Required]
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; }
[Required]
public ETypeOfIngredient Type { get; set; }
[Required]
public double Amount { get; set; }
[Required]
public EUnitOfMeasure Unit { get; set; }
public int RecipeStepID { get; set; }
[JsonIgnore]
[IgnoreDataMember]
[ForeignKey("RecipeStepID")]
public RecipeStep RecipeStep { get; set; }
}
Resources:
public class RecipeStepResource
{
public int ID { get; set; }
public string Description { get; set; }
public List<IngredientResource> Ingredients { get; set; }
public TimerResource Timer { get; set; }
}
public class IngredientResource
{
public int ID { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public double Amount { get; set; }
public string Unit { get; set; }
public int RecipeStepID { get; set; }
}
Mapping code:
CreateMap<Ingredient, IngredientResource>()
.ForMember(src => src.Type,
opt => opt.MapFrom(src => src.Type.ToDescriptionString()))
.ForMember(src => src.Unit,
opt => opt.MapFrom(src => src.Unit.ToDescriptionString()));
CreateMap<Timer, TimerResource>();
CreateMap<RecipeStep, RecipeStepResource>()
.ForMember(dest => dest.Ingredients,
opt => opt.MapFrom(src => src.Ingredients))
.ForMember(src => src.Timer,
opt => opt.MapFrom(src => src.Timer));
Enum to string conversion code:
public static string ToDescriptionString<TEnum>(this TEnum #enum)
{
FieldInfo info = #enum.GetType().GetField(#enum.ToString());
var attributes = (DescriptionAttribute[])info.GetCustomAttributes(typeof(DescriptionAttribute), false);
return attributes?[0].Description ?? #enum.ToString();
}
What I have tried:
Creating an Ingredient -> IngredientResource map
Creating a List<Ingredient> -> List<IngredientResource> map
Adding an AfterMap call to the List<Ingredient> -> List<IngredientResource> map to convert the enum value
None of these have worked. Really struggling to understand why AutoMapper is not picking up the Ingredient to IngredientResource map for a List property on the RecipeStep object, I thought it would have done this automatically.
The issue came down to the parent object, I had it incorrectly mapped with both the model and resource files referring to RecipeStep, instead of RecipeStep -> RecipeStepResource. Really want to thank #Lucian for helping me and making me go back to basics to work through the understanding from a simpler standpoint and building up to a representative model.

Autmapper nested mapping

I have the following main class:
public class ResearchOutcome
{
public ResearchOutcomeCategory ResearchOutcomeCategory { get; set; }
public string? UniqueIdentifier { get; set; }
}
And the category class is:
public class ResearchOutcomeCategory
{
public int Id { get; set; }
public string Name { get; set; }
public string? Description { get; set; }
}
The View models for above classes are:
public class ResearchOutcomeDetailVm : IMapFrom<ResearchOutcome>
{
public int Id { get; set; }
public virtual ResearchOutcomeCategoryDetailVm ResearchOutcomeCategory { get; set; }
}
public class ResearchOutcomeCategoryDetailVm : IMapFrom<ResearchOutcomeCategory>
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
Now, I have used the following mapping profile:
// First this one
profile.CreateMap<ResearchOutcomeCategory, ResearchOutcomeCategoryDetailVm>();
profile.CreateMap<ResearchOutcome, ResearchOutcomeDetailVm>();
//Then I tried this one
profile.CreateMap<ResearchOutcome, ResearchOutcomeDetailVm>()
.ForMember(o => o.ResearchOutcomeCategory,
cat => cat.MapFrom( o => o.ResearchOutcomeCategory));
But the ResearchOutcomeCategory is always null. Any help would be appreciated.
After digging more, I identified that I was not "Including" the relevant item in the query, hence, the view model was always empty. Pretty dumb on my part :D
Regarding the mapping, if the properties (even complex ones) have the same names, then the mapper will map them automatically. So simply this line worked
profile.CreateMap<ResearchOutcomeCategory, ResearchOutcomeCategoryDetailVm>();
Hope it helps someone

Automapper, mapping single destination property as a concatenation of multiple source property

I have a situation where I need to map a single property as a combination of multiple source properties based on some conditions.
Destination :
public class Email
{
public Email() {
EmailRecipient = new List<EmailRecipient>();
}
public string Subject{get; set;}
public string Body {get; set;}
public virtual ICollection<EmailRecipient> EmailRecipient { get; set; }
}
public class EmailRecipient
{
public int EmaiId { get; set; }
public string RecipientEmailAddress { get; set; }
public int RecipientEmailTypeId { get; set; }
public virtual Email Email { get; set; }
}
Source:
public class EmailViewModel
{
public List<EmailRecipientViewModel> To { get; set; }
public List<EmailRecipientViewModel> Cc { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailRecipientViewModel
{
public string RecipientEmailAddress { get; set; }
}
I want Mapper.Map<EmailViewModel,Email>()
Here I would like to map my Email.EmailRecipient as a combination of EmailViewModel.To and EmailViewModel.Cc.
However the condition is, Email.EmailRecipient.RecipientEmailTypeId will be 1 for To and 2 for Cc
Hope my question is clear.
One possible way to achieve this is to create a map that uses a specific method for this conversion. The map creation would be:
Mapper.CreateMap<EmailViewModel, Email>()
.ForMember(e => e.EmailRecipient, opt => opt.MapFrom(v => JoinRecipients(v)));
Where the JoinRecipients method would perform the conversion itself. A simple implementation could be something like:
private ICollection<EmailRecipient> JoinRecipients(EmailViewModel viewModel) {
List<EmailRecipient> result = new List<EmailRecipient>();
foreach (var toRecipient in viewModel.To) {
result.Add(new EmailRecipient {
RecipientEmailTypeId = 1,
RecipientEmailAddress = toRecipient.RecipientEmailAddress
});
}
foreach (var ccRecipient in viewModel.Cc) {
result.Add(new EmailRecipient {
RecipientEmailTypeId = 2,
RecipientEmailAddress = ccRecipient.RecipientEmailAddress
});
}
return result;
}
I'm a huge opponent of converters, mostly because for other people in your project, things will just happen 'like magic' after the mapping call.
An easier way of handling this would be to implement the property as a method that converts other properties on the viewmodel to the required formatting. Example:
public class EmailViewModel
{
public ICollection<EmailRecipient> EmailRecipient {
get {
return To.Union(Cc);
}
}
public List<EmailRecipientViewModel> To { get; set; }
public List<EmailRecipientViewModel> Cc { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
Now automapper automatically maps from EmailRecipient property to EmailRecipient property, and if someone is trying to figure out how it happens, they just need to look on the viewmodel.
Editing this some years later: Just as a warning, doing things this way means that every time you call EmailRecipient, you incur the o(n) task of unioning the To and Cc fields. This is fine if you're only dealing with one email, but if you're reusing the viewmodel and someone sticks it in a loop with say, every other email in the system, it might be a huge performance issue. In that case I'd go with the accepted answer so that you dodge this potential performance pitfall.

Why does this ploymorphic List property go null when adding additional objects to the list?

Consider the following:
public class VideoContainer<T>
{
public string Name { get; set; }
//public List<VideoContainer<T>> VideoContainers { get; set; }
}
public class Perspective : VideoContainer<Perspective>
{
public List<VideoContainer<SourceContainer>> VideoContainers { get; set; }
}
I want to ensure VideoContainer<Perspective>.VideoContainers can only contain VideoContainer<SourceContainer> types.
I add a new Perspective object to a List<Perspective> with three VideoContainers. The problem is that when I add a new Perspective to the list, the previously-added Perspective.VideoContainers is null.
Why is this happening?
It sounds like you need two generic types:
public class VideoContainer<T, U>
{
public string Name { get; set; }
public List<VideoContainer<U>> VideoContainers { get; set; }
}
public class Perspective : VideoContainer<Perspective, SourceContainer>
{
// No longer declare the list, just use it... it's now:
// public List<VideoContainer<SourceContainer>> VideoContainers { get; set; }
}

I need a mix of table per hierarchy or type in entity framework

I have 3 entities and I want to use them for EF as POCO`s:
TimeTable is the base class for Period and LessonPlanner classes.
How can I use/map these 3 classes with EF 5.0 or higher?
I want to get 2 sql tables created: Period and LessonPlanner.
EF supports only Table per hierarchy or Table per type.
Either I get one table containing all properties as fields OR I get 3 separated tables.
I just want 2 tables whatever approach I have to take I do not care: Database/Model/Code first...
public class TimeTable
{
public int Id { get; set; }
public int LessonNumber { get; set; }
public string SchoolclassNumberForSunday { get; set; }
public string SchoolclassNumberForMonday { get; set; }
public string SchoolclassNumberForTuesday { get; set; }
public string SchoolclassNumberForWednesday { get; set; }
public string SchoolclassNumberForThursday { get; set; }
public string SchoolclassNumberForFriday { get; set; }
public string SchoolclassNumberForSaturday { get; set; }
}
public class Period : TimeTable
{
public enum WeekType
{
A,
AB,
}
}
public class LessonPlanner : TimeTable
{
public DateTime LessonDate { get; set; }
}
Using code first in Entity Framework 5, in the DbContext derived class, override OnModelCreating and add the following code. I've not tried it with your types but I've used a variant of this to create two classes for a three deep hierarchy.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Period>()
.Map<Period>(p => p.MapInheritedProperties())
.ToTable("Period");
modelBuilder.Entity<LessonPlanner>()
.Map<LessonPlanner>(lp => lp.MapInheritedProperties())
.ToTable("LessonPlanner");
}

Resources