Automapper configuration for grouping the data - c#

I am having the following models
Source:
public class Opportunity
{
public Guid Id { get; set; }
public string Name { get; set; }
public Guid QuotationId { get; set; }
public int? QuotationNumber { get; set; }
public int? QuotationVersionNumber { get; set; }
}
Target:
public class OpportunityDto
{
public Guid Id { get; set; }
public string Name { get; set; }
public List<QuotationDto> Quotations { get; set; }
}
public class QuotationDto
{
public Guid Id { get; set; }
public int Number { get; set; }
public int VersionNumber { get; set; }
}
The data I would fetch from my database would be flat as the Opportunity model and my api is exposing the OpportunityDto model.
so, in my auto-mapper configuration, I have the following code:
services
.AddSingleton(new MapperConfiguration(cfg =>
{
cfg.CreateMap<OpportunityDto, Opportunity>().ReverseMap();
cfg.CreateMap<QuotationDto, Quotation>().ReverseMap();
}).CreateMapper())
what I want to achive is a list of unique opportunities and each opportunity would have a nested member which would have the list of the quotations.
how can I make the automapper perform this grouping? right now the Quotations member of the opportunityDto returned from the api is always empty.

AutoMapper Configuration
You can do something like the following:
public static void InitialiseMapper()
{
Mapper.Initialize(cfg =>
{
cfg.CreateMap<IEnumerable<Opportunity>, OpportunityDto>()
.ForMember(x => x.Id, x => x.MapFrom(y => y.FirstOrDefault().Id))
.ForMember(x => x.Name, x => x.MapFrom(y => y.FirstOrDefault().Name))
.ForMember(x => x.Quotations,
x => x.MapFrom(y => Mapper.Map<IEnumerable<Opportunity>, IEnumerable<QuotationDto>>(y).ToArray()))
;
cfg.CreateMap<Opportunity, QuotationDto>()
.ForMember(x => x.Id, x => x.MapFrom(y => y.QuotationId))
.ForMember(x => x.Number, x => x.MapFrom(y => y.QuotationNumber))
.ForMember(x => x.VersionNumber, x => x.MapFrom(y => y.QuotationVersionNumber))
;
});
}
Test
Which then successfully knows how to map as demonstrated by the following test:
[TestMethod]
public void TestMethod1()
{
var oppo1Guid = Guid.NewGuid();
var opportunities = new List<Opportunity>
{
new Opportunity
{
Id = oppo1Guid,
Name = "Mikeys Oppurtunity",
QuotationId = Guid.NewGuid(),
QuotationNumber = 169,
QuotationVersionNumber = 80,
},
new Opportunity
{
Id = oppo1Guid,
Name = "Mikeys Oppurtunity",
QuotationId = Guid.NewGuid(),
QuotationNumber = 170,
QuotationVersionNumber = 20,
}
};
var dtos = Mapper.Map<IEnumerable<Opportunity>, OpportunityDto>(opportunities);
var json = JsonConvert.SerializeObject(dtos, Formatting.Indented);
Console.WriteLine(json);
}
And the output is
Test Output
{
"Id": "623c17df-f748-47a2-bc7e-35eb124dbfa3",
"Name": "Mikeys Oppurtunity",
"Quotations": [
{
"Id": "ad8b31c2-6157-4b7f-a1f2-9f8cfc1474b7",
"Number": 169,
"VersionNumber": 80
},
{
"Id": "515aa560-6a5b-47da-a214-255d1815e153",
"Number": 170,
"VersionNumber": 20
}
]
}

