Convert Dictionary to ICollection in Entity Framework Core - c#

Here I am stucked with the conversion of dictionary to Icollection in EF Core. I have Dictionary in FlatEmployee class in which I am storing list of key, value pair in database. I have declared like this:
public class FlatEmployee
{
public int EmployeeId { get; set; }
public Dictionary<string, long> PayAndAllowances { get; set; }
}
//====================Configuration
public void Configure(EntityTypeBuilder<FlatEmployee> builder)
{
builder.HasKey(f => new { f.EmployeeId });
builder.Property(sb => sb.PayAndAllowances)
.HasConversion(
pa => JsonConvert.SerializeObject(pa),
pa => JsonConvert.DeserializeObject<Dictionary<string, long>>(pa));
}
This works absolutely fine when I am seeding or inserting. But the problem I am facing when I am trying to get the FlatEmployee class. This is because I want to get the dictionary in Collection. For that purpose I have declared another class like this:
public class LastPayCertificateViewModel: IHaveCustomMapping
{
public int EmployeeCode { get; set; }
public EmployeeEarningDTO PayAndAllowances { get; set; }
public void CreateMappings(Profile configuration)
{
configuration.CreateMap<FlatEmployee, LastPayCertificateViewModel>()
.ForMember(dto => dto.EmployeeCode , opt => opt.MapFrom(entity => entity.EmployeeId ));
}
}
public class EmployeeEarningDTO : IHaveCustomMapping
{
public ICollection<BaseEmployeeDictionary> PayAndAllowances { get; set; }
public void CreateMappings(Profile configuration)
{
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>()
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity.PayAndAllowances));
}
}
public class BaseEmployeeDictionary
{
public string Name { get; set; }
public long Amount { get; set; }
}
When I am trying to use above classes for getting the data like this:
public class LastPayCertificateQuery : IRequest<LastPayCertificateViewModel>
{
public string EmployeeCode { get; set; }
}
public async Task<LastPayCertificateViewModel> Handle(LastPayCertificateQuery request, CancellationToken cancellationToken)
{
var predicate = PredicateBuilder.False<FlatEmployee>();
predicate = predicate.Or(emp => emp.EmployeeId == request.EmployeeCode);
var employee = await _context.FlatEmployee
.Where(predicate)
.FirstOrDefaultAsync(cancellationToken);
if (employee == null)
return null;
var empVM = _mapper.Map<LastPayCertificateViewModel>(employee);
}
Then I am getting null in PayAndAllowances in empVM. This is what my problem is. Where is my problem? I thought it was because that Dictionary has key value pair and which is not been able to convert to BaseEmployeeDictionary. I have tried this way as well to add List Item to PayAndAllowances in empVM
foreach (KeyValuePair<string, long> data in employee.DeductionsByAdjustment)
{
BaseEmployeeDictionary listItem = new BaseEmployeeDictionary
{
Name = data.Key,
Amount = data.Value
};
empVM.EarningDetails.PayAndAllowances.Add(listItem);
return empVM;
}
Which of course wont work because the empVM.EarningDetails.PayAndAllowances is null and throws NullReferenceException. My queries is how to map between the Dictionary to ICollection while Creating Map in EmployeeEarningDTO. OR It would be really appretiated for your valuable suggestion and solution please.

