AutoMapper ForMember method breaks the functionality of mapping - c#

I want to use AutoMapper in my .Net 6 APIs to convert the entity model User to DTO model UserDTO.
The User model class is:
public class User : BaseEntity
{
public Guid Id { get; set; }
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public string Avatar { get; set; } = null!;
public string Email { get; set; } = null!;
public ICollection<Book>? FavoriteBooks { get; set; }
}
And the UserDTO is a record as follows:
public record UserDTO(Guid Id, string FullName, string Avatar, string Email);
I have added the required package AutoMapper.Extensions.Microsoft.DependencyInjection v.12.0.0,
and the configuration steps are given below:
1- Create MappingProfile that inherits from the Profile class
public class MappingProfiles : Profile
{
public MappingProfiles()
{
CreateMap<User, UserDTO>()
.ForMember(
dest => dest.FullName,
opt => opt.MapFrom(src => string.Join(" ", src.FirstName, src.LastName))
);
}
}
2- Register the service in Program.cs file:
builder.Services.AddAutoMapper(typeof(Program));
3- Use the mapper as an injected service inside Service project:
public IEnumerable<UserDTO> GetAllUsers(bool trackChanges)
{
var users = _repository.User.GetAllUsers(trackChanges);
return _mapper.Map<IEnumerable<UserDTO>>(users);
}
When I call the GetAllUsers method in postman, I get the following error:
Error mapping types.
Mapping types:
List -> IEnumerable
After a few days of struggling and searching, I realized that the .ForMember() method breaks the functionality of the profile class. In other words, if I change the UserDTO record:
public record UserDTO(Guid Id, string FirsName, string Avatar, string Email);
the FullName filed changed to FirstName to have compatibility with the User model. Also change the MappingProfile class:
public class MappingProfiles : Profile
{
public MappingProfiles()
{
CreateMap<User, UserDTO>();
}
}
the GetAllUsers method works as expected. So to conclude, if I add the .ForMember() method to the constructor of the MappingProfile class as in documentation, it breaks the functionality of the CreatMap method.
How should I use the .ForMember() method to map the User model to the corresponding DTO? Is this method obsolete? Is there any replacement for this method?

