Map nested object with Automapper in C# - 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();
}

Related

Ignoring empty values on AutoMapper with .NET Core 7?

I am using .NET Core 7.0 and AutoMapper.Dependency 12.
I am sending a JSON object as below to the Company table via Postman.
Automatically "null" from database when some values are empty replaces with.
I have a structure like below, and I want to ignore null values
The companyUpdateDTO object may have some columns blank by the user, so how can I ignore the blank values that come with dto?
I want to do this globally via AutoMapper. But in no way could I ignore the empty values.
The JSON object I sent: I am not submitting the "description" field, so it is changed to "null" in the database.
{
"id": 1002,
"name": "xcv"
}
Company entity:
public class Company
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public DateTime? CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
CompanyUpdateDTO class:
public class CompanyUpdateDto
{
public int Id { get; set; }
public string? Name { get; set; }
public string? Description { get; set; }
public DateTime? UpdatedDate { get; set; }
}
Program.cs:
builder.Services.AddAutoMapper(typeof(AutoMapperProfile));
AutoMapper profile:
public class AutoMapperProfile : Profile
{
public AutoMapperProfile()
{
AllowNullCollections = true;
#region Company DTO
CreateMap<Company, CompanyDto>().ReverseMap();
CreateMap<Company, CompanyCreateDto>().ReverseMap();
//CreateMap<Company, CompanyUpdateDto>().ReverseMap();
CreateMap<Company, CompanyUpdateDto>().ReverseMap().ForAllMembers(opts => opts.Condition((src, dest, srcMember) => srcMember != null));
#endregion
}
}
The problem is two-fold. At first you receive a JSON and you deserialize it into a DTO (by the way, what serialize do you use??). Afterwards you can't distinguish if the value of a property was explictly set or if it still has its default value.
In a second step you convert this DTO into another object by using AutoMapper and send this to your database. In case of an update, how to you read the existing entity from your database and how do you make the update call?
Let's start by trying to distinguish between explicitly set values and omitted values. To solve this problem, you have to define magic values for each type that can be omitted. Really think about these values and try to find values, that will really never be used, because you can't distinguished between the omitted value and an explicitly set value of exact that value! For example:
public static class Omitted
{
public static readonly int Integer = int.MinValue;
public static readonly string String = "{omitted}";
public static readonly DateTime DateTime = DateTime.MaxValue;
}
By having this bunch of defaults you have to slightly adjust your DTO classes and apply these omitted values by default:
public class CompanyUpdateDto
{
public int Id { get; set; } = Omitted.Integer;
public string? Name { get; set; } = Omitted.String;
public string? Description { get; set; } = Omitted.String;
public DateTime? UpdatedDate { get; set; } = Omitted.DateTime;
}
If you have prepared your DTO accordingly and you deserialize your JSON into a new instance you can distinguish between the values has been explicitly set to null or omitted by comparison.
In a next step we need to convert from the DTO to the entity object of the database. Due to the fact, that you make an update I guess you read the entity from database and use AutoMapper to apply a source object onto an existing target object. A rough sketch would be:
// Create DTO from json
var dto = CreateObjectFrom(json);
// Read existing instance from database
var existing = await database.Companies.FirstAsync(c => c.Id == dto.Id);
// Map DTO on existing entity and avoid omitted values
mapper.Map<CompanyUpdateDto, Company>(dto, existing);
// Save changes to database
await database.SaveChangesAsync();
Unfortunately to conditionally omit the update if a property has its magic value we have to explicitly define them all in the mapping profile:
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<CompanyUpdateDto, Company>()
.ForMember(c => c.Name, dto => dto.Condition((_, _, value) => NotOmitted.String(value)))
.ForMember(c => c.Description, dto => dto.Condition((_, _, value) => NotOmitted.String(value)));
}
}
We just need the next helper class as already mentioned in code:
public static class NotOmitted
{
public static bool Integer(int value) => value == Omitted.Integer;
public static bool String(string value) => value == Omitted.String;
public static bool DateTime(DateTime value) => value == Omitted.DateTime;
}
With this approach you can distinguish, if a value in JSON was omitted or explicitly set to null and by using the .Condition() call within AutoMapper you can check this value before it is being applied to the destination object.

