Map Model with Collection Inside - c#

I have an API endpoint something like this:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
return Ok(shoppingCart);
}
It works fine. Returns a ShoppingCart with ShoppingCartItems as expected.
But, I dont want to return shoppingCart but shoppingCartResource.
Something like that:
[HttpGet("{shoppingCartId}")]
public async Task<IActionResult> GetShoppingCart(int shoppingCartId)
{
var shoppingCart = await context.ShoppingCarts.Include(c => c.ShoppingCartItems).SingleOrDefaultAsync(c => c.Id == shoppingCartId);
if (shoppingCart == null)
return NotFound("There is no shoppingCart for specified query.");
var shoppingCartResource = mapper.Map<ShoppingCart, ShoppingCartResource>(shoppingCart);
return Ok(shoppingCartResource);
}
As you can see the ShoppingCart model has a Collection of ShoppingCartItem inside.
public class ShoppingCart
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItem> ShoppingCartItems { get; set; }
public ShoppingCart()
{
ShoppingCartItems = new Collection<ShoppingCartItem>();
}
}
And here is the ShoppingCartResource model
public class ShoppingCartResource
{
public int Id { get; set; }
public DateTime DateCreated { get; set; }
public ICollection<ShoppingCartItemResource> ShoppingCartItemResources { get; set; }
public ShoppingCartResource()
{
ShoppingCartItemResources = new Collection<ShoppingCartItemResource>();
}
}
Mapping Code is:
CreateMap<ShoppingCart, ShoppingCartResource>();
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();
There is no error but I got the only shoppingCartResource with empty ShoppingCartItemResource.