#Afflatus I would give you the idea that you can follow. I am assuming that you are using AspNetCore based on your services variable.
You can create an extension method like this, to later make a call on your ConfigurationServices like services.RegisterMappingsWithAutomapper():
public static IServiceCollection RegisterMappingsWithAutomapper(this IServiceCollection services)
{
var mapperConfig = AutoMapperHelper.InitializeAutoMapper();
services.AddScoped<IMapper>(provider => new Mapper(mapperConfig));
return services;
}
The InitializeAutoMapper is below:
public static class AutoMapperHelper
{
public static MapperConfiguration InitializeAutoMapper()
{
//Load here all your assemblies
var allClasses = AllClasses.FromLoadedAssemblies();
MapperConfiguration config = new MapperConfiguration(cfg =>
{
if (allClasses != null)
{
//here normally I add another Profiles that I use with reflection, marking my DTOs with an interface
cfg.AddProfile(new MappingModelsAndDtos(allClasses));
cfg.AddProfile(new MyCustomProfile());
}
});
return config;
}
}
now you need to implement the Profile, in this case MyCustomProfile
public class ModelProfile : Profile
{
public ModelProfile()
{
//put your source and destination here
CreateMap<MySource, MyDestination>()
.ConvertUsing<MySourceToMyDestination<MySource, MyDestination>>();
}
}
you need to implement MySourceToMyDestination class then.
Bellow is an example of the code of how I am using it in my projects
public class ApplicationModel2ApplicationDto : ITypeConverter<ApplicationModel, ApplicationDto>
{
public ApplicationDto Convert(ApplicationModel source, ApplicationDto destination, ResolutionContext context)
{
var mapper = context.Mapper;
try
{
destination = new ApplicationDto
{
ApplicationId = source.ApplicationId,
ApplicationName = source.ApplicationName,
Documents = mapper.Map<IEnumerable<DocumentDto>>(source.Documents),
Tags = mapper.Map<IEnumerable<TagInfoDto>>(source.TagInfos)
};
}
catch
{
return null;
}
return destination;
}
}
Hope this helps

Related

One to many relation cannot get the Attendees List