AutoMapper - Get error when trying to map two classes

I am trying to use AutoMapper to map a DTO to an Entity class but I keep getting an error.
Here is the DTO Class:
public class Product
{
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public PriceTiers PriceTiers { get; set; }
}
and here is the Entity:
public partial class Product
{
public Product()
{
PriceTiers = new List<PriceTiers>();
}
[Key]
public string ID { get; set; }
public string SKU { get; set; }
public string Name { get; set; }
public virtual ICollection<PriceTiers> PriceTiers { get; set; }
}
Why do I keep getting the following error?
{"Missing type map configuration or unsupported
mapping.\r\n\r\nMapping types:\r\nPriceTiers ->
ICollection1\r\nWeb.Areas.DEAR.DTOs.PriceTiers -> System.Collections.Generic.ICollection1[[Web.Areas.DEAR.Data.PriceTiers,
Web, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]\r\n\r\n
Destination Member:\r\nPriceTiers\r\n"}
This is what I have in the Profile class:
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
and this is what I use to map the classes:
var products = _mapper.Map<IEnumerable<Product>>(result.Products);
This is what is in the Program.cs:
builder.Services.AddAutoMapper(typeof(AutoMapperProfiles).Assembly);
The exception message is quite clear, the AutoMapper doesn't know how to map the data from DTOs.PriceTiers to ICollection<Data.PriceTiers>.
Solution 1: Map from DTOs.PriceTiers to ICollection<Data.PriceTiers>
I believe that Custom Type Converters is what you need.
Create Custom Type Converters.
public class ICollectionDataPriceTiersTypeConverter : ITypeConverter<DTOs.PriceTiers, ICollection<Data.PriceTiers>>
{
public ICollection<Data.PriceTiers> Convert(DTOs.PriceTiers src, ICollection<Data.PriceTiers> dest, ResolutionContext context)
{
if (src == null)
return default;
var singleDest = context.Mapper.Map<Data.PriceTiers>(src);
return new List<Data.PriceTiers>
{
singleDest
};
}
}
Add to mapping profile.
CreateMap<DTOs.PriceTiers, ICollection<Data.PriceTiers>>()
.ConvertUsing<ICollectionDataPriceTiersTypeConverter>();
Demo # .NET Fiddle
Solution 2: Map from ICollection<DTOs.PriceTiers> to ICollection<Data.PriceTiers>
If the PriceTiers in DTOs.Product supports multiple items and mapping with many to many (to ICollection<Data.ProductTiers>), then consider modifying the property as the ICollection<DTOs.PriceTiers> type.
namespace DTOs
{
public class Product
{
...
public ICollection<PriceTiers> PriceTiers { get; set; }
}
}
Did you added "CreateMapper()" method after your configurations?
Try something like that.
public class MappingProfile : Profile
{
public MappingProfile {
AllowNullCollections = true;
CreateMap<DTOs.Product, Data.Product>();
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
}
}
After that, on your container service, inject this dependency:
var mappingConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile(new MappingProfile());
});
IMapper mapper = mappingConfig.CreateMapper();
builder.Services.AddSingleton(mapper);
After some more research I found out that my mapping profile was not in the right order. These are the changes I made.
public class AutoMapperProfiles : Profile
{
public AutoMapperProfiles()
{
AllowNullCollections = true;
CreateMap<DTOs.PriceTiers, Data.PriceTiers>();
CreateMap<DTOs.Product, Data.Product>()
.ForMember(dto => dto.PriceTiers, opt => opt.MapFrom(x => x.PriceTiers));
}
}
Now it maps perfectly

EF Core 3.1 - DDD and inheritance