It turns out to be AutoMapper mapping issue.
First, EmployeeEarningDTO inside LastPayCertificateViewModel creates additional level compared to FlatEmployee:
LastPayCertificateViewModel.PayPayAndAllowances.PayAndAllowances
vs
FlatEmployee.PayAndAllowances
AutoMapper maps by default properties with the same name. So inside FlatEmployee to LastPayCertificateViewModel map it would try to map Dictionary<string, long> PayAndAllowances to EmployeeEarningDTO PayAndAllowances. But there is no mapping from Dictionary<string, long> to EmployeeEarningDTO. Instead, there is a mapping from FlatEmployee to EmployeeEarningDTO, so you have to tell AM to use it:
configuration.CreateMap<FlatEmployee, LastPayCertificateViewModel>()
.ForMember(dto => dto.EmployeeCode, opt => opt.MapFrom(entity => entity.EmployeeId))
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity)); // <--
Second, mapping from FlatEmployee to EmployeeEarningDTO - AM will automatically try to map PayAndAllowances properties, but there is no mapping from KeyValuePair<string, long> to BaseEmployeeDictionary. You could define such mapping
configuration.CreateMap<KeyValuePair<string, long>, BaseEmployeeDictionary>()
.ForMember(dst => dst.Name, opt => opt.MapFrom(src => src.Key))
.ForMember(dst => dst.Amount, opt => opt.MapFrom(src => src.Value));
which will allow you to use simply
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>();
however you probably won't do that because you don't want every KeyValuePair<string, long> to be mapped to BaseEmployeeDictionary, so you could do that mapping inplace:
configuration.CreateMap<FlatEmployee, EmployeeEarningDTO>()
.ForMember(dto => dto.PayAndAllowances, opt => opt.MapFrom(entity => entity.PayAndAllowances
.Select(src => new BaseEmployeeDictionary { Name = src.Key, Amount = src.Value })));

Related

AutoMapper mapping between enum and its integer values fails with ReverseMap

The application is built with DDD approach, with a separate set of persistence models. I called database object, or dbo:
public class ParentDbo
{
public int ParentId { get; set; }
public int TypeId { get; set; }
}
public class ChildDbo
{
public int ChildId { get; set; }
public ParentDbo Parent { get; set; }
public int RetryNumber { get; set; }
}
We have a simple model to look at: a parent and a child relationship. The RetryNumber presents the enum value in the database.
On retrieving data, it uses Dapper to first query the database, and use its splitOn feature to map data into them. This part is irrelevant but I will show it anyway for completeness:
const string sql = "SELECT * FROM XXX ....";
using (var cnt = _dbConnectionFactory.CreateConnection())
{
var childDbos = await cnt.QueryAsync<ChildDbo, ParentDbo, ChildDbo>(
sql: sql,
map: (childDbo, parentDbo) =>
{
childDbo.Parent = parentDbo;
return childDbo;
},
splitOn: "ParentId"
);
}
Dapper has limitation that it couldn't map data to private complex objects. That's mainly the reason why I have to have 2 sets of models. I would like to encapsulate the data and logic within domain models, with private setters and other techniques.
Here are my domain models:
public class Parent
{
public int Id { get; private set; }
public int TypeId { get; private set; }
public Parent(int parentId, int typeId)
{
// Validations
this.Id = parentId;
this.TypeId = typeId;
}
}
public class Child
{
public int Id { get; private set; }
public Parent Parent { get; private set; }
public Attempt Attempt { get; private set; }
public Child(int childId, Parent parent, Attempt attempt)
{
// Validations
this.Id = childId;
this.Parent = parent;
this.Attempt = attempt;
}
}
For domain models, I don't want public setters, and parameter-less constructors.
The Attempt is the enum with integer backing values:
public enum Attempt
{
Original = 1,
FirstRetry = 2,
SecondRetry = 3,
LastRetry = 4
}
Lastly, I want to use AutoMapper to map between Dbos and the domain models. Here is the mapping:
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap();
CreateMap<Parent, ParentDbo>()
.ForMember(dest => dest.ParentId, opts => opts.MapFrom(src => src.Id))
.ReverseMap();
}
}
I want to have two-ways mappings so I use ReverseMap().
.Net Fiddle demo: https://dotnetfiddle.net/saEHWd
It maps domain models to dbos without problem:
But its reverse, mapping from dbos to domain models, is throwing exceptions:
Unhandled exception. System.ArgumentException: Program+Child needs to have a constructor with 0 args or only optional args. (Parameter 'type')
at lambda_method18(Closure , Object , Child , ResolutionContext )
at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap)
at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination)
at AutoMapper.Mapper.Map[TDestination](Object source)
at Program.Main()
I've tried to remove the enum property and everything worked so I'm pretty sure it's the enum mapping that's having issues.
As far as I can see in your fiddle you are trying to map from ChildDbo to Parent and there is no mapping setup for it. Change the mapping code to:
var child2 = mapper.Map<Child>(childDbo);
And since there is mismatch in third Child's ctor param and source property names change map to:
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap()
.ConstructUsing((dbo, ctx) => new Child(dbo.ChildId, ctx.Mapper.Map<Parent>(dbo.Parent), (Attempt)dbo.RetryNumber));
See here
Or rename third Child's ctor parameter to retryNumber:
public Child(int childId, Parent parent, Attempt retryNumber)
see here.
or use ForCtorParam:
CreateMap<Child, ChildDbo>()
.ForMember(dest => dest.ChildId, opts => opts.MapFrom(src => src.Id))
.ForMember(dest => dest.RetryNumber, opts => opts.MapFrom(src => (int)src.Attempt))
.ReverseMap()
.ForCtorParam("attempt", opt => opt.MapFrom(dbo => dbo.RetryNumber))
Here.

