Automapper & EF Core: One-to-many relationship exposed by DTO - automapper

I effectively have the following entities defined:
public class Order
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Part
{
public int Id { get; set; }
public string Description { get; set; }
public int? OrderId { get; set; }
public Order Order { get; set; }
}
They are configured so that the OrderId in Part is linked to the Order Id:
// Order Model Builder
var orderModelBuilder = modelBuilder.Entity<Order>()
.ToTable("Orders");
orderModelBuilder.HasKey(i => i.Id);
orderModelBuilder.HasMany<Part>()
.WithOne()
.HasForeignKey(i => i.OrderId);
// Part Model Builder
var partModelBuilder = modelBuilder.Entity<Part>()
.ToTable("Parts");
partModelBuilder.HasKey(i => i.Id);
partModelBuilder.HasOne(i => i.Order)
.WithMany()
.HasForeignKey(i => i.OrderId);
Now I would like to map these to a detailed order DTO which includes a collection of parts for an order:
public class PartDto
{
public int Id { get; set; }
public string Description { get; set; }
}
public class OrderDetailsDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<PartDto> Parts { get; set; }
}
Currently, I use Automapper's ProjectTo() to handle queries so I can query what I want and get a list of orders back already mapped to my desired DTO. Now, I want to add Parts to that query so I can get all of the orders back with parts in one query without having to loop through orders and individually fetch the parts for each order returned. I could easily do that if List was part of the Order entity class but adding it at this stage isn't really an option.
CreateMap<Part, PartDto>()
.ForMember(d => d.Id, a => a.MapFrom(s => s.Id))
.ForMember(d => d.Description, a => a.MapFrom(s => s.Description))
;
CreateMap<Order, OrderDetailsDto>()
.ForMember(d => d.Id, a => a.MapFrom(s => s.Id))
.ForMember(d => d.Name, a => a.MapFrom(s => s.Name))
.ForMember(d => d.Parts, a => a.MapFrom(s => ?????))
;
.
..
...
await dbContext.Set<Order>().Where(...).ProjectTo<OrderDetailsDto>(configurationProvider).ToListAsync()
I know I can get shadow variables using EF.Property(object, name) but not sure how to load a restricted collection (dbContext.Set.Where(i => i.OrderId == orderId)). Any suggestions would be greatly appreciated!
--
I'm also open to adding another Entity class if there is a way to make it exist next to the existing entity. So far, I can't find a way to do that without EF Core complaining that it can only have one entity per class (without a discriminator column which would defeat the purpose!).

Related

Automapper 8 mapping not working properly