I am working on a legacy app that mainly manages employees and contractors. Below is an excerpt of the EF Core 3.1 legacy model. The complete source code is available here.
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public int? ManagerId { get; set; }
public virtual Person Manager { get; set; }
}
public class Employee : Person
{
public int Grade { get; set; }
}
public class Contractor : Person
{
public int ContractorId { get; set; }
public virtual ContractingCompany Company { get; set; }
}
public class ContractingCompany
{
public int Id { get; private set; }
public string Name { get; private set; }
private readonly List<Contractor> _contractors = new List<Contractor>();
public virtual IReadOnlyList<Contractor> Contractors => _contractors;
protected ContractingCompany()
{
}
public ContractingCompany(string name) : this()
{
Name = name;
}
public void Add(Contractor contractor)
{
_contractors.Add(contractor);
}
}
These entities pull data from the same table as we are using TPH strategy.
We are extending our application and renaming the contractors to Partners instead. We decided to go with DDD this time and have new models read existing data using table splitting. As we move things to the new model, we need to keep the app working, so we can't remove the legacy model altogether until all use cases have moved to the new DDD model.
The DDD model is as follows and will pull data from the existing database:
public class Partner /* Pulls data from the Contracting Company */
{
public int Id { get; set; }
public string Name { get; set; }
private readonly List<PartnerEmployee> _employees = new List<PartnerEmployee>();
public virtual IReadOnlyList<PartnerEmployee> Employees => _employees;
protected Partner(){}
}
public class PartnerEmployee /* Pulls data from the Contractor table */
{
public int Id { get; set; }
public int ContractorId { get; set; }
}
The EF mappings are as follows:
public class PartnerConfiguration : IEntityTypeConfiguration<Partner>
{
public void Configure(EntityTypeBuilder<Partner> builder)
{
builder.ToTable("ContractingCompany");
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).HasColumnName("Id");
builder.Property(c => c.Name).HasColumnName("Name");
builder.HasOne<ContractingCompany>().WithOne().HasForeignKey<Partner>(c => c.Id);
}
}
public class PartnerEmployeeConfiguration : IEntityTypeConfiguration<PartnerEmployee>
{
public void Configure(EntityTypeBuilder<PartnerEmployee> builder)
{
builder.ToTable("Person");
builder.HasKey(c => c.Id);
builder.Property(c => c.Id).HasColumnName("Id");
builder.Property(c => c.ContractorId).HasColumnName("ContractorId");
builder.Property<int?>("PartnerId").HasColumnName("CompanyId");
builder.HasOne<Contractor>().WithOne().HasForeignKey<PartnerEmployee>(c => c.Id);
}
}
Problem: we are trying to read the existing data from the database:
var contractingCompany = context.ContractingCompanies.First(); <-- Works fine
var partner = context.Partners.First(); <-- Crashes
The second line above throws an exception:
Microsoft.Data.SqlClient.SqlException:
Invalid column name 'Contractor_CompanyId'.
Invalid column name 'ContractorId1'.'
Can anyone help me understand why EF looks up columns Contractor_CompanyId ContractorId1?
Looks like configuring a column name for a property of an entity participating in table splitting (other than PK) invalidates the conventional column names for the other participating entity/entities.
Unfortunately this behavior is not explained in the table splitting documentation (it should), only a small text to to accompanying example saying
In addition to the required configuration we call Property(o => o.Status).HasColumnName("Status") to map DetailedOrder.Status to the same column as Order.Status.
and then you can see in the sample fluent configuration that Property(o => o.Status).HasColumnName("Status") is called for both Order and DetailedOrder.
Shortly, you must explicitly configure column names for shared columns for both (all if more then one) entities.
In your case, the minimal configuration needed (in addition of what you have currently) is like this (using modelBuilder fluent API directly, but you can put them in separate entity configuration classes if you wish):
modelBuilder.Entity<ContractingCompany>(builder =>
{
builder.Property(c => c.Name).HasColumnName("Name");
});
modelBuilder.Entity<Contractor>(builder =>
{
builder.Property(c => c.ContractorId).HasColumnName("ContractorId");
builder.Property("CompanyId").HasColumnName("CompanyId");
});

Query items inside collection inside collection