i'm using actually abp 4.4.0, hello, I am trying to recode the https://community.abp.io/articles/creating-an-event-organizer-application-with-the-blazor-ui-wbe0sf2z but for EfCore here is the code of the tables:
public class Event : FullAuditedAggregateRoot<Guid>
{
[Required]
public string Title { get; set; }
public string Description { get; set; }
public DateTime StartTime { get; set; }
public bool IsFree { get; set; }
public ICollection<EventAttendee> Attendees { get; set; }
public Event()
{
Attendees = new Collection<EventAttendee>();
}
}
public class EventAttendee : Entity<int>
{
public Event Event { get; set; }
public Guid EventId { get; set; }
public Guid AttendeeId { get; set; }
}
Here the DbContextModelCreatingExtensions:
builder.Entity<Event>(t =>
{
t.ToTable("Events");
t.ConfigureByConvention();
t.HasMany(x => x.Attendees)
.WithOne(x => x.Event)
.HasForeignKey(x => x.EventId)
.IsRequired(false);
});
builder.Entity<EventAttendee>(t =>
{
t.ToTable("Attendees");
t.ConfigureByConvention();
});
the DBContext referemcement
public DbSet<Event> Events { get; set; }
public DbSet<EventAttendee> Attendees { get; set; }
and the seed
public async Task SeedAsync(DataSeedContext context)
{
await _eventRepository.InsertAsync(new Event()
{
Title = "First Event",
Description = "This is a test",
IsFree = true,
StartTime = DateTime.Now.AddDays(2),
Attendees = new List<EventAttendee>()
{
new EventAttendee(){AttendeeId = Guid.NewGuid()},
new EventAttendee(){AttendeeId = Guid.NewGuid()},
new EventAttendee(){AttendeeId = Guid.NewGuid()}
}
});
}
And the EventAppService
public class EventAppService : ManagerAppService, IEventAppService
{
private readonly IRepository<Event, Guid> _eventRepository;
private readonly IRepository<IdentityUser> _userRepository;
public EventAppService(IRepository<Event, Guid> eventRepository, IRepository<IdentityUser> userRepository)
{
_eventRepository = eventRepository;
_userRepository = userRepository;
}
public async Task<EventDetailDto> GetAsync(Guid id)
{
var #event = await _eventRepository.GetAsync(id);
var attendeeIds = #event.Attendees.Select(a => a.AttendeeId).ToList();
var queryable = await _userRepository.GetQueryableAsync();
var query = queryable
.Where(u => attendeeIds.Contains(u.Id));
var attendees = (await AsyncExecuter.ToListAsync(query))
.ToDictionary(x => x.Id);
var result = ObjectMapper.Map<Event.Event, EventDetailDto>(#event);
foreach (var attendeeDto in result.Attendees)
{
attendeeDto.UserName = attendees[attendeeDto.UserId].UserName;
}
return result;
}
But i have a problem, when i execute the DBMigrator, the seed is created correctely but when i want to get my events, the attendees list is empty
{
"title": "First Event",
"description": "This is a test",
"isFree": true,
"startTime": "2021-09-23T07:48:34.663988",
"attendees": [],
"creationTime": "2021-09-21T07:48:35.656599",
"creatorId": null,
"id": "39ff1912-edee-0d2a-9aca-00a2ff5ed128"
}
and I don't understand why he can't get the attendees back, if I forgot something ?
Thank you in advance
For relational DB (EF Core), define DefaultWithDetailsFunc:
Configure<AbpEntityOptions>(options =>
{
options.Entity<Event>(eventOptions =>
{
eventOptions.DefaultWithDetailsFunc = query => query.Include(e => e.Attendees);
});
});
Alternatively, explicitly load the collection:
var #event = await _eventRepository.GetAsync(id);
await _eventRepository.EnsureCollectionLoadedAsync(#event, e => e.Attendees); // Add this
var attendeeIds = #event.Attendees.Select(a => a.AttendeeId).ToList();
Reference: https://docs.abp.io/en/abp/4.4/Entity-Framework-Core

odata: How do I expand an nested ICollection of items using $expand in Odata using .net core 3.1

OData url:
https://{{baseUrl}}/api/vehicles?$expand=OwnershipHistories
I want to return a vehicle(s) and than $expand the OwnershipHistories as a List of owner ship histories.
But I only get one (1) Ownershiphistorie.
Question: Why can I not get all the ownership histories???
I have this on my controller:
[HttpGet]
[EnableQuery(PageSize = 10, MaxExpansionDepth = 20)]
[ODataRoute("vehicles")]
I use this url: {{baseUrl}}/api/vehicles?$expand=OwnershipHistories
I use the following c# code ():
public async Task<IQueryable<VehicleEntity>> GetVehicles()
{
var vehicles = _context.Vehicles; // .Include(v => v.OwnershipHistories) => ??? Is not working...
return vehicles;
}
The result is:
{
"#odata.context": "https://localhost:5001/api/$metadata#Vehicles(OwnershipHistories())",
"value": [
{
"id": 1,
"registrationNumber": "10STNV",
"vehicleIdentificationNumber": "JF1BP9LLA6G052053",
// more fields
"**OwnershipHistories**": [ ?????? Only **one** record, not the **8** records I expect ?????)
{
"VweID": 1,
"registrationNumber": "10STNV",
// more fields
}
]
},
}
This is the query for the database:
I have two entities and they are views in the sql database:
OwnershipHistoryEntity:
[Table("EigenaarsHistorie", Schema = "Voertuig")]
public class OwnershipHistoryEntity
{
[Key]
public int VweID { get; set; } // VDNI
// more fields
public virtual VehicleEntity Vehicle { get; set; }
}
VehicleEntity:
namespace VWE.MijnVWE.Vehicle.Api.DAL.Entities
{
[Table("VoertuigInformatie", Schema = "Voertuig")]
public class VehicleEntity
{
[Key]
public int VweID { get; set; } // VDNI
public string Kenteken { get; set; }
public string VoertuigIdentificatieNummer { get; set; }
// more fields
[ForeignKey("VweID")]
public ICollection<OwnershipHistoryEntity> OwnershipHistories { get; set; } = new List<OwnershipHistoryEntity>();
}
}
EdmModel builder:
using Microsoft.AspNet.OData.Builder;
using Microsoft.OData.Edm;
using VWE.MijnVWE.Vehicle.Api.DAL.Entities;
namespace VWE.MijnVWE.Vehicle.Api.BLL.Builders
{
public static class ModelBuilder
{
private static IEdmModel _edmModel;
public static IEdmModel GetEdmModel()
{
return GetExplicitEdmModel();
}
static IEdmModel GetExplicitEdmModel()
{
if (_edmModel == null)
{
var modelBuilder = new ODataConventionModelBuilder();
var vehicles = modelBuilder.EntitySet<VehicleEntity>("Vehicles");
var ownershipHistories = modelBuilder.EntitySet<OwnershipHistoryEntity>("OwnershipHistories");
modelBuilder.Namespace = "vwe.mijnvwe.vehicle";
modelBuilder.ContainerName = "vehicleApiContainer";
vehicles.EntityType.Name = "vehicles";
vehicles.EntityType.HasKey(k => k.VweID);
vehicles.EntityType.HasMany(v => v.OwnershipHistories);
vehicles.HasManyBinding(v => v.OwnershipHistories, "OwnershipHistories");
ownershipHistories.EntityType.Name = "ownershipHistories";
ownershipHistories.EntityType.HasKey(k => k.VweID);
ownershipHistories.EntityType.HasRequired(o => o.Vehicle, (o, t) => o.VweID == t.VweID);
vehicles.EntityType.Property(p => p.VweID).Name = "id";
vehicles.EntityType.Property(p => p.Kenteken).Name = "registrationNumber";
vehicles.EntityType.Property(p => p.VoertuigIdentificatieNummer).Name = "vehicleIdentificationNumber";
// more fields
ownershipHistories.EntityType.Property(p => p.Kenteken).Name = "registrationNumber";
ownershipHistories.EntityType.Property(p => p.EventDatum).Name = "eventDate";
ownershipHistories.EntityType.Property(p => p.SoortAansprReferentieCode).Name = "liabilityReferenceCode";
ownershipHistories.EntityType.Property(p => p.RegistratieDatumAansprakelijkheid).Name = "from";
ownershipHistories.EntityType.Property(p => p.RegistratieDatumAansprakelijkheidTot).Name = "to";
ownershipHistories.EntityType.Property(p => p.DagenInBezit).Name = "numberOfDays";
ownershipHistories.EntityType.Property(p => p.DatumGewijzigd).Name = "changedDateTime";
_edmModel = modelBuilder.GetEdmModel();
}
return _edmModel;
}
}
}
Ef core modelBuilder:
public class VehicleDbContext : DbContext
{
public VehicleDbContext(DbContextOptions<VehicleDbContext> options)
: base(options)
{ }
public DbSet<VehicleEntity> Vehicles { get; set; }
public DbSet<OwnershipHistoryEntity> OwnershipHistories { get; set; }
// more fields
protected override void OnModelCreating(ModelBuilder builder)
{
// ...
base.OnModelCreating(builder);
}
}
This is the Select query:
SELECT [t].[VweID], [t].[AandrijvingOmschrijving],
//more fields.. [t0].[SoortAansprReferentieCode], [t0].[c0]
FROM (
SELECT TOP(#__TypedProperty_3) [v].[VweID], [v].[AandrijvingOmschrijving], [v].[AantalAangedrevenAssen],
// more fields
[v].[Werkingsbeginsel], [v].[Wielbasis]
FROM [Voertuig].[VoertuigInformatie] AS [v]
ORDER BY [v].[VweID]
) AS [t]
OUTER APPLY (
SELECT TOP(#__TypedProperty_1) #__TypedProperty_2 AS [c], [e].[VweID], [e].[DagenInBezit], [e].[DatumGewijzigd], [e].[EventDatum], [e].[Kenteken], [e].[RegistratieDatumAansprakelijkheid], [e].[RegistratieDatumAansprakelijkheidTot], [e].[SoortAansprReferentieCode], CAST(1 AS bit) AS [c0]
FROM [Voertuig].[EigenaarsHistorie] AS [e]
WHERE [t].[VweID] = [e].[VweID]
ORDER BY [e].[VweID]
) AS [t0]
ORDER BY [t].[VweID], [t0].[VweID]
It is quit similar to the following question:
Same question about washington school odata question
Washington school code odata example

Automapping child injecting data

I'm trying to map a parent/child to another parent/child where the child.id need to be read from a key/value dictionary.
I could do a foreach loop to map the children but I'm interrested to see if there is another way. The code is a simplified version of my model and the remoteDict is read async so using a IValueResolver seems unusable?
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ParentFrom, ParentTo>()
.ForMember(d => d.Children, o => o.MapFrom(s => s.Children));
cfg.CreateMap<(ChildFrom child, Dictionary<string, int> dict), ChildTo>()
.ForMember(d => d.Id, o => o.MapFrom(s => s.dict.GetValueOrDefault(s.child.ExternalId)));
});
var mapper = config.CreateMapper();
Dictionary<string, int> remoteDict = new Dictionary<string, int>();
remoteDict.Add("A1", 1);
remoteDict.Add("B1", 1);
ParentFrom p = new ParentFrom() { Id = 1001 };
p.Children.Add(new ChildFrom() { ExternalId = "A1" });
p.Children.Add(new ChildFrom() { ExternalId = "B1" });
ParentTo p2 = mapper.Map<ParentTo>(p);
/*
Missing type map configuration or unsupported mapping.
Mapping types:
ChildFrom -> ChildTo
mapping.ChildFrom -> mapping.ChildTo
*/
}
}
public class ParentFrom
{
public int Id { get; set; }
public List<ChildFrom> Children { get; set; } = new List<ChildFrom>();
}
public class ChildFrom
{
public string ExternalId { get; set; }
}
public class ParentTo
{
public int Id { get; set; }
public List<ChildTo> Children { get; set; }
}
public class ChildTo
{
public int Id { get; set; }
}
UPDATE
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ParentFrom, ParentTo>()
.ForMember(d => d.Children, o => o.MapFrom(s => s.Children));
cfg.CreateMap<ChildFrom, ChildTo>()
.ForMember(d => d.Id, o => o.MapFrom<FromRemote>());
});
var mapper = config.CreateMapper();
Dictionary<string, int> remoteDict = new Dictionary<string, int>();
remoteDict.Add("A1", 1);
remoteDict.Add("B1", 1);
ParentFrom p = new ParentFrom() { Id = 1001 };
p.Children.Add(new ChildFrom() { ExternalId = "A1" });
p.Children.Add(new ChildFrom() { ExternalId = "B1" });
ParentTo p2 = mapper.Map<ParentTo>(p, opt => opt.Items["dict"] = remoteDict);
}
}
public class FromRemote : IValueResolver<ChildFrom, ChildTo, int>
{
public int Resolve(ChildFrom source, ChildTo destination, int destMember, ResolutionContext context)
{
var dict = context.Items["dict"] as Dictionary<string, int>;
return dict.GetValueOrDefault(source.ExternalId);
}
}
public class ParentFrom
{
public int Id { get; set; }
public List<ChildFrom> Children { get; set; } = new List<ChildFrom>();
}
public class ChildFrom
{
public string ExternalId { get; set; }
}
public class ParentTo
{
public int Id { get; set; }
public List<ChildTo> Children { get; set; }
}
public class ChildTo
{
public int Id { get; set; }
}