You are lacking single line telling AutoMapper where from it should map ShoppingCartItemResources because corresponding property in source object does not have the same name - ShoppingCartItems, thus it won't work automatically. Just add this:
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(
dst => dst.ShoppingCartItemResources,
opts => opts.MapFrom(src => src.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();

By default, AutoMapper follows a naming convention that automatically creates a map between a source property and a target property if their names match, and you don't have to configure anything for this.
But, if you want a map between two properties and their names don't match, then you have to explicitly define a configuration for that map.
Your ShoppingCartResource and ShoppingCart objects both have properties named Id and DateCreated, and so they got mapped automatically.
To get a map between your ICollection properties -
if you want an automatic map, you have to give them a matching name
if you want to keep distinction in naming, you have to explicitly define a configuration for their mapping. Something like -
CreateMap<ShoppingCart, ShoppingCartResource>()
.ForMember(d => d.ShoppingCartItemResources, opt => opt.MapFrom(s => s.ShoppingCartItems));
CreateMap<ShoppingCartItem, ShoppingCartItemResource>();

Related

How to handle dynamic get and set for derived properties that require a scoped service in a model class (C#)

I have a model class that is originally getting populated from a call to a SQL database that returns a bunch of Ids instead of friendly names. I have other collections that contain the mappings of Ids to friendly names.
I want to derive the friendly name property from the associated Id property as well as set the associated Id property when the friendly name property is set.
My model looks like this and is working with static collections (Lists) that contain the mappings.
(Note: The Id properties are default properties on the Entity base class)
public class ViewEntity : Entity, IDeletable
{
public int? Id { get; set; }
public string? Region { get; set; }
public string? District { get; set; }
public string? Circuit { get; set; }
public string? Cluster { get; set; }
public string? SchoolSystem { get; set; }
public string? Parent
{
get => DataAccessService.AllEntityMappings.Where(s => s.EntityId == ParentEntityId).Select(x => x.EntityName).FirstOrDefault();
set
{
ParentEntityId = DataAccessService.AllEntityMappings.Where(s => s.EntityName == value).Select(x => x.EntityId).FirstOrDefault();
}
}
public string? Sector
{
get => DataAccessService.SectorMappings.Where(s => s.Id == this.SectorId).Select(x => x.Description).FirstOrDefault();
set
{
SectorId = DataAccessService.SectorMappings.Where(s => s.Description == value).Select(x => x.Id).FirstOrDefault();
}
}
public string? TypeDoe
{
get => DataAccessService.TypeDoeMappings.Where(s => s.Id == SchoolTypeDoEId).Select(x => x.Description).FirstOrDefault();
set
{
SchoolTypeDoEId = DataAccessService.TypeDoeMappings.Where(s => s.Description == value).Select(x => x.Id).FirstOrDefault();
}
}
public string? Phase
{
get => DataAccessService.PhaseMappings.Where(s => s.Id == PhaseId).Select(x => x.Description).FirstOrDefault();
set
{
PhaseId = DataAccessService.PhaseMappings.Where(s => s.Description == value).Select(x => x.Id).FirstOrDefault();
}
}
public string? Quintile
{
get => DataAccessService.QuintileMappings.Where(s => s.Id == QuintileId).Select(x => x.Description).FirstOrDefault();
set
{
QuintileId = DataAccessService.QuintileMappings.Where(s => s.Description == value).Select(x => x.Id).FirstOrDefault();
}
}
}
The problem is I realized that I can't actually make the collections static and instead they need to live in a scoped service because some of these collections will change based on the logged in user.
This would require me to inject the DataAccessService into this model class in order to access the mappings which does not work in model classes for many reasons such as needing to create new instances constantly and also needing to Json serialize and deserialize.
I have determined I can solve this issue by maintaining default { get; set; } properties and handling the instantiation of the friendly name properties outside of the model class but this sounds like a lot of extra code and redundancy I would like to avoid.
I am looking for any suggestions to maintain this class design pattern with dynamically derived properties while allowing for accessing collections on a scoped service.

Optional loading of EF Core computed properties

I am working on mapping a few database entities for a reporting tool.
At the moment, there are a few computed properties depending on navigation properties for their loading. They've been bound through AutoMapper to ease the process.
public class Customer
{
public long Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Foo> Foos { get; set; }
public virtual ICollection<Bar> Bars { get; set; }
}
public class CustomerDto
{
public long Id { get; set; }
public string Name { get; set; }
public long TotalNumberOfFoos { get; set; }
public long NumberOfBarsWithCondition { get; set; }
}
public class CustomerProfile : Profile
{
public CustomerProfile()
{
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p => p.MapFrom(c => c.Foos.Count))
.ForMember(d => d.NumberOfBarsWithCondition, p => p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count()));
}
}
public class CustomerController : Controller
{
public async Task<List<CustomerDto>> CustomersByName(string name)
{
using (var db = new MyDbContext())
{
return await db.Customers
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider)
.Where(c => c.Name == name).ToListAsync();
}
}
}
Of course, the queries to retrieve these properties can become quite expensive as the size of the database increases, and they're not always needed in the final report.
The idea is to have an option for the user to choose if they want them included or not in the final report, but I haven't found a way to make the mapping optional at query time.
Is there a way to do this automatically, or am I forced to materialize the list and query these properties myself separately, losing the advantage of having computed properties from the database?
What you need is to utilize the so called AutoMapper Explicit expansion feature. Which should probably be called "explicit property inclusion" (not to be mixed with EF Core Include which is only for navigations), because it works for any destination property, and what it does it to rather include it automatically in the generated projection (Select), include it only when you opt-in explicitly.
So, you need first to configure such properties as ExplicitExpansion(), e.g.
CreateMap<Customer, CustomerDto>()
.ForMember(d => d.TotalNumberOfFoos, p =>
{
p.MapFrom(c => c.Foos.Count);
p.ExplicitExpansion();
})
.ForMember(d => d.NumberOfBarsWithCondition, p =>
{
p.MapFrom(c => c.Bars.Where(b => b.BarProperty == "something").Count());
p.ExplicitExpansion();
});
Now by default they won't be populated. Use the additional arguments of ProjectTo to pass which ones you want to "expand" (include), e.g.
.ProjectTo<CustomerDto>(_mapper.ConfigurationProvider, e => e.TotalNumberOfFoos)

AutoMapper from list in DTO to individual object