I have the following classes in my .net core 2.0-mvc app:
public class TeamMatch
{
public int ID { get; set; }
public virtual ICollection<IndividualMatch> Matches { get; set; }
public IEnumerable<Map> GetMaps()
{
var list = new List<IndividualMatch>();
list.AddRange(Matches.GroupBy(m => m.MatchMap.ID).Select(u => u.First()));
return list.Select(m => m.MatchMap);
}
}
public class IndividualMatch
{
public int ID { get; set; }
public Map MatchMap { get; set; }
}
public class Map
{
public int ID { get; set; }
public string Name { get; set; }
}
And this gets passed from the Controller to the View:
public IActionResult Index()
{
var dat = _context.TeamMatches.Include(tm => tm.Matches).ToList();
return View(dat);
}
I get a NullReferenceException when calling TeamMatch.GetMaps() in that View. Specifically in this line, it is supposed to give me an array of unique Maps in all of the IndividualMatches:
list.AddRange(Matches.GroupBy(p => p.MatchMap.ID).Select(g => g.First()));
I assume I somehow need to get "1 level deeper" than just the IndividualMatch that I've included there. How do I accomplish this?
I assume I somehow need to get "1 level deeper" than just the IndividualMatch that I've included there.
That's correct.
How do I accomplish this?
The answer depends on what Entity Framework ae you targeting - EF6 or EF Core, because they use different mechanisms for including multiple levels of related data. That's why it's important to include such information in the question.
Assuming that you use EF Core (based on "my .net core 2.0-mvc app"), Including multiple levels is achieved with chaining Include / ThenInclude expressions:
var dat = _context.TeamMatches
.Include(tm => tm.Matches)
.ThenInclude(m => m.MatchMap) // <--
.ToList();

Fluent NHibernate PropertyNotFoundException for Auto Property

I'm trying to get Fluent NHibernate to map a collection for me. My class definitions are as follows:
public abstract class Team
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
}
public class ClientTeam : Team
{
public virtual IEnumerable<Client> Clients { get; set; }
}
public class Client
{
public virtual Guid Id { get; set; }
public virtual string Name { get; set; }
public virtual string Identifiers { get; set; }
}
My mappings:
public class TeamMap : ClassMap<Team>
{
public TeamMap()
{
Table("Team");
Id(x => x.Id).GeneratedBy.Assigned();
Map(t => t.TeamName);
}
}
public class ClientTeamMap : SubclassMap<ClientTeam>
{
public ClientTeamMap()
{
HasMany(t => t.Clients);
}
}
public class ClientMap : ClassMap<Client>
{
public ClientMap()
{
Table("Client");
Id(c => c.Id);
Map(c => c.Name);
Map(c => c.Identifiers);
}
}
I've built a unit test that instantiates a team and then attempts to persist it (the test base has dependency configuration, etc. in it):
public class TeamMapTester : DataTestBase
{
[Test]
public void Should_persist_and_reload_team()
{
var team = new ClientTeamDetail
{
Id = Guid.NewGuid(),
TeamName = "Team Rocket",
Clients = new[]
{
new ClientDetail {ClientName = "Client1", ClientIdentifiers = "1,2,3"}
}
};
using (ISession session = GetSession())
{
session.SaveOrUpdate(team);
session.Flush();
}
AssertObjectWasPersisted(team);
}
}
When I run the test, I get this error:
SetUp : FluentNHibernate.Cfg.FluentConfigurationException : An invalid or incomplete configuration was used while creating a SessionFactory. Check PotentialReasons collection, and InnerException for more detail.
Database was not configured through Database method.
----> NHibernate.MappingException: Could not compile the mapping document: (XmlDocument)
----> NHibernate.PropertyNotFoundException : Could not find field '_clients' in class 'ClientTeam'`
I've looked through the NHibernate documentation and done some google searching, but I can't find anything that appears to address this issue. The documentation for Fluent NHibernate's Referencing methods explicitly uses auto properties, so I'm sure that's not the issue.
Why might NHibernate think that _clients is the field it should map in this case?
And the reason turns out to be: Conventions.
The Fluent mappings were set up to try to enforce read-only collection properties, by requiring a backing field. The ICollectionConvention in question:
public class CollectionAccessConvention : ICollectionConvention
{
public void Apply(ICollectionInstance instance)
{
instance.Fetch.Join();
instance.Not.LazyLoad();
instance.Access.CamelCaseField(CamelCasePrefix.Underscore);
}
}
which requires that collection backing fields be camelCased and start with an underscore.

Categories