Automapper Complex mapping from List to Object

I have object structure as below
public class Source
{
public Connection Connection { get; set;}
}
public class Connection
{
public string Id { get; set;
public IEnumerable<Distributor> Distributors { get; set;
}
public class Distributor { get; set; }
{
public DistributorType Type {get; set;}
public string X { get; set; }
public string Y { get; set; }
}
public class Destination
{
public Distribution Distribution { get; set;
}
public class Distribution
{
public string Id { get; set;}
public string X { get; set; }
public string Y { get; set; }
}
I would like to map the Source to Destination for the property Distribution. The mapping is as below
Source.Connection.Distributors.FirstOrDefault().X => Destination.Distribution.X
Source.Connection.Distributors.FirstOrDefault().Y => Destination.Distribution.Y
Source.Connection.Id => Destination.Distribution.Id
I have tried using the Custom Resolver but no luck
public class CustomDistributorResolver : IValueResolver<Source, Destination, Distribution >
{
public Distribution Resolve(Source source, Destination destination, Distribution destMember, ResolutionContext context)
{
var result = source.Connection.Distributors.FirstOrDefault(x => x.DistributorType =="ABC");
if (result == null) return null;
return new Distribution
{
Id = source.Connection?.Id,
X = result.X,
Y = result.Y
};
}
}
Mapping Pofile
CreateMap<Source, Destination>()
.ForMember(d => d.Distribution, opt => opt.MapFrom( new CustomDistributorResolver()));
I always get Distribution value as NULL.
I am not sure what I am doing wrong here on mapping.
-Alan-
I am not very sure what you will deal with the IEnumerable. So I use linq to approach.
var id = Source.Connection.Id;
var distributions = Source.Connection.Distributors.Select(m=> new Distribution()
{
Id = id,
X = m.X,
Y = m.Y,
});
Automapper would like :
CreateMap<Source, Destination>()
.ForMember(dest => dest.Distribution.Id ,opt => opt.MapFrom(src => src.Connection.Id))
.ForMember(dest => dest.Distribution.X, opt => opt.MapFrom(src => src.Connection.Distributors.FirstOrDefault().X))
.ForMember(dest => dest.Distribution.Y, opt => opt.MapFrom(src => src.Connection.Distributors.FirstOrDefault().Y));
You can use a type converter
private class DestinationConverter : ITypeConverter<Source, Destination>
{
public Destination Convert(Source source,
Destination destination,
ResolutionContext context)
{
var result = source.Connection.Distributors.FirstOrDefault(x => x.Type == "ABC");
if (result == null) return null;
destination = new Destination();
destination.Distribution = new Distribution
{
Id = source.Connection?.Id,
X = result.X,
Y = result.Y
};
return destination;
}
}
and register the converter
CreateMap<Source, Destination>().ConvertUsing<DestinationConverter>();

AutoMapper behaviour with readonly properties in source not being mapped to destination

I have a configuration that does a reverse map to unflatten the source object into a destination object, however, if the source object property is read-only it is automatically ignored and I have to manually map it.
Some of my source objects have up to 100 fields, creating manual mappings like this feels cumbersome and I'm unsure why this behavior exists in auto mapper.
See below code snippet for a working example of what I am describing.
using System;
using AutoMapper;
namespace automappertests
{
class Source
{
public int Field1Suffix1 { get; set; }
public int Field1Suffix2 { get; set; }
public int Field2Suffix1 { get; set; }
// !!Attention!!
// This is a readonly property with a private setter
public int Field2Suffix2 => Field2Suffix1;
}
class Destination
{
public Wrapper Field1 { get; set; }
public Wrapper Field2 { get; set; }
}
class Wrapper
{
public int Suffix1 { get; set; }
public int Suffix2 { get; set; }
public static Wrapper New(int suffix1, int suffix2) =>
new Wrapper
{
Suffix1 = suffix1,
Suffix2 = suffix2
};
}
class DestinationProfile : Profile
{
public DestinationProfile()
{
CreateMap<Destination, Source>()
.ReverseMap()
// !!Attention!!
// Why do I need to have an explicit ForMember MapFrom for the readonly property?
.ForMember(m => m.Field2, o => o.MapFrom(src => Wrapper.New(src.Field2Suffix1, src.Field2Suffix2)));
}
}
class Program
{
static void Main(string[] args)
{
var config = new MapperConfiguration(cfg => { cfg.AddProfile<DestinationProfile>(); });
var mapper = config.CreateMapper();
var source = new Source()
{
Field1Suffix1 = 1,
Field1Suffix2 = 2,
Field2Suffix1 = 3,
};
var destination = mapper.Map<Source, Destination>(source);
Console.WriteLine($"Field1.Suffix1 = {destination.Field1.Suffix1}");
Console.WriteLine($"Field1.Suffix2 = {destination.Field1.Suffix2}");
Console.WriteLine($"Field2.Suffix1 = {destination.Field2.Suffix1}");
Console.WriteLine($"Field2.Suffix2 = {destination.Field2.Suffix2}");
Console.WriteLine("Press any key to continue.");
Console.ReadKey();
}
}
}
Output without .ForMember:
Field1.Suffix1 = 1
Field1.Suffix2 = 2
Field2.Suffix1 = 3
Field2.Suffix2 = 0
Output with .ForMember:
Field1.Suffix1 = 1
Field1.Suffix2 = 2
Field2.Suffix1 = 3
Field2.Suffix2 = 3
Actually you have no setter. Add an explicit private setter.
public int Field2Suffix2 { get => Field2Suffix1; private set => Field2Suffix1 = value; }

Categories