I have a behavioral class TaskBehavior, that has a one-argument constructor and that drives the visibility of properties in a form and at the same time the properties, that should be mapped to database entity.
Secondly, I have a DTO class TaskDto, and lastly a database entity Task:
public class TaskDto
{
public string Name { get; set; }
public string Description { get; set; }
}
public class Task
{
public string Name { get; set; }
public string Description { get; set; }
}
public class TaskBehavior
{
public TaskBehavior(bool secure)
{
NameVisible = true;
DescriptionVisible = secure;
}
public bool NameVisible { get; private set; }
public bool DescriptionVisible { get; private set; }
}
I need to map properties from TaskDto to Task but only those, that are decided by TaskBehavior. The purpose is to have only one parametrized ruleset used to direct visibility and conditional mapping:
void SaveData(TaskDto taskDto)
{
var behavior = new TaskBehavior(false);
var entity = Mapper.Map<Task>(taskDto); // TODO: only map properties based on "behavior" definition
}
The result should be that only Name property is mapped in this case from taskDto to entity (according behavior instance).
Finally I made it working myself. The idea is to use ad-hoc created MappingEngine instance and dynamically created mappings based on SOME parameter. The whole magic is in the class TaskMappingEngineFactory, which furthermore caches the created MappingEngine-s to a static collection to boost the performance.
public static class TaskMappingEngineFactory
{
private static readonly object _lock = new object();
private static Dictionary<Enums.PortalEnum, MappingEngine> _mappingEngines = new Dictionary<Enums.PortalEnum, MappingEngine>();
public static MappingEngine GetMappingEngine(TaskBehavior taskBehavior)
{
lock (_lock)
{
MappingEngine mappingEngine;
if (!_mappingEngines.TryGetValue(taskBehavior.PortalType, out mappingEngine))
{
ConfigurationStore store = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
store.CreateMap<TaskInfoDetail, TaskInfoDetail>()
.ForMember(dst => dst.CanExecuteAutomatically, o => o.Condition((Func<TaskInfoDetail,bool>)(src => taskBehavior.AdvancedPropertiesVisible)))
.ForMember(dst => dst.CanExecuteManually, o => o.Condition((Func<TaskInfoDetail,bool>)(src => taskBehavior.AdvancedPropertiesVisible)));
MappingEngine engine = new MappingEngine(store);
_mappingEngines.Add(taskBehavior.PortalType, mappingEngine = engine);
}
return mappingEngine;
}
}
}
In an "updating" method, I simply add a line creating an engine instance via
the factory. Quite simple, quite straightforward, not so sofisticated, but enough for now;)
var behavior = new TaskBehavior(NopContext.Current.PortalType());
var engine = TaskMappingEngineFactory.GetMappingEngine(behavior);
engine.Map(task, updatedTask);
Related
I have MyWizardDto which I use to transfer Dto objects between different layers. The Dto objects can be any number of objects. MyWizardDto is used in MyWizardAppService to insert records for each entity into the database. In MyWizardAppService I would know in advance the super set of repositories that may be required, currently I have added two for demonstration purposes but these can increase, and if any Dto object is null then we don't need to map or call its repository method for interacting with the database.
In MyWizardAppService.CreateOrUpdate method, I can hardwire multiple if/else statements to first map each Dto (ClientDto, ContactDto) to its corresponding entity (Client, Contact) using Automapper and then call the corresponding repository (_clientRepository, _contactRepository) to add/update record into the database. However, I am wondering if I can use reflection or some other method to avoid multiple if/else statements as number of entities can increase. I will always know the superset in advance so all relevant repositories will be injected in the same way as the current two repositories.
Is it possible to change my MyWizardAppService.CreateOrUpdate method to accomplish my objective without using multiple if/else statements? I am following the same naming convention e.g ClientDto, Client and _clientRepository so I don't know if that can be of help for using as rule of thumb.
public class MyWizardDto
{
public ClientDto client { get; set; }
public ContactDto contact { get; set; }
}
public class ClientDto
{
public string Forename { get; set; }
public string Surname { get; set; }
public DateTime? DateOfBirth { get; set; }
}
public class ContactDto
{
public string Street { get; set; }
public string City{ get; set; }
}
public class Client
{
public string Forename { get; set; }
public string Surname { get; set; }
public DateTime? DateOfBirth { get; set; }
}
public class Contact
{
public string Street { get; set; }
public string City{ get; set; }
}
public interface IMyWizardAppService
{
Task<ResponseOutputDto> CreateOrUpdate(MyWizardDto myWizardDto);
}
public class ResponseOutputDto
{
public bool IsSuccess { get; set; }
public string Message { get; set; }
}
public class MyWizardAppService
{
private readonly IRepository<Client> _clientRepository;
private readonly IRepository<Contact> _contactRepository;
// there can be more repository entities
private ResponseOutputDto responseOutputDto;
public MyWizardAppService(
IRepository<Client> clientRepository,
IRepository<Contact> contactRepository)
{
_clientRepository = clientRepository;
_contactRepository = contactRepository;
responseOutputDto = new ResponseOutputDto();
}
public async Task<ResponseOutputDto> CreateOrUpdate(MyWizardDto myWizardDto)
{
PropertyInfo[] properties = myWizardDto.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
string temp = property.Name;
// to check if property is dto object and is not null
// if not null then map dto to its corresponding object e.g ClientDto to Client,
//I am using AutoMapper e.g ObjectMapper.Map<Client>(myWizardDto.client);
// now call corresponding entities repository e.g if ClientDto then call
// _clientRepository's insert.
}
//if no error return success
responseOutputDto.IsSuccess = true;
return responseOutputDto;
}
}
As suggested by #Unknown I changed the MyWizardAppService as shown below but now I get compile time error: Client / IRepository is a type which is not valid in the given context
public class MyWizardAppService
{
private readonly IRepository<Client> _clientRepository;
private readonly IRepository<Contact> _contactRepository;
private Dictionary<Type, IRepository> _repositoryPairs;
// there can be more repository entities
private ResponseOutputDto responseOutputDto;
public MyWizardAppService(
IRepository<Client> clientRepository,
IRepository<Contact> contactRepository)
{
_clientRepository = clientRepository;
_contactRepository = contactRepository;
responseOutputDto = new ResponseOutputDto();
_repositoryPairs = new Dictionary<Type, IRepository>();
//GET ERROR ON FOLLOWING LINE
_repositoryPairs.Add(<Client>, IRepository<Client>);
}
public async Task<ResponseOutputDto> CreateOrUpdate(MyWizardDto myWizardDto)
{
PropertyInfo[] properties = myWizardDto.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
string temp = property.Name;
// to check if property is dto object and is not null
// if not null then map dto to its corresponding object e.g ClientDto to Client,
//I am using AutoMapper e.g ObjectMapper.Map<Client>(myWizardDto.client);
// now call corresponding entities repository e.g if ClientDto then call
// _clientRepository's insert.
}
//if no error return success
responseOutputDto.IsSuccess = true;
return responseOutputDto;
}
}
Update 2 I now get error on TryGet it says Disctionary does not contain a definition for TryGet.
public class MyWizardAppService
{
private readonly IRepository<Client> _clientRepository;
private readonly IRepository<Contact> _contactRepository;
private Dictionary<Type, IRepository> _repositoryPairs;
// there can be more repository entities
private ResponseOutputDto responseOutputDto;
public MyWizardAppService(
IRepository<Client> clientRepository,
IRepository<Contact> contactRepository)
{
_clientRepository = clientRepository;
_contactRepository = contactRepository;
responseOutputDto = new ResponseOutputDto();
_repositoryPairs = new Dictionary<Type, IRepository>();
_repositoryPairs.Add(typeOf(Client), _clientRepository);
}
public async Task<ResponseOutputDto> CreateOrUpdate(MyWizardDto myWizardDto)
{
PropertyInfo[] properties = myWizardDto.GetType().GetProperties();
foreach (PropertyInfo property in properties)
{
string temp = property.Name;
IRepository repo;
if (_repositoryPairs.TryGet(property.GetType(), out repo))
{
//call insert here some how
}
// to check if property is dto object and is not null
// if not null then map dto to its corresponding object e.g ClientDto to Client,
//I am using AutoMapper e.g ObjectMapper.Map<Client>(myWizardDto.client);
// now call corresponding entities repository e.g if ClientDto then call
// _clientRepository's insert.
}
//if no error return success
responseOutputDto.IsSuccess = true;
return responseOutputDto;
}
}
Am not sure if this answers your question. If not, just take it as a hint.
For removing multiple if-else statements for Mapping Dto to Corresponding Model, you can refer this post.
For removing multiple if-else statements for calling appropriate repository,
simply have a Dictionary<Type,Repository> and pre-initialize it with all the repositories like this.
//Just a sample code, not actual one.
repoDictionary.Add(<ClientModelType>,<ClientRepositoryInstance>);
repoDictionary.Add(<ContactModelType>,<ContactRepositoryInstance>);
Then instead of using conditional statements, you can simply query the repository dictionary to perform DB operations like this.
//Just a sample code, not actual one.
Repository repo;
if(repoDictionary.TryGetValue(model.GetType(), out repo)){
repo.CallRequiredMethod(model);
}
Hope this might help you in some way.
I have two classes, a ViewModel and a Dto, that are basically identical except that the Dto has a field 'readonly long? Phone;' while the ViewModel has the a property 'string Phone { get; set; }'.
The only way I've found to get AutoMapper to work is by changing the ViewModel Property to a backing property:
public long? Phone { get; set; }
public string PhoneNumberString
{
get
{
var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
return srv.GetFormattedPhoneNumber(Phone);
}
set
{
var srv = DependencyResolver.Current.GetService<IPhoneNumberService>();
Phone = srv.GetLongPhoneNumber(value);
}
}
And then in AutoMapper, have a gigantic line to call the constructor:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<MyViewModel, MyDto>()
.ConstructUsing(src => new MyDto(
src.Phone
/* ...Some ~30 other parameters here... */))
.ReverseMap();
});
...There must be a better way to do this? I've tried these:
.ForSourceMember(x => x.PhoneNumberString, opt => opt.DoNotValidate())
and
.ForMember(x => x.PhoneNumberString, opt => opt.Ignore())
and
.ForMember(viewModel => viewModel.Phone, options => options.MapFrom<PhoneNumberResolver>());//PhoneNumberResolver implements IValueResolver<ProspectMaintenanceViewModel, ProspectMaintenanceDto, long?>
Which all give 'Core.DTO.MyDto needs to have a constructor with 0 args or only optional args.' when trying to map, and:
.ForMember(dest => dest.Phone, opt => opt.MapFrom(src => 5))
Which gives 'System.ArgumentException: 'Expression must be writeable' when trying to configure AutoMapper.
Is there some way I can make AutoMapper understand that it can entirely ignore PhoneNumberString (or, even better, some way by which I can make it map long? to string so I don't need the backing property) without having to use the dto's constructor?
Is there any special reason that requires your DTO to not have a default constructor?
I have all my fields as readonly so that I can include a constructor that modifies (e.g. 'Description = description?.Trim();') and validates (e.g. 'if (Phone.HasValue && Phone.ToString().Length != 10) throw ...') the parameters. This way I can ensure that the Dto, being a value object, is always in a valid state.
1) Mapping to readonly field
So you have a Dto class:
public class Dto
{
public readonly long? PhoneNumber;
}
And then you are trying to force AutoMapper to do this:
var dto = new Dto();
dto.PhoneNumber = 123; // <== ERROR! A readonly field cannot be assigned to.
AutoMapper cannot write to readonly fields or properties. In matter of fact you neither. Either turn your field into a property with protected or private setter:
public class Dto
{
public long? PhoneNumber { get; private set; }
}
or make it a regular field by removing the readonly keyword:
public class Dto
{
public long? PhoneNumber;
}
2) Custom value resolving
ASP.NET MVC
Use a ValueResolver:
public class StringPhoneNumberResolver : IValueResolver<Dto, ViewModel, string>
{
private readonly IPhoneNumberService _phoneNumberService;
public StringPhoneNumberResolver()
{
_phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
}
public string Resolve(Dto source, ViewModel destination, string destMember, ResolutionContext context)
{
return _phoneNumberService.GetFormattedPhoneNumber(source.PhoneNumber);
}
}
You should know that generally it is an anti-pattern to have service injection in a DTO or IValueResolver. AutoMapper should be dumb and all kind of injections and so on should be handled elsewhere. That being said, here is the AutoMapper configuration:
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Dto, ViewModel>()
.ForMember(viewModel => viewModel.PhoneNumber, options =>
options.MapFrom<StringPhoneNumberResolver>());
});
If you want to reverse the process of long ==> string to string ==> long simply add another value resolver:
public class LongPhoneNumberResolver : IValueResolver<ViewModel, Dto, long?>
{
private readonly IPhoneNumberService _phoneNumberService;
public LongPhoneNumberResolver()
{
_phoneNumberService = DependencyResolver.Current.GetService<IPhoneNumberService>();
}
public long? Resolve(ViewModel source, Dto destination, long? destMember, ResolutionContext context)
{
return _phoneNumberService.GetLongPhoneNumber(source.PhoneNumber);
}
}
.NET Core
If you would operate in .NET Core environment, which fully supports IServiceCollection integration, you should add this AutoMapper configuration:
serviceCollection.AddAutoMapper(config =>
{
config.CreateMap<Dto, ViewModel>()
.ForMember(viewModel => viewModel.PhoneNumber, options =>
options.MapFrom<StringPhoneNumberResolver>());
}, typeof(Startup));
and then have IPhoneNumberServce automagically injected into value resolver:
public StringPhoneNumberResolver(IPhoneNumberService phoneNumberService)
{
_phoneNumberService = phoneNumberService;
}
For dependency injection I used automapper.extensions.microsoft.dependencyinjection package.
Well, I found the problem. It has absolutely nothing to do with what I thought it did. Mapping map long? to string works out of the box.
The problem I had was with an entirely different property.
I had the following structure:
public class MyDto
{
public readonly AddressDto BillingAddress;
public readonly AddressDto ShippingAddress;
public readonly long? Phone;
...
}
public class AddressDto
{
public readonly string Country;
public readonly string SubnationalEntity;
...
}
public class MyViewModel
{
public string BillingAddressCountry { get; set; }
public string BillingAddressSubnationalEntity { get; set; }
public string ShippingAddressCountry { get; set; }
public string ShippingAddressSubnationalEntity { get; set; }
public string Phone { get; set; }
...
}
It worked once I changed it to the following:
public class MyDto
{
public readonly AddressDto BillingAddress;
public readonly AddressDto ShippingAddress;
public readonly long? Phone;
...
}
public class AddressDto
{
public readonly string Country;
public readonly string SubnationalEntity;
...
}
public class MyViewModel
{
public string AddressViewModel BillingAddress { get; set; }
public string AddressViewModel ShippingAddress { get; set; }
public string Phone { get; set; }
...
}
public class AddressViewModel
{
public string Country { get; set; }
public string SubnationalEntity { get; set; }
...
}
When I save the change everything looks good. CaseWorkNote entity is properly created and added to workNotes collection (property of Case entity).
When CurrentUnitOfWork calls DbContext->SaveChanges() I see that my entity is there with status Added.
In the end nothing is saved to DB.
What I miss in my code or what I'm doing wrong?
Below is my code and screenshot with tracked entity.
Model:
public class Case : FullAuditedAggregateRoot<Guid>
{
[Required]
public CaseType Type { get; set; }
[Required]
public string Subject { get; set; }
public string Descripion { get; set; }
//Aggregated entity
private HashSet<CaseWorkNote> _workNotes;
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
//
public CaseWorkNote AddNote(string text)
{
if (_workNotes is null)
{
_workNotes = new HashSet<CaseWorkNote>();
}
CaseWorkNote workNote = CaseWorkNote.Create(this, text);
_workNotes.Add(workNote);
return workNote;
}
}
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public Case Case { get; private set; }
[Required]
public string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
DBcontext:
public class testDbContext : AbpZeroDbContext<Tenant, Role, User, testDbContext>
{
public DbSet<Case> Cases { get; set; }
public DbSet<CaseWorkNote> CaseWorkNotes { get; set; }
public testDbContext(DbContextOptions<testDbContext> options)
: base(options) { }
public override int SaveChanges()
{
//Here I see CaseWorkNote entity with state = "Added"
var entries = this.ChangeTracker.Entries();
foreach (var item in entries)
{
Debug.WriteLine("State: {0}, Type: {1}", item.State.ToString(), item.Entity.GetType().FullName);
}
return base.SaveChanges();
}
}
Application Service Class:
public class CaseAppService : AsyncCrudAppService<Case, CaseDto, Guid, PagedCaseResultRequestDto, CreateCaseDto, UpdateCaseDto>, ICaseAppService
{
//Removed for brevity
...
//
public async Task AddWorkNote(CreateUpdateCaseWorkNoteDto input)
{
var kase = await this.GetEntityByIdAsync(input.CaseId);
kase.AddNote(input.Text);
CurrentUnitOfWork.SaveChanges();
}
protected override async Task<Case> GetEntityByIdAsync(Guid id)
{
var kase = await Repository
.GetAllIncluding(c => c.WorkNotes)
.FirstOrDefaultAsync(c => c.Id == id);
if (kase == null)
{
throw new EntityNotFoundException(typeof(Case), id);
}
return kase;
}
public async Task<ListResultDto<CaseWorkNoteDto>> GetWorkNotes(EntityDto<Guid> entity)
{
var kase = await this.GetEntityByIdAsync(entity.Id);
return new ListResultDto<CaseWorkNoteDto>(MapToEntityDto(kase).WorkNotes.ToList());
}
}
thanks
The problem is caused by the default EF Core property access mode and ToList() call here
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes?.ToList();
Not sure what type of methodology are you following, but you are violating the simple good design rule that property (and especially collection type) should not allocate on each get. Not only because it is inefficient, but also allows the "smart" client like EF Core to detect the actual type as List and try using it to add items when loading related data.
In reality with this type of implementation they are adding to a list which is discarded, in other words - nowhere. So EF Core loading related data / navigation property fixup doesn't work, which also may affect the change tracker and lead to weird behaviors.
To fix the EF Core issue, you should configure EF Core to use directly the backing field. The easiest way is to set it globally inside the OnModelCreating override:
modelBuilder.UsePropertyAccessMode(PropertyAccessMode.Field);
It also can be set per entity or per entity property, but I would suggest the above, moreover one of the expected changes in EF Core 3.0 is that Backing fields will be used by default.
Anyway, now the problem in question will be solved.
Still, it will be better to follow the good practices. The _workNotes member should be initialized with initializer or in class constructor, and property getter should return it directly. If the idea was to prevent the caller to get access to the private member by casting the result, then there are other ways to prevent that which does not clone the collection content. For instance:
//Aggregated entity
private readonly HashSet<CaseWorkNote> _workNotes = new HashSet<CaseWorkNote>();
public IEnumerable<CaseWorkNote> WorkNotes => _workNotes.Select(e => e);
//
Regardless of whether you keep your current implementation of the navigation property or not, you must let EF Core use the backing field directly.
Add foreign key property CaseId.
Also added Virtual keyword.
public class CaseWorkNote : FullAuditedEntity
{
[ForeignKey("CaseId")]
[Required]
public virtual Case Case { get; private set; }
public virtual Guid CaseId { get; private set; } /* Added */
[Required]
public virtual string Text { get; set; }
private CaseWorkNote() : base() { }
public static CaseWorkNote Create(Case kase, string text)
{
return new CaseWorkNote()
{
Case = kase,
Text = text
};
}
}
I'm trying to use AutoMapper (v6.1.1) to flatten out a class containing further nested classes.
For reasons™ I'm unable to alter these classes, so altering names is not possible.
Bearing this in mind I can solve it using the static Mapper like so:
// This is the nested user, it has a further nested class
public class NestedUser
{
public long id { get; set; }
public string type { get; set; }
public Attributes attributes { get; set; }
}
public class Attributes
{
public string first_name { get; set; }
public string last_name { get; set; }
public string name { get; set; }
}
// This is the flattened representation
public class FlattenedUser
{
public long id { get; set; }
public string type { get; set; }
public string first_name { get; set; }
public string last_name { get; set; }
public string name { get; set; }
}
// Create a nested user
var nested = new NestedUser
{
id = 1,
type = "Contact",
attributes = new Attributes
{
first_name = "Equals",
last_name = "Kay",
name = "Equalsk"
}
};
// Use the static Mapper to flatten
Mapper.Initialize(cfg =>
{
cfg.CreateMap<Attributes, FlattenedUser>(MemberList.None);
cfg.CreateMap<NestedUser, FlattenedUser>(MemberList.None)
.ConstructUsing(s => Mapper.Map<FlattenedUser>(s.attributes));
});
Mapper.AssertConfigurationIsValid();
var flattened = Mapper.Map<FlattenedUser>(nested);
The object flattened now has all of its properties correctly populated.
For more reasons™ I want to use an instance of AutoMapper, something like this:
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Attributes, FlattenedUser>(MemberList.None);
cfg.CreateMap<NestedUser, FlattenedUser>(MemberList.None)
.ConstructUsing(s => Mapper.Map<FlattenedUser>(s.attributes));
});
config.AssertConfigurationIsValid();
var flattened = config.CreateMapper().Map<FlattenedUser>(nested);
My issue is the line .ConstructUsing(s => ... ));, it refers to the static Mapper and so it throws a runtime exception:
System.InvalidOperationException: 'Mapper not initialized. Call Initialize with appropriate configuration. If you are trying to use mapper instances through a container or otherwise, make sure you do not have any calls to the static Mapper.Map methods, and if you're using ProjectTo or UseAsDataSource extension methods, make sure you pass in the appropriate IConfigurationProvider instance.'
I don't want to use .ForMember(...) for each nested property as it defeats the object of what I'm trying to do.
Now I'm stuck. Is it possible to flatten out the nested class using an instance of AutoMapper as opposed to the static way?
I see 2 options to resolve the issue, depending on your case one should fit:
First: if you want to continue using static mapper for nested object than you have to move registration of nested objects to static configuration:
Mapper.Initialize(cfg=> cfg.CreateMap<Attributes, FlattenedUser>(MemberList.None));
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<NestedUser, FlattenedUser>(MemberList.None)
.ConstructUsing(s => Mapper.Map<FlattenedUser>(s.attributes));
});
Second: Use ResolutionContext
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<Attributes, FlattenedUser>(MemberList.None);
cfg.CreateMap<NestedUser, FlattenedUser>(MemberList.None)
.ConstructUsing((s, r) => r.Mapper.Map<FlattenedUser>(s.attributes));
});
I have an entity class Person and its corresponding DTO class PersonDto.
public class Person: Entity
{
public virtual string Name { get; set; }
public virtual string Phone { get; set; }
public virtual string Email { get; set; }
public virtual Sex Sex { get; set; }
public virtual Position Position { get; set; }
public virtual Division Division { get; set; }
public virtual Organization Organization { get; set; }
}
public class PersonDto: Dto
{
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public Guid SexId { get; set; }
public Guid PositionId { get; set; }
public Guid DivisionId { get; set; }
public Guid OrganizationId { get; set; }
}
After receiving a DTO object I have to convert it into a person entity. Now I do it completely manually. The code looks like this.
public class PersonEntityMapper: IEntityMapper<Person, PersonDto>
{
private IRepository<Person> _personRepository;
private IRepository<Sex> _sexRepository;
private IRepository<Position> _positionRepository;
private IRepository<Division> _divisionRepository;
private IRepository<Organization> _organizationRepository;
public PersonEntityMapper(IRepository<Person> personRepository,
IRepository<Sex> sexRepository,
IRepository<Position> positionRepository,
IRepository<Division> divisionRepository,
IRepository<Organization> organizationRepository)
{
... // Assigning repositories
}
Person Map(PersonDto dto)
{
Person person = CreateOrLoadPerson(dto);
person.Name = dto.Name;
person.Phone = dto.Phone;
person.Email = dto.Email;
person.Sex = _sexRepository.LoadById(dto.SexId);
person.Position = _positionRepository.LoadById(dto.PositionId);
person.Division = _divisionRepository.LoadById(dto.DivisionId);
person.Organization = _organizationRepository.LoadById(dto.OrganizationId);
return person;
}
}
The code is in fact trivial. But as the number of entities grows so does the number of mapper classes. The result is lots of similar code. Another issue is that when there are mode associations I have to add constructor parameteres for additional repositories. I tried to inject a some kind of a repository factory instead, but it smelled a bad-known Service Locator so I reverted to an original solution.
Unit testing of these mappers also results in a number of similar-looking test methods.
With all this been said I wonder if there exists a solution that can reduce the amount of manually written code and make the unit testing easier.
Thanks in advance.
UPDATE
I'd accomplished the task with Value Injecter but then I realized that I could safely remove it and the rest would still work. Here is the resulting solution.
public abstract class BaseEntityMapper<TEntity, TDto> : IEntityMapper<TEntity, TDto>
where TEntity : Entity, new()
where TDto : BaseDto
{
private readonly IRepositoryFactory _repositoryFactory;
protected BaseEntityMapper(IRepositoryFactory repositoryFactory)
{
_repositoryFactory = repositoryFactory;
}
public TEntity Map(TDto dto)
{
TEntity entity = CreateOrLoadEntity(dto.State, dto.Id);
MapPrimitiveProperties(entity, dto);
MapNonPrimitiveProperties(entity, dto);
return entity;
}
protected abstract void MapNonPrimitiveProperties(TEntity entity, TDto dto);
protected void MapPrimitiveProperties<TTarget, TSource>(TTarget target, TSource source, string prefix = "")
{
var targetProperties = target.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);
var sourceProperties = source.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(p => p.Name);
foreach (var targetProperty in targetProperties) {
foreach (var sourceProperty in sourceProperties) {
if (sourceProperty.Name != string.Format("{0}{1}", prefix, targetProperty.Name)) continue;
targetProperty.SetValue(target, sourceProperty.GetValue(source, null), null);
break;
}
}
}
protected void MapAssociation<TTarget, T>(TTarget target, Expression<Func<T>> expression, Guid id) where T : Entity
{
var repository = _repositoryFactory.Create<T>();
var propertyInfo = (PropertyInfo)((MemberExpression)expression.Body).Member;
propertyInfo.SetValue(target, repository.LoadById(id), null);
}
private TEntity CreateOrLoadEntity(DtoState dtoState, Guid entityId)
{
if (dtoState == DtoState.Created) return new TEntity();
if (dtoState == DtoState.Updated) {
return _repositoryFactory.Create<TEntity>().LoadById(entityId);
}
throw new BusinessException("Unknown DTO state");
}
}
Mapping of each entity is performed with a concrete class derived from BaseEntityMapper. The one for Person entities looks like this.
public class PersonEntityMapper: BaseEntityMapper<Person, PersonDto>
{
public PersonEntityMapper(IRepositoryFactory repositoryFactory) : base(repositoryFactory) {}
protected override void MapNonPrimitiveProperties(Person entity, PersonDto dto)
{
MapAssociation(entity, () => entity.Sex, dto.SexId);
MapAssociation(entity, () => entity.Position, dto.PositionId);
MapAssociation(entity, () => entity.Organization, dto.OrganizationId);
MapAssociation(entity, () => entity.Division, dto.DivisionId);
}
}
Explicitly calling MapAssociation protects against future properties renamings.
You can have a look on the two most used Object-Object mapper:
AutoMapper
AutoMapper is a simple little library built to solve a deceptively
complex problem - getting rid of code that mapped one object to
another. This type of code is rather dreary and boring to write, so
why not invent a tool to do it for us?
Value Injecter
ValueInjecter lets you define your own convention-based matching
algorithms (ValueInjections) in order to match up (inject) source
values to destination values.
There is a comparison article on SO: AutoMapper vs ValueInjecter
You can use GeDA for mapping any entity to a DTO object, it comes with either annotations or DSL support.
http://inspire-software.com/confluence/display/GeDA/FAQ
There are only basic examples on the wiki but jUnits of source code are full of useful examples
You can get it from sourceforge or google code manually or via maven dependency
Details are here: http://inspire-software.com/confluence/display/GeDA/GeDA+-+Generic+DTO+Assembler