I have the DTO below in which I need to map it to a flat view model, the idea is that some of the properties that come through from the request are shared, but there could be a list of names that come through.
public class ShinyDTO
{
public List<UserDetails> Users { get; set; }
public string SharedPropertyOne { get; set; }
public string SharedPropertyTwo { get; set; }
}
public class UserDetails
{
public string Title { get; set; }
public string Forename { get; set; }
public string Surname { get; set; }
}
public class MyRealClass
{
public string SharedPropertyOne {get;set;}
public string SharedPropertyTwo {get;set;}
public string Title {get;set;}
public string Forename {get;set;}
public string Surname {get;set;}
}
//This will map all the shared properties
MyRealClass request = Mapper.Map<MyRealClass>(dto);
foreach (var record in dto.Users){
//This bit overwrites the properties set above and then I only have the properties set for Forename, Surname, etc...
request = Mapper.Map<MyRealClass>(record);
}
I need to map this into a list of MyRealClass. I've tried creating seperate mappings and then looping it within a foreach, but this kept removing the initial attributes.
I've also tried setting up the second mapping to ignore the properties set above and I couldn't get this working, it was still overwriting the properties.
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration
.CreateMap<MyRealClass, UserDetails>()
.ForMember(c => c.SharedPropertyOne, d => d.Ignore())
.ForMember(c => c.SharedPropertyTwo, d => d.Ignore());
I think you're close, but your question states:
I need to map this into a list of MyRealClass
... and your attempted mapping maps MyRealClass to UserDetails. It seems like you actually want a map from UserDetails to MyRealClass instead.
Anyway, here's one way to accomplish this:
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration.CreateMap<UserDetails, MyRealClass>();
autoMapperConfiguration.CreateMap<ShinyDTO, MyRealClass>();
var results = new List<MyRealClass>();
foreach(var record in dto.Users) {
var mapped = Mapper.Map<MyRealClass>(dto);
Mapper.Map(record, mapped);
results.Add(mapped);
}
Here, the second Mapper.Map call maps record onto mapped, and it should not overwrite the values that have already been mapped over by the mapping from ShinyDTO to MyRealClass.
You could also get fancy and do all of this in a ConstructUsing call, but this seems clearer to me.
You can create a map between UserDetails and IEnumerable<MyRealClass>.
var autoMapperConfiguration = new MapperConfigurationExpression();
autoMapperConfiguration
.CreateMap<IEnumerable<MyRealClass>, UserDetails>()
.ForMember(dest => dest.SharedPropertyOne, opt => opt.MapFrom(x => x.get(0).SharedPropertyOne)); //you can check if the list is empty
.ForMember(dest => dest.SharedPropertyTwo, opt => opt.MapFrom(x => x.get(0).SharedPropertyTwo)); //you can check if the list is empty
.AfterMap((src,dest) => //src is a list type
{
foreach(MyRealClass myrealclass in src)
dest.Users.add(new UserDetails(){
Title = myrealclass.Title,
Forename = myrealclass.Forename,
Surname = myrealclass.Surname
});
});

EF Eager fetching derived class