I have two model classes, when I try to map different properties of different name by using Automapper ForMember method. It throws an automapper configuration validation exception on the mapping of different property.
I have tried a lot but It does not help.I do not know why It is throwing an exception when I try to map Quantity property with Quntity property. but when I put same name of the property in both the model classes then it works
Below is located all the model classes, exception and configurations regarding automapper.
Could you please help me, that how to solve problem?
public class ProductModel
{
public ProductModel()
{
Id = GuidContext.Current.NewGuid();
ProductHistory = new HashSet<ProductHistoryModel>();
}
public Guid Id { get; set; }
public string ProductCode { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
public decimal? Price { get; set; }
public int? Quntity { get; set; }
public Guid ProductCategoryId { get; set; }
public Guid? BrandId { get; set; }
public Guid ProductAttributeId { get; set; }
public virtual BrandModel Brand { get; set; }
public virtual ProductCategoryModel ProductCategory { get; set; }
public virtual ProductAttributeModel ProductAttribute { get; set; }
public virtual ICollection<ProductHistoryModel> ProductHistory { get; set; }
}
The another class is
public class ProductModel
{
public string Name { set; get; }
//public List<string> Attributes { set; get; }
//public string Brand { get; set; }
public decimal? Price
{
get; set;
}
public int? Quantity { get; set; }
}
}
and the mapping configuration is
public class ProductModelMapConfigurator : Profile, IMapConfigurator
{
public void Configure()
{
Mapper.Initialize(cfg =>
{
CreateMap<StandardizeInventory.Models.Product.ProductModel, Models.ProductModel>()
//.ForMember(dest => dest.Brand, opt => opt.MapFrom(src => src.Brand.Name))
.ForMember(dest => dest.Name, opt => opt.MapFrom(src => src.Name))
.ForMember(dest => dest.Price, opt => opt.MapFrom(src => src.Price))
.ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quntity));
//.AfterMap((src, dest) => {
// dest.Attributes = src.ProductAttribute.ProductAttributeValue.Select(x => x.Value).ToList();
//});
CreateMap<Models.ProductModel, StandardizeInventory.Models.Product.ProductModel>();
});
}
}
Below is the Exception Details
AutoMapper.AutoMapperConfigurationException:
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
==========================================================================================
AutoMapper created this type map for you, but your types cannot be mapped using the current configuration.
ProductModel -> ProductModel (Destination member list)
StandardizeInventory.Models.Product.ProductModel -> InventoryStoreApi.Models.ProductModel (Destination member list)
Unmapped properties:
Quantity
at AutoMapper.ConfigurationValidator.AssertConfigurationIsValid(IEnumerable`1 typeMaps) in
Any help would be appreciated. Thanks
You're using a Profile wrong, see the documentation on Profiles
Your profile should look like:
public class ProductModelMapConfigurator : Profile, IMapConfigurator
{
public ProductModelMapConfigurator()
{
CreateMap<StandardizeInventory.Models.Product.ProductModel, Models.ProductModel>()
//.ForMember(dest => dest.Brand, opt => opt.MapFrom(src => src.Brand.Name))
.ForMember(dest => dest.Quantity, opt => opt.MapFrom(src => src.Quntity));
//.AfterMap((src, dest) => {
// dest.Attributes = src.ProductAttribute.ProductAttributeValue.Select(x => x.Value).ToList();
//});
CreateMap<Models.ProductModel, StandardizeInventory.Models.Product.ProductModel>();
}
}
Get rid of that Mapper.Initialize call from inside your Profile, and change the profile to use a constructor, not whatever that Configure method is. You also don't need MapFrom when the names match, that's the "Auto" of "AutoMapper".

AutoMapper automatically map prefixed properties

I want AutoMapper to map automatically Members like this:
class Model {
public int MDId { get; set; }
public int MDName { get; set; }
}
class ModelDto {
public int Id { get; set; }
public int Name { get; set; }
}
Here, I would do a
CreateMap<Model, ModelDTO>()
.ForMember(x => x.Id, e => e.MapFrom(x => x.MDId )
.ForMember(x => x.Id, e => e.MapFrom(x => x.MDName )
but,Most of my classes are like that,My Model have prefix in Database,
DTO haven't;And classes prefix not only 'MD', all different in my project
how could I make AutoMapper do the mapping automatically? thanks!

Flatten nested lists Automapper

Trying to map the following
public class WorkPreferance
{
public int ID { get; set; }
public string Name { get; set; }
public IEnumerable<Location> PreferedLocations { get; set; }
}
public class Location
{
public int LocationID { get; set; }
public string LocationName { get; set; }
}
to the following destination ...
public class WorkingPreferenceViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public int LocationID { get; set; }
public string LocationName { get; set; }
}
but I cant wrap my head around what should be the mapping deceleration ..
so far i have this:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Location, WorkingPreferenceViewModel>()
.ForMember(d => d.LocationID, o => o.MapFrom(s => s.LocationID))
.ForMember(d => d.LocationName, o => o.MapFrom(s => s.Description))
.ForAllOtherMembers(o => o.Ignore());
cfg.CreateMap<WorkPreferance, WorkingPreferenceViewModel>()
.ForMember(d => d.LocationID, o => o.MapFrom(s => s.PreferedLocations))
.ForMember(d => d.LocationName, o => o.MapFrom(s => s.PreferedLocations))
}
I'm afraid AutoMapper cannot do this. It can map single objects to single objects, and a collection of objects to a collection of objects.
What you want to do is map a single object to a collection of another object. I can't think of how you would use AutoMapper to achieve that.
But, I must say, I don't really understand why you have defined your viewmodel that way. Why would you repeat the Id and Name so many times? In what way does your proposed structure become more easy to process?
What I would do is create a LocationViewModel, and have WorkingPreferenceViewModel contain an IEnumerable<LocationViewModel>. That way you can map it easily with AutoMapper by creating maps between the objects and their respective view models. It will neatly take care of the fact that there is a collection in there.

NHibernate group by parentId in child and get the value from parent table for this parentId

I have one to many between Header and Detail as shown in the below class
public class Header
{
public virtual string ScriptNumber { get; set; }
public virtual IList<Detail> Details { get; set; }
public virtual string StartTime { get; set; }
}
public class Detail
{
public virtual string ScriptNumber { get; set; }
public virtual DateTime UpdatedDate { get; set; }
public virtual Header ProgramHeader { get; set; }
}
I am trying to query the Detail to group by ScriptNumber and get the corresponding StartTime from the Header table. Can anyone help me how to do this.
Currently my NHibernate query is:
var changesInDetail = _session.QueryOver<Detail>()
.Where(x => x.UpdatedDate.IsBetween(changedFrom).And(changedTo))
.SelectList(list => list.SelectGroup(pr => pr.ScriptNumber))
.Future<string>();
How to get the StartTime from Header for the grouped by value in Detail?
The following works as I expected. Can anyone please confirm if this is the best approach?
var changedScriptsInDetail1 = _session.QueryOver<Detail>(() => detailAlias)
.JoinQueryOver(d => d.ProgramHeader, () => headerAlias)
.Where(() => detailAlias.UpdatedDate.IsBetween(changedFrom).And(changedTo))
.SelectList(list => list
.SelectGroup(pr => pr.ScriptNumber)
.SelectGroup(()=>headerAlias.StartTime)
)
.List<object[]>();
var changedScriptsInDetail1 = _session.QueryOver<Detail>(() => detailAlias)
.JoinQueryOver(d => d.ProgramHeader, () => headerAlias)
.Where(() => detailAlias.UpdatedDate.IsBetween(changedFrom).And(changedTo))
.SelectList(list => list
.SelectGroup(pr => pr.ScriptNumber)
.SelectGroup(()=>headerAlias.StartTime)
)
.List<object[]>();

Automapper newbie question regarding list property

As a new fan of AutoMapper, how would I use it to do the following:
Given the following classes, I want to create FlattenedGroup from Group where the list of item string maps to the title property of Item.
public class Group
{
public string Category { get; set; }
public IEnumerable<Item> Items { get; set; }
}
public class Item
{
public int ID { get; set; }
public string Title { get; set; }
}
public class FlattenedGroup
{
public string Category { get; set; }
public IEnumerable<string> Items { get; set; }
}
Thanks
Joseph
The other thing you can do is create a converter from Item -> string:
Mapper.CreateMap<Item, string>().ConvertUsing(item => item.Title);
Now you don't need to do anything special in your Group -> FlattenedGroup map:
Mapper.CreateMap<Group, FlattenedGroup>();
That's all you'd need there.
Give this a try, you can probably use Linq and a lambda expression to map the list of strings in FlattenedGroup with the titles in Group.
Mapper.CreateMap<Group, FlattenedGroup>()
.ForMember(f => f.Category, opt => opt.MapFrom(g => g.Category))
.ForMember(f => f.Items, opt => opt.MapFrom(g => g.Items.Select(d => d.Title).ToList()));
Make sure you add System.Linq to your using statements

Resources