I found 2 solutions:
Solution 1:
I created a method to get the full name of the user. The method name should be prefixed with get:
The naming convention can cover simpler examples where the source object has a property, method, or method with a “Get” as a prefix with the same name as the property of a destination object.
so I have modified the User model class and added the following method:
public class User : BaseEntity
{
... // User model properties
public string GetFullName() => $"{this.FirstName} {this.LastName}";
}
and removed the .ForMemeber()` method from the profile class:
public MappingProfiles()
{
CreateMap<User, UserDTO>();
}
Solution 2:
It seems that .ForMember() is obsolete, I have found an alternative for that, .ForCtorParam():
public MappingProfiles()
{
CreateMap<User, UserDTO>()
.ForCtorParam(
"FullName",
opt => opt.MapFrom(src => string.Join(" ", src.FirstName, src.LastName))
);
}
In these ways, I have converted my User model class to UserDTO.

Related

Map nested object with Automapper in C#

I use the latest version of AutoMapper 10.1.1 in my .NET Core project. I have a simple database for learning new words in a language I want to learn. I have the following tables:
Words
WordExamples
WordExampleTranslation
In Words there is an ID for the word, and in the WordExamples I refer to this ID to link an example for that word. In WordExampleTranslation I have a reference to the ID of WordExamples to have a translation in my language (just to understand what the example means). Every table has a lot of columns such as CreatedAt, CreatedBy and so on.
With Entity Framework Core, I read this data based on the word ID and I want to return to the UI only the essential fields.
public IQueryable<WordExample> GetAllByWordId(long wordId)
{
return _db.WordExamples
.Include(c => c.Word)
.Include(c => c.Translations)
.Where(r => r.WordId == wordId);
}
For that, I created 2 classes for basic information
public class BaseWordExample
{
public long LanguageId { get; set; }
public long WordId { get; set; }
public string Example { get; set; }
public IEnumerable<BaseWordExampleTranslation> Translations { get; set; }
}
public class BaseWordExampleTranslation
{
public long LanguageId { get; set; }
public long WordId { get; set; }
public long DestinationLanguageId { get; set; }
public string TraslationExample { get; set; }
}
Then, I have my MappingProfile for AutoMapper
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<WordExample, BaseWordExample>()
.ReverseMap();
CreateMap<WordExampleTranslation, BaseWordExampleTranslation>()
.ReverseMap();
}
}
Then, I have an API
public async Task<IActionResult> GetAllAsync(long wordId)
{
var list = _localDb.GetAllByWordId(wordId);
var model = _mapper.Map<List<BaseWordExample>>(list);
return model != null ? Ok(model) : NotFound();
}
I expect to receive a json mapped to the basic classes with all the data from WordExamples and also from its dependency table WordExampleTranslation. What I have is only the WordExamples values. The field Translations is not recognized by AutoMapper.
[
{
"id": 1,
"language": 5,
"wordId": 1,
"example": "Eu parto para Inglaterra",
"exampleHtml": "<i>Eu</i> <b>parto</b> para Inglaterra"
}
]
Then, I tried to change the MappingProfile like the following
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<WordExample, BaseWordExample>()
.ForMember(dest => dest.Translations,
act => act.MapFrom(src => src.Translations))
.ReverseMap();
CreateMap<WordExampleTranslation, BaseWordExampleTranslation>()
.ReverseMap();
}
}
but in this case I get this error
System.Reflection.TargetInvocationException: Exception has been thrown
by the target of an invocation.
---> System.MissingMethodException: Method not found: 'System.Collections.Generic.IEnumerable`1<WB.Domain.Base.BaseWordExampleTranslation>
WB.Domain.Base.BaseWordExample.get_Translations()'.
Update: I tried to convert the IQueryable result in a list
var list = _localDb.GetAllByWordId(wordId).ToList();
and then use Mapper but, again, I have only the main object. All the data from the dependency table WordExampleTranslation are ignored.
Try this one
public async Task<IActionResult> GetAllAsync(long wordId)
{
var list = _localDb.GetAllByWordId(wordId);
var model = _mapper.Map<List<BaseWordExample>, List<WordExample>>(list);
return model != null ? Ok(model) : NotFound();
}

EF Core can't pass value type via aggregate constructor

The problem
I have the need to initialise a new domain entity that accepts a basic value object via its constructor. The persistence mechanism behind the domain is SQL server driven by Entity Framework Core.
The issue I have is that when EF Core tries to create the database schema it can't associate the value object parameter with any mapped property.
What am I missing here? Any help would be much appreciated.
My simplified domain model
public sealed class Address
{
public string StreetAddress { get; }
public string Town { get; }
public string PostalCode { get; }
internal Address(string streetAddress, string town, string postalCode)
{
...
}
}
public sealed class PropertyListing
{
public Guid Id { get; }
public string Title { get; }
public string Description { get; }
public Address Address { get; }
public decimal GuidePrice { get; }
internal PropertyListing(Guid id, string title, string description, Address address, decimal guidePrice)
{
...
}
}
My database model configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<PropertyListing>(builder =>
{
builder.HasKey(model => model.Id);
builder.Property(model => model.Title).IsRequired();
builder.Property(model => model.Description).IsRequired();
builder.Property(model => model.GuidePrice).IsRequired();
builder.OwnsOne(model => model.Address, address =>
{
address.Property(model => model.StreetAddress).IsRequired();
address.Property(model => model.Town).IsRequired();
address.Property(model => model.PostalCode).IsRequired();
});
});
}
The results
When it comes time to create the database schema, Entity Framework throws the following exception:
System.InvalidOperationException: 'No suitable constructor found for entity type 'PropertyListing'. The following parameters could not be bound to properties of the entity: 'address'.'
My technology stack
Entity Framework Core 2.1.0-preview1-final
ASP.NET Core 2.0
Reference materials
Entity types with constructors
Owned entity types
Allow related entities to be passed to constructor of aggregate root