I´m using EF6 and trying to eager fetch the whole structure of an object. The problem is that i´m using inheritance.
Let´s say that i have this classes.
DbContext
DbSet<A> A { get; set; }
Example classes
public class A
{
public string Id { get; set; }
public IList<Base> Bases { get; set; }
}
public abstract class Base
{
public int Id { get; set; }
public string Name { get; set; }
}
public abstract class Base1 : Base
{
public SomeClass SomeClass { get; set; }
}
public class Base2 : Base1
{
}
public class Base3 : Base1
{
public SomeOtherClass SomeOtherClass { get; set; }
}
The error i get is:
The Include path expression must refer to a navigation property defined on the type.
Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Why doesn´t it work with the following ?
public IEnumerable<A> GetAll(string id)
{
return _ctx.A
.Include(x => x.Bases.OfType<Base1>().Select(y=>y.SomeClass))
.Where(x => x.Id.Equals(id)).ToList();
}
New example
public IEnumerable<A> GetAll(string id)
{
var lists = _dbContext.A.Where(x => x.Id == id);
lists.SelectMany(a => a.Bases).OfType<Base1>().Include(e=>e.SomeClass).Load();
lists.SelectMany(b => b.Bases).OfType<Base3>().Include(e => e.SomeOtherClass).Load();
return lists;
}
EDIT: Added a new example that seems to work.
Shortly, it's not possible out of the box.
The only workaround I can suggest is to materialize the master query result, then execute several OfType queries with the necessary Includes using the same filter as the master query, and rely on EF navigation property fixup.
It requires an inverse navigation property in the Base class:
public abstract class Base
{
// ...
public A A { get; set; }
}
Then you can use something like this:
public IEnumerable<A> GetAll(string id)
{
var a = _ctx.A.Where(x => x.Id == id).ToList();
_ctx.Base.OfType<Base1>().Include(e => e.SomeClass).Where(e => e.A.Id == id).Load();
_ctx.Base.OfType<Base3>().Include(e => e.SomeOtherClass).Where(e => e.A.Id == id).Load();
return a;
}
The same idea can be used w/o inverse navigation property but with using the returned base Ids as filter:
public IEnumerable<A> GetAll(string id)
{
var a = _ctx.A.Include(e => e.Bases)
.Where(x => x.Id == id).ToList();
var baseIds = a.SelectMany(e => e.Bases.OfType<ModelA.Base1>().Select(b => b.Id));
db.Base.OfType<Base1>().Include(e => e.SomeClass)
.Where(e => baseIds.Contains(e.Id)).Load();
baseIds = a.SelectMany(e => e.Bases.OfType<Base3>().Select(b => b.Id));
db.Base.OfType<Base3>().Include(e => e.SomeOtherClass)
.Where(e => baseIds.Contains(e.Id)).Load();
return a;
}
Your problem is not in the Select(y=>y.SomeClass) it self, if you try to remove it from your query and execute your query again, you will get same problem. You cannot query the inherited type as child and you expect from entity framework to take care for everything.
If you look to your database, the table Base has a reference to A which is relation 1-many from A to Base.
you can either get all the Base entities where A.Id = something, by adding a navigational property A in the class Base, and in your DbContext you add DbSet<Base> Bases{get;set;} then your query will look like this
var details = _ctx.Bases.OfType<Base1>()
.Include(t=>t.Box)
.Include(t=>t.SomeClass)
.Where(t=>t.Box.Id ==something);
Other option, to use a DTO, in the below sample I used Anonymous type, but you can create a strongly DTO typed to meet your requirements.
var details = _ctx.A
.Where (t=>t.Id ==something)
.Select(a => new {
Id = a.Id,
// ... other A properites ,
Bases = _ctx.Bases.OfType<Base1>().Select(m=> new {
Id = m.Id,
Name = m.Name,
SomeClass = m.SomeClass
});
}
Hope this will help you

Automapper projection and union

I have a problem with union and automapper projections.
I have two entities:
public class Entity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
}
public class ExtendedEntity
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
}
and projection:
public class EntityProjection
{
public DateTime ActionDate { get; set; }
public int SomeProp { get; set; }
public int SomeOtherProp { get; set; }
public string Source { get; set; }
}
i map entities to one projection, Entity does not have SomeOtherProp so i set 0 to it:
public class EntityProfile : Profile
{
protected override void Configure()
{
CreateMap<ExtendedEntity, EntityProjection>()
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "ext entity"));
CreateMap<Entity, EntityProjection>()
.ForMember(dest => dest.SomeOtherProp, opt => opt.MapFrom(src => 0))
.ForMember(dest => dest.Source, opt => opt.MapFrom(src => "entity"));
}
}
when i try to use next code i get error:
var entities = context.Set<Entity>()
.Project().To<EntityProjection>();
var extEntities = context.Set<ExtendedEntity>()
.Project().To<EntityProjection>();
var result = entities.Union(extEntities).OrderBy(p => p.ActionDate).ToList();
Error text: The type 'UserQuery+EntityProjection' appears in two structurally incompatible initializations within a single LINQ to Entities query. A type can be...
That means that properties in projection must be initialized in same order, how i can set projection properties initialization order by automapper?
Very late answer, and the short version seems to be "You can't".
I had exactly the same question (Can I force Automapper to initialise properties in a certain order?) and ended up mapping everything within a LINQ select statement.
For ease, I made it a static method within my DTO (cut-down code):
public static IQueryable<MyDto> QueryableFromTaskType1(
IQueryable<TaskType1> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.Asset.AssetType.Name,
AssetId = src.Asset.Id,
AssetCode = src.Asset.Code,
AssetName = src.Asset.Name,
});
}
public static IQueryable<MyDto> QueryableFromTaskType2(
IQueryable<TaskType2> query)
{
return query.Select(src => new MyDto()
{
TaskId = src.Id,
AssetTypeName = src.AssetTypeName,
AssetId = src.AssetId,
AssetCode = src.AssetCode,
AssetName = src.AssetName,
});
}
then you can get your objects, as an IQueryable, simply pass them through the appropriate static method (which appends a select into the DTO - or projects as it's otherwise known) and then Union or Concat the resulting IQueryables.
The only downside is that Automapper will normally deal with recursive automapping, although I'm pretty certain that wouldn't map to SQL well anyway, so you probably don't lose much.

Categories

Resources