What I want to do is to get the lists (plural) in MugDTO record to the list (singular) in Mug class. They ar both exactly the same except for the "top level".
But I can't figure out how it should work. I've tried with custom value resolver and a Custom type converter but to no avail. Unfortunately I deleted the code for the converter and the resolver so I can't add them here.
This is what I started with:
CreateMap<MugDTO, Mug>().ForMember(x => x.MugItems, m => m.MapFrom(s => s.Carriers));
On the front-end it looks like this:
public abstract class MugItem
{
public string TypeName { get; set; }
public string BackgroundColor { get; set; }
public string BorderColor { get; set; }
}
public class Cartridge : MugItem
{
}
public class Carrier : MugItem
{
}
public class Mug
{
public int Id { get; set; }
public List<MugItem> MugItems { get; set; }
}
On the back end it looks the same except for the mug has separate litst for the carrier and the cartridge.
public record MugDTO
{
public int Id { get; set; }
public List<CarrierDTO> Carriers { get; set; }
public List<CartridgeDTO> Cartridges { get; set; }
}
Error I get:
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Error mapping types.
Mapping types:
MugDTO -> Mug
Test.Shared.Domain.MugDTO -> Test.Client.Domain.Mug
Type Map configuration:
MugDTO -> Mug
Test.Shared.Domain.MugDTO -> Test.Client.Domain.Mug
Destination Member:
MugItems
AutoMapper.AutoMapperMappingException: Error mapping types.
Mapping types:
MugDTO -> Mug
Test.Shared.Domain.MugDTO -> Test.Client.Domain.Mug
Type Map configuration:
MugDTO -> Mug
Test.Shared.Domain.MugDTO -> Test.Client.Domain.Mug
Destination Member:
MugItems
---> System.ArgumentException: Cannot create an instance of abstract type Test.Client.Domain.MugItem.
So the solution was that I needed two lines of code and to map and union the records inline.
CreateMap<MugItemDTO, MugItem>().IncludeAllDerived();
CreateMap<MugDTO, Mug>()
.ForMember(dear => dear.MugItems, opt => opt.MapFrom(
(src, dest, member, context) => context.Mapper.Map<List<CarrierDTO>>(src.Carriers).Union<MugItemDTO>(context.Mapper.Map<List<CartridgeDTO>>(src.Cartridges))));
Related
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.
Given I have 2 classes, Foo and Bar:
public class Foo
{
private readonly List<Bar> _bars = new List<Bar>();
public int Id { get; private set; }
public string Name { get; private set; }
public IEnumerable<Bar> Bars => _bars;
public void AddBar(Bar bar)
{
_bars.Add(bar);
}
public static Foo Create(string name)
{
return new Foo { Name = name };
}
private Foo() { }
}
public class Bar
{
public int Id { get; private set; }
public string Description { get; private set; }
public static Bar Create(string description)
{
return new Bar { Description = description };
}
}
With 2 corresponding DTOs,
public class BarDto
{
public int Id { get; set; }
public string Description { get; set; }
}
public class FooDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<BarDto> Bars { get; set; }
public FooDto()
{
Bars = new List<BarDto>();
}
}
And an AutoMapper/AutoMapper.Collection.EntityFrameworkCore setup of
var config = new MapperConfiguration(cfg =>
{
cfg.AddCollectionMappers();
cfg.UseEntityFrameworkCoreModel<DemoContext>();
cfg.CreateMap<BarDto, Bar>().EqualityComparison((src, dest) => src.Id == dest.Id);
cfg.CreateMap<FooDto, Foo>().ForMember(dest => dest.Bars, opt =>
{
opt.MapFrom(s => s.Bars);
opt.UseDestinationValue();
}).EqualityComparison((src, dest) => src.Id == dest.Id);
});
I have a use case whereby the incoming FooDto may contain inserted, appended, updated and deleted items in the Bars collection which I am attempting to handle by:
Looking up the existing entity from the database
Mapping changes from the DTO to the entity
Saving the changes to the database
However the following code produces an InvalidOperationException exception stating that "The instance of entity type 'Bar' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached"
var fooToUpdate = db.Foos.Include(_ => _.Bars).FirstOrDefault(_ => _.Id == fooDto.Id);
mapper.Map(fooDto, fooToUpdate);
db.SaveChanges();
My understanding was that becuase I am setting EqualityComparison for the BarDto -> Bar mapping it should update the tracked entity and the save operation should succeed becuase it was referencing the same object?
I am not sure if I'm going about this the wrong way or simply missing somthing in the configuration.
Update
It seems the problem I am facing may be related to this issue on github.
let's say I have
public class EFObject
{
public int Id { get; set; }
public int NavId { get; set; }
public NavObject Nav { get; set; }
}
public class DTOObject
{
public int Id { get; set; }
public int NavId { get; set; }
public string NavName { get; set; }
}
My expectation was high, and I thought to my self the built-in flattening should handle this, so my mapping is very simple
CreateMap<DTOObject, EFObject>().ReverseMap();
Unfortunately, converting DTOObject to EFObject does not work as expected because EFObject.Nav is null. Since I used the name NavId and NavName I would expect it to create a new NavObject and set the Nav.Id and Nav.Name accordingly.
My Problem : Is there a feature in Automapper that will allow me to achieve the intended result without having to manually write a rule to create an NavObject when mapping the Nav property?.
Unflattening is only configured for ReverseMap. If you want unflattening, you must configure Entity -> Dto then call ReverseMap to create an unflattening type map configuration from the Dto -> Entity.
as noted by Automapper documentation here
I have a class with a few properties and some methods
public class Content
{
public int Id { get; set; }
public string Application { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
public override bool Equals(object obj) {...}
public override int GetHashCode() {...}
}
With this Fluent NHibernate mapping:
public class ContentMapping : ClassMap<Content>
{
public ContentMapping()
{
Table("vw_all_contents");
CompositeId()
.KeyProperty(x => x.Id, "id")
.KeyProperty(x => x.Application, "application");
Map(x => x.Property1, "property1");
Map(x => x.Property2, "property2");
}
}
Up to here everything works fine.
I now want to populate the same object but with a table a federated table that connects to another database.
So I have:
public class ContentOnProductionDatabase : Content { }
With a mapping:
public class ContenOnProductionDatabasetMapping : ClassMap<ContentOnProductionDatabase>
{
public ContentOnProductionDatabaseMapping()
{
Table("vw_federated_all_contents");
CompositeId()
.KeyProperty(x => x.Id, "id")
.KeyProperty(x => x.Application, "application");
Map(x => x.Property1, "property1");
Map(x => x.Property2, "property2");
}
}
And here is where NHibernate gets really confused and the queries return mixed results from both databases.
The problem goes away if my ContentOnProductionDatabase does not extend Content but instead is a duplicate class like this:
public class ContentOnProductionDatabaseMapping
{
public int Id { get; set; }
public string Application { get; set; }
public string Property1 { get; set; }
public string Property2 { get; set; }
public override bool Equals(object obj) {...}
public override int GetHashCode() {...}
}
So now everything is fine but I don't like the fact that there is so much code duplication and it seems to me there must be some sort of Mapping configuration out there to force NHibernate to ignore the inheritance and differentiate the two, especially since they map to different databases.
The repository framework is an inbuilt one handles the session and the queries.
public class ContentRepository : NHibernateRepositoryBase, IContentRepository
{
public ContentRepository(INHibernateContext context, ISettingsManager settingsManager): base(context){ }
public Content ReadContent(int id, string application)
{
using (ISessionContainer container = Context.GetSessionContainer())
{
return
container.AsQueryable<Content>()
.FirstOrDefault(c => c.Id == id && c.Application == application);
// All queries using <Content> return the correct results
}
}
public ContentOnProductionDataBase ReadFederatedContent(int id, string application)
{
using (ISessionContainer container = Context.GetSessionContainer())
{
return
container.AsQueryable<ContentOnProductionDataBase>()
.FirstOrDefault(c => c.Id == id && c.Application == application);
// All queries using <ContentOnProductionDataBase> return the combined results of <Content> and <ContentOnProductionDataBase>
}
}
}
Internally the container.AsQueryable works by invoking this:
public IQueryable<TEntity> AsQueryable<TEntity>() where TEntity : class
{
return LinqExtensionMethods.Query<TEntity>(this.Session);
}
Any ideas how to get rid of the code duplication?
To define the class mapping and the properties only once, you have to define a base class and define the mapping with UseUnionSubclassForInheritanceMapping which will allow you to use independent tables per entity which is derived from that base class.
You don't have to but you should declare your base class as abstract, because it will not have a database representation. So persisting the base class will fail! Meaning, you don't want anyone to use it as an entity, instead use your derived classes...
To do so, create one base, and 2 derived classes which should be stored in one table per class.
public abstract class ContentBase
{
public virtual int Id { get; set; }
public virtual string Application { get; set; }
public virtual string Property1 { get; set; }
public virtual string Property2 { get; set; }
public override bool Equals(object obj)
{
[..]
}
public override int GetHashCode()
{
[..]
}
}
public class Content : ContentBase
{
}
public class ContentOnProductionDatabaset : ContentBase
{
}
The mapping of the base class must call UseUnionSubclassForInheritanceMapping, otherwise nHibernate would combine the classes.
public class ContentBaseMapping : ClassMap<ContentBase>
{
public ContentBaseMapping()
{
UseUnionSubclassForInheritanceMapping();
CompositeId()
.KeyProperty(x => x.Id, "id")
.KeyProperty(x => x.Application, "application");
Map(x => x.Property1, "property1");
Map(x => x.Property2, "property2");
}
}
The subclass mappings just have to define that the base is abstract.
Here you can also define each table name the entity should use.
public class ContentMapping : SubclassMap<Content>
{
public ContentMapping()
{
Table("vw_all_contents");
Abstract();
}
}
public class ContentOnProductionDatabaseMapping : SubclassMap<ContentOnProductionDatabaset>
{
public ContentOnProductionDatabaseMapping()
{
Table("vw_federated_all_contents");
Abstract();
}
}
I am trying to map two objects that are mostly similar with AutoMapper but one member (AudioSummary) raises the following exception :
The following property on EchoNestModel.AudioSummary cannot be mapped: AudioSummary
Add a custom mapping expression, ignore, add a custom resolver, or modify the destination type EchoNestModel.AudioSummary.
Context:
- Mapping to property AudioSummary from EchoNest.Api.AudioSummary to EchoNestModel.AudioSummary
- Mapping from type EchoNest.Api.TrackProfile to EchoNestModel.Profile
Exception of type 'AutoMapper.AutoMapperConfigurationException' was thrown.
Mapping definition
var map = Mapper.CreateMap<TrackProfile, Profile>();
map.ForMember(dest => dest.ForeignIds, opt => opt.ResolveUsing<ForeignIdResolver>());
map.ForMember(dest => dest.ForeignReleaseIds, opt => opt.ResolveUsing<ForeignReleaseIdResolver>());
map.ForMember(s => s.Media, t => t.Ignore());
map.ForMember(s => s.ProfileId, t => t.Ignore());
map.ForMember(s => s.AudioSummary, t => t.MapFrom(s => s.AudioSummary));
I've added the following two lines but a totally different error occurs :
map.ForMember(s => s.AudioSummary.Profile, t => t.Ignore());
map.ForMember(s => s.AudioSummary.AudioSummaryId, t => t.Ignore());
Expression 's => s.AudioSummary.Profile' 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.
Parameter name: lambdaExpression
How can I successfully map AudioSummary ?
Source object
Target object
EDIT: In general, try AutoMapper.Mapper.AssertConfigurationIsValid();, this will show you all possible problems in your mapper setup.
From the information you provided, it looks like you need to define map for the AudioSummary classes (dest and source) as well:
[TestFixture]
public class MappingTest
{
public class SourceAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class TrackProfile
{
public string Whatever { get; set; }
public SourceAudioSummary AudioSummary { get; set; }
}
public class DestAudioSummary
{
public int Id { get; set; }
public string OtherData { get; set; }
}
public class Profile
{
public string Whatever { get; set; }
public DestAudioSummary AudioSummary { get; set; }
}
[Test]
public void Mapping()
{
Mapper.CreateMap<SourceAudioSummary, DestAudioSummary>();
Mapper.CreateMap<TrackProfile, Profile>();
var trackProfile = new TrackProfile
{
Whatever = "something",
AudioSummary = new SourceAudioSummary
{
Id = 1,
OtherData = "other"
}
};
var profile = Mapper.Map<TrackProfile, Profile>(trackProfile);
Assert.That(profile.Whatever == "something");
Assert.That(profile.AudioSummary.Id == 1);
Assert.That(profile.AudioSummary.OtherData == "other");
}
}