AutoMapper throws Unmapped property exception despite ignore operation is set

I'm trying to map my DTO class to the native one. Here are the classes:
public class CategoryResource
{
public int Id { get; set; }
public string Code { get; set; }
public string Description { get; set; }
}
public class Category
{
public int Id { get; set; }
[Required]
[StringLength(255)]
public string Code { get; set; }
[StringLength(255)]
public string Description { get; set; }
public ICollection<CategoryToProduct> Products { get; set; }
public Category()
{
Products = new Collection<CategoryToProduct>();
}
}
and the usage
var category = mapper.Map<CategoryResource, Category>(categoryResource);
As above code throws an error of this kind:
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
=========================================================================== CategoryResource -> Category (Destination member list)
Unmapped properties: Products
I changed my mapping profile to:
CreateMap<CategoryResource, Category>()
.ForMember(x=>x.Products, opt => opt.Ignore());
But still, I have the same error.
Could you please advice me what I do wrong here?
I already restarted the IIS and AutoMapper version is 6.2.2
To answer question from comments, that's the whole MappingProfile
public class MappingProfile : Profile
{
public MappingProfile()
{
//Domain to API
CreateMap<Type, TypeResource>();
CreateMap<Unit, UnitResource>();
CreateMap<Category, CategoryResource>();
//API to domain
CreateMap<TypeResource, Type>();
CreateMap<UnitResource, Unit>();
CreateMap<CategoryResource, Category>()
.ForMember(x=>x.Products, opt => opt.Ignore());
}
}
As of 6.2, AutoMapper creates type maps on the fly (documented here):
When you call Mapper.Map for the first time, AutoMapper will create the type map configuration and compile the mapping plan.
Given that the only thing MappingProfile is doing in your example code is ignoring the Products property, this leads me to believe that MapperProfile is not being registered.
See Integrating AutoMapper with ASP.NET Core DI by Jimmy Bogard for more details on how to register your custom Profiles. To summarise, you can use the AutoMapper.Extensions.Microsoft.DependencyInjection nuget package and either use the defaults or provide your own list of assemblies that need to be scanned.

Unable to determine the serialization information for i => i.Id using interfaces

First up, I know there is already questions out there with this error message, but I haven't found any that relate to the use of using interfaces with this type of query.
I'm currently trying to update a MongoDB entity using the C# Driver 2.0. However, I'm getting an error when it's trying to build the query (I'm assuming it's the Builders<T>.Filter.Eq(i => i.Id, entity.Id) bit of code) and I'm getting the following error:
Unable to determine the serialization information for i => i.Id.
I've got the following class that I'm trying to update
public interface IEntity {
string Id { get; set; }
}
public interface ITrack : IEntity {
string Name { get; set; }
}
public class TrackDTO : ITrack
{
[BsonId]
public string Id { get; set; }
public string Name { get; set; }
}
I'm then using the interface to save the object into the database using the following method in a generic DAO class to replace the entire document. Note, in the example below T is coded as ITrack (i.e. TrackDao = new Dao<ITrack>) but when the object is passed in at runtime it's a TrackDTO object (which is correct):
public async Task<T> Save(T entity)
{
// Save the entity to the collection.
ReplaceOneResult result = await _collection.ReplaceOneAsync(Builders<T>.Filter.Eq(i => i.Id, entity.Id), entity, new UpdateOptions() { IsUpsert = true });
// return the saved user object.
return entity;
}
I don't know if the Id property of my IEntity class also requires the [BsonId] attribute, but I'd like to avoid this if possible, as I want to keep my model layer (where IEntity resides) free of any database platform specific references.
I've also tried adding the following class map which has had no effect either:
BsonClassMap.RegisterClassMap<TrackDTO>(cm =>
{
cm.AutoMap();
cm.MapMember(c => c.Id).SetSerializer(new StringSerializer(BsonType.ObjectId));
cm.SetIdMember(cm.GetMemberMap(c => c.Id));
});
For the same reason as not having the [BsonId] attributes in the Model layer, I don't want to have the Model classes decorated with [BsonKnownTypes] that reference DTO objects, however I don't mind if this needs to occur as part of a class map.
For
"Unable to determine the serialization information for i => i.Id."
Try to use: nameof().
Builders<T>.Filter.Eq(nameof(IEntity.Id), entity.Id)
2.
...but I'd like to avoid this if possible, as I want to keep my model
layer (where IEntity resides) free of any database platform specific
references.
My solution for problem like your and similar problems:
public interface IEntity {
[BsonId]
string Id { get; set; }
string IdEntity { get; set; }
}
[BsonIgnoreExtraElements(Inherited = true)]
public abstract class BaseEntity : IEntity
{
[BsonRepresentation(BsonType.ObjectId)]
public virtual string Id { get; set; }
[BsonIgnoreIfNull]
public string IdEntity { get; set; }
}