AutoMapper, mapping one type in two different types

I have the following models:
public class Stuff
{
...
public IList<Place> Places { get; set; } = null!;
...
}
public class Place
{
...
public IList<Stuff> Stuffs { get; set; } = null!;
...
}
public class StuffEntity
{
...
public IList<PlaceStuffEntity> Places { get; set; } = null!;
...
}
public class PlaceEntity
{
...
public IList<PlaceStuffEntity> Stuffs { get; set; } = null!;
...
}
public class PlaceStuffEntity
{
public int StuffId { get; private set; }
public StuffEntity Stuff { get; set; } = null!;
public int PlaceId { get; private set; }
public PlaceEntity Place { get; set; } = null!;
}
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(d => d.Places,
opt => opt.MapFrom(s => s.Places.Select(y => y.Place).ToList()));
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
cfg.CreateMap<PlaceAndStuffEntity, Stuff>() // < -- Issue
.IncludeMembers(entity=> entity.Stuff);
cfg.CreateMap<PlaceAndStuffEntity, Place>() // < -- Issue
.IncludeMembers(entity=> entity.Place);
by some reason when I add both last lines, conversion does not work ...
But if I add only one line for example for converting PlaceAndStuffEntity -> Stuff works only one conversion from PlaceEntity -> Place
var place = mapper.Map<Place>(placeEntity); // <- This works
var stuff = mapper.Map<Stuff>(stuffEntity); // <- Does not work !!
Is there a way properly handle the following conversions ?
It sounds like you want to map through the joining table (PlaceAndStuff) to get to the other entity type. For instance in your Place to get a list of Stuff, and Stuff to get a list of Place, you want to direct Automapper how to navigate through the joining table.
For instance:
cfg.CreateMap<StuffEntity, Stuff>()
.ForMember(x => x.Places, opt => opt.MapFrom(src => src.PlaceEntity));
// Where StuffEntity.Places = PlaceAndStuffEntities, to map Stuff.Places use PlaceAndStuffs.PlaceEntity
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(x => x.Stuffs, opt => opt.MapFrom(src => src.StuffEntity));
So rather than trying to tell EF how to map the joining entity PlaceStuffEntity, we focus on the PlaceEntity and StuffEntity, and tell Automapper to navigate through the joining entity to get at the actual Stuff and Place relatives via the joining entity.
Change
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Places.Select(y => y.Stuff).ToList()));
to
cfg.CreateMap<PlaceEntity, Place>()
.ForMember(d => d.Stuffs,
opt => opt.MapFrom(s => s.Stuffs.Select(y => y.Stuff).ToList()));
Source type PlaceEntity does not have a property named Places, only Stuffs.

Mapping from dynamic to type with property defined with interface type