Automapper: How to not repeat mapping config from complex type to base class

I have a bunch of DTO classes that inherit from this CardBase:
// base class
public class CardBase
{
public int TransId {get; set; }
public string UserId { get; set; }
public int Shift { get; set; }
}
// one of the concrete classes
public class SetNewCardSettings : CardBase
{
// specific properties ...
}
In my MVC project I have a bunch of view models with a AuditVm complex type that has the same properties of CardBase:
public class AuditVm
{
public int TransId {get; set; }
public string UserId { get; set; }
public int Shift { get; set; }
}
public class CreateCardVm : CardVm
{
// specific properties here ...
public AuditVm Audit { get; set }
}
Those view models cannot inherit from AuditVm because each of them already has a parent. I thought I could setup my mapping like below so I would not have to specify the map from AuditVm to the CardBase for every view model that has AuditVm as a complex type. But it is not working. How do I properly map from a complex type to a flatten type with properties on the base class?
Mapper.CreateMap<AuditorVm, CardBase>()
.Include<AuditorVm, SetNewCardSettings>();
// this does not work because it ignores my properties that I map in the second mapping
// if I delete the ignore it says my config is not valid
Mapper.CreateMap<AuditorVm, SetNewCardSettings>()
.ForMember(dest => dest.Temp, opt => opt.Ignore())
.ForMember(dest => dest.Time, opt => opt.Ignore());
Mapper.CreateMap<CreateCardVm, SetNewCardSettings>()
// this gives me an error
.ForMember(dest => dest, opt => opt.MapFrom(src => Mapper.Map<AuditorVm, SetNewCardSettings>(src.Auditor)));
// I also tried this and it works, but it does not map my specific properties on SetNewCardSettings
//.ConvertUsing(dest => Mapper.Map<AuditorVm, SetNewCardSettings>(dest.Auditor));
UPDATE:
here is the fiddle https://dotnetfiddle.net/iccpE0
.Include is for a very specific case--you have two identically-structured class hierarchies you'd like to map, for example:
public class AEntity : Entity { }
public class BEntity : Entity { }
public class AViewModel : ViewModel { }
public class BViewModel : ViewModel { }
Mapper.CreateMap<Entity, ViewModel>()
.Include<AEntity, AViewModel>()
.Include<BEntity, BViewModel>();
// Then map AEntity and BEntity as well.
So unless you have this kind of situation, .Include isn't the right thing to use.
I think your best bet is to use ConstructUsing:
Mapper.CreateMap<AuditVm, CardBase>();
Mapper.CreateMap<AuditVm, SetNewCardSettings>()
.ConstructUsing(src =>
{
SetNewCardSettings settings = new SetNewCardSettings();
Mapper.Map<AuditVm, CardBase>(src, settings);
return settings;
})
.IgnoreUnmappedProperties();
Mapper.CreateMap<CreateCardVm, SetNewCardSettings>()
.ConstructUsing(src => Mapper.Map<SetNewCardSettings>(src.Audit))
.IgnoreUnmappedProperties();
I've also incorporated this answer's extension method to ignore all unmapped properties. Since we're using ConstructUsing, AutoMapper doesn't know that we've already taken care of those properties.
Updated fiddle: https://dotnetfiddle.net/6ZfZ3z

Categories