The context around this is that I'd like to map a dynamic dapper result to an object and that object has a property which has an interface type:
public class TargetModel
{
public int Id { get; set; }
public IAddress AbstractAddress { get; set; }
}
The dynamic to object bit is straight forward but I'm unable to configure AutoMapper to tell it how to handle the interface.
dynamic sourceModel = new ExpandoObject();
// flat model - id should map to TargetModel, Address01 will map to a nested type Address on TargetModel
sourceModel.Id = 1;
sourceModel.Address01 = "address01";
// for debugging purposes address maps ok from dynamic
Address address = Mapper.Map<Address>(sourceModel);
// this maps, but AbstractAddress is null - I need to config AutoMapper to understand how to map IAddress to Address
TargetModel target = Mapper.Map<TargetModel>(sourceModel);
I've tried to tell it how to deal with the property:
CreateMap<ExpandoObject, TargetModel>()
.ForMember(y => y.AbstractAddress, opts => opts.MapFrom(f => f));
Which fails with:
System.ArgumentException: Cannot create an instance of interface type
So I attempt to hint at the concrete:
CreateMap<IAddress, Address>().As<Address>();
Which doesn't resolve the issue and the exception remains.
I've looked at the following questions/concepts and tried various configuration options but haven't been able to get the map working:
AutoMapper: Mapping objects with interface properties
https://dotnetfiddle.net/fPQxWx
This is trying to map from a string in the ExpandoObject to an IAddress in your target type. Obviously it can't create an instance of IAddress to fill so you have to fulfill that yourself in the mapping.
If your models look like this:
public class TargetModel
{
public int Id { get; set; }
public IAddress AbstractAddress { get; set; }
}
public interface IAddress
{
string Address01 { get; set; }
}
public class Address : IAddress
{
public string Address01 { get; set; }
}
Then your config and setup can look like this:
public void MappingTests()
{
dynamic sourceModel = new ExpandoObject();
// flat model - id should map to TargetModel, Address01 will map to a nested type Address on TargetModel
sourceModel.Id = 1;
sourceModel.Address01 = "address01";
Mapper.Initialize(cfg =>
{
cfg.CreateMap<ExpandoObject, TargetModel>()
.ForMember(dest => dest.AbstractAddress, opt => opt.MapFrom(src => new Address() { Address01 = src.First(kvp => kvp.Key == "Address01").Value.ToString() }))
.ForMember(destinationMember => destinationMember.Id, opt => opt.MapFrom(src => src.First(kvp => kvp.Key == "Id").Value));
});
TargetModel target = Mapper.Map<TargetModel>(sourceModel);
}
I just used it as an IEnumerable<KeyValuePair<string, object>>, but you can also treat the expando object like a dictionary by casting.
cfg.CreateMap<ExpandoObject, TargetModel>()
.ForMember(dest => dest.AbstractAddress, opt => opt.MapFrom(src => new Address() { Address01 = ((IDictionary<string, object>)src)["Address01"].ToString() }))
.ForMember(destinationMember => destinationMember.Id, opt => opt.MapFrom(src => ((IDictionary<string, object>)src)["Id"]));

resolving complex entity model to flat model view using AutoMapper

I want to create a map for a somewhat complex entity model to a flattened view Model
My entity model is like so
cbItems
has many cbItemsContent
has many cbRegulators
so my viewmodels are like so
for cbItems:
public class ItemViewModel
{
public ItemViewModel()
{
this.CbItemsContents = new HashSet<ItemContentViewModel>();
}
public int ItemID { get; set; }
......
public virtual ICollection<ItemContentViewModel> CbItemsContents { get; set; }
}
}
for cbItemsContent:
public class ItemContentViewModel
{
public int ItemContentID { get; set; }
public int ItemID { get; set; }
....
public ItemContentRegulatorsViewModel RegulatedBy { get; set; }
}
}
for cbRegulators:
public class ItemContentRegulatorsViewModel
{
public int ItemContentId { get; set; }
public IEnumerable<int> RegulatorIds { get; set; }
}
}
I had hoped it would be as easy as this:
config.CreateMap<CbItem, ItemViewModel>();
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy.ItemContentId,
m => m.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatedBy.RegulatorIds,
n => n.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));
from teh following query:
ItemViewModel item =
_context.cbItems.Where(u => u.ItemId = id)
.ProjectTo<ItemViewModel>()
.first();
But this results in an error:
Expression 'dest => dest.RegulatedBy.ItemContentId' must resolve to
top-level member and not any child object's properties. Use a custom
resolver on the child type or the AfterMap option instead. Parameter
name: lambdaExpression
HOw can I achieve my desired model layout?
You have to map ItemContentRegulatorsViewModel, then you don't need to set it from the ViewModel above.
#Rabban probably means something like this:
config.CreateMap<CbItemsContent, ItemContentViewModel>()
.ForMember(dest => dest.RegulatedBy, o => o.MapFrom(src => src));
config.CreateMap<CbItemsContent, ItemContentRegulatorsViewModel>()
.ForMember(dest => dest.ItemContentId, o => o.MapFrom(src => src.GenericID))
.ForMember(dest => dest.RegulatorIds, o => o.MapFrom(src => src.cbItemsContentRegulators.Select(q => q.cbRegulator.RegulatorId)));

AutoMapper: Mapping Tuple to Tuple

I'm using AutoMapper in my ASP.NET MVC4 project. I have a problem when mapping 2 class Question and QuestionViewModel. Here my two model classes:
public class Question
{
public int Id { get; set; }
public string Content { get; set; }
public Tuple<int, int> GetVoteTuple()
{
"some code here"
}
}
public class QuestionViewModel
{
public int Id { get; set; }
public string Content { get; set; }
public Tuple<int, int> VoteTuple { get; set; }
}
Here is my controller code :
public class QuestionController: Controller
{
public ActionResult Index(int id)
{
Question question = Dal.getQuestion(id);
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.MapFrom(
s => s.GetVoteTuple()
));
QuestionViewModel questionViewModel =
Mapper.Map<Question, QuestionViewModel>(question);
return View(questionViewModel);
}
}
When I run this code the VoteTuple property in QuestionViewModel has null value. How can I map 2 class with Tuple property ?
Thanks.
Mapping from Tuple to Tuple is not possible by default through Automapper, because Tuple doesn't have setter properties (they can only be initialized through the constructor).
You have 2 options:
1) Create a custom resolver for Automapper and then use the .ResolveUsing method in the mapping config: .ForMember(p => p.VoteTuple, m => m.ResolveUsing<CustomTupleResolver>())
2) Map to a properties / a class instead, like this:
public class QuestionViewModel
{
public int Id { get; set; }
public string Content { get; set; }
public int VoteItem1 { get; set; }
public int VoteItem2 { get; set; }
}
And then:
.ForMember(p => p.VoteItem1, m => m.MapFrom(g => g.Item1))
.ForMember(p => p.VoteItem2, m => m.MapFrom(g => g.Item2))
You don't really need to use Tuple in your view model, so I'd recommend the 2nd option.
Edit:
I see that you've updated your code so that GetVoteTuple() is a function, not a property. In that case, you could easily adapt the code like this:
.ForMember(p => p.VoteItem1, m => m.MapFrom(g => g.GetVoteTuple().Item1))
.ForMember(p => p.VoteItem2, m => m.MapFrom(g => g.GetVoteTuple().Item2))
Your CreateMap call is incorrect:
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.MapFrom(
s => s.GetVoteTuple()
//-----------^
));
Try using ResolveUsing instead of MapFrom (and use the generic s argument in your lambda instead of the local variable reference:
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,
m => m.ResolveUsing(
s => s.GetVoteTuple()
));
MapFrom is used to map properties directly. Since you're wanting to "map" from the result of a function call, ResolveFrom is more appropriate.
Also, you should only call CreateMap once in your application, typically that's done in Application_Start in global.asax
try this :
Mapper.CreateMap<Question, QuestionViewModel>()
.ForMember(p => p.VoteTuple,op=>op.MapFrom(v=>new Tuple<int,int>(v.GetVoteTuple.Item1,v.GetVoteTuple.Item2)));

Categories