I'm creating three-layered application, based on Entity Framework
and AutoMapper
. I have DAL, BLL and Presentation layer. Here are my Entities for data access layer:
[Table("Person")]
public class Person
{
[Key]
public virtual long Id { get; set; }
[Column("Pib")]
public virtual string Pib { get; set; }
[Column("Bd")]
public virtual Nullable<DateTime> BirthdaySingle { get; set; }
[Column("Bp")]
public virtual string BirthPlace { get; set; }
[Column("Other")]
public virtual string Other { get; set; }
public virtual ICollection<Photo> Photos { get; set; }
}
[Table("Photo")]
public class Photo
{
[Key]
public virtual long Id { get; set; }
[Column("Ph")]
public virtual byte[] RealPhoto { get; set; }
public virtual Nullable<long> PersonId { get; set; }
[ForeignKey("PersonId")]
public virtual Person Person { get; set; }
}
For business level layer:
public class PersonDTO
{
public virtual long Id { get; set; }
public virtual string Pib { get; set; }
public virtual Nullable<DateTime> BirthdaySingle { get; set; }
public virtual string BirthPlace { get; set; }
public virtual string Other { get; set; }
public virtual ICollection<PhotoDTO> Photos { get; set; }
}
public class PhotoDTO
{
public virtual long Id { get; set; }
public virtual byte[] RealPhoto { get; set; }
public virtual Nullable<long> PersonId { get; set; }
public virtual PersonDTO PersonDTO { get; set; }
}
And for presentation layer:
// class for showing details
public class PersonViewModel
{
public virtual long Id { get; set; }
public virtual string Pib { get; set; }
public virtual Nullable<DateTime> BirthdaySingle { get; set; }
public virtual string BirthPlace { get; set; }
public virtual string Other { get; set; }
public virtual ICollection<PhotoViewModel> Photos { get; set; }
public override string ToString()
{
string result = string.Empty;
if (!string.IsNullOrWhiteSpace(Pib))
result = string.Format("\r\n{0}", Pib);
if (BirthdaySingle.HasValue)
result += string.Format("\r\n{0}", BirthdaySingle.Value.ToShortDateString());
if (!string.IsNullOrWhiteSpace(BirthPlace))
result += string.Format("\r\n{0}", BirthPlace);
if (!string.IsNullOrWhiteSpace(Other))
result += string.Format("\r\n{0}", Other);
return result;
}
}
// class for showing list of objects
public class PersonListViewModel
{
public class PersonShortViewModel
{
[DisplayName("#")]
public virtual long Id { get; set; }
[DisplayName("Full Name")]
public virtual string Pib { get; set; }
[DisplayName("Birth Date")]
[DisplayFormat(DataFormatString = "{0:dd.MM.yyyy}")]
public virtual Nullable<DateTime> BirthdaySingle { get; set; }
}
public IPagedList<PersonShortViewModel> Persons { get; set; }
}
public class PhotoViewModel
{
public virtual long Id { get; set; }
public virtual byte[] RealPhoto { get; set; }
public virtual Nullable<long> PersonId { get; set; }
}
So, I have a DataService class in BLL:
public class DataService : IDataService
{
IUnitOfWork Database { get; set; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="T:System.Object"/>.
/// </summary>
public DataService(IUnitOfWork database)
{
//AutoMapperBLLConfiguration.Configure();
Database = database;
}
public bool IsConnected()
{
return Database.IsConnected();
}
public IQueryable<PersonDTO> GetPersons()
{
Mapper.CreateMap<Person, PersonDTO>().ForMember(ph => ph.Photos, opt => opt.Ignore());
return Database.Persons.GetAll().ProjectTo<PersonDTO>();
}
public PersonDTO GetPerson(long id)
{
var person = Database.Persons.GetById(id);
if (person == null)
{
throw new ValidationException("Об'єкт не знайдено.", "");
}
Mapper.CreateMap<Photo, PhotoDTO>();
Mapper.CreateMap<Person, PersonDTO>().ForMember(pe => pe.Photos, opt => opt.MapFrom(p => p.Photos));
return Mapper.Map<PersonDTO>(person);
}
public IEnumerable<PersonDTO> GetPersonsBy(Expression<Func<PersonDTO, bool>> predicate)
{
if (predicate == null)
{
throw new ValidationException("Відсутня умова пошуку.", "");
}
Mapper.CreateMap<PersonDTO, Person>().ForMember(person => person.CbdId, opt => opt.Ignore());
return
Mapper.Map<IEnumerable<PersonDTO>>(
Database.Persons.GetByCondition(predicate.RemapForType<PersonDTO, Person, bool>()));
}
public PhotoDTO GetPhoto(long id)
{
var photo = Database.Photos.GetById(id);
if (photo == null)
{
throw new ValidationException("Зображення не знайдено.", "");
}
return Mapper.Map<PhotoDTO>(photo);
}
public IEnumerable<PhotoDTO> GetPhotosBy(Expression<Func<PhotoDTO, bool>> predicate)
{
if (predicate == null)
{
throw new ValidationException("Відсутня умова пошуку.", "");
}
Expression<Func<Photo, bool>> mappedSelector = Mapper.Map<Expression<Func<Photo, bool>>>(predicate);
return Mapper.Map<IEnumerable<PhotoDTO>>(Database.Photos.GetByCondition(mappedSelector));
}
public void Dispose()
{
Database.Dispose();
}
}
I decided to create separate configuration for AutoMapper because I think Presentation layer doesn't need to know about BLL...
public static class AutoMapperBLLConfiguration
{
public static void Configure()
{
Mapper.Initialize(configuration =>
/*configuration.AddProfile(new PhotoIgnoreProfile());
configuration.AddProfile(new PhotoIncludeProfile());*/
/*configuration.AddProfile(new PhotoProfile());
configuration.AddProfile(new PersonProfile());*/
GetConfiguration(Mapper.Configuration)
);
Mapper.AssertConfigurationIsValid();
}
private static void GetConfiguration(IConfiguration configuration)
{
var profiles =
typeof(PhotoProfile).Assembly.GetTypes().Where(type => typeof(Profile).IsAssignableFrom(type));
foreach (Type profile in profiles)
{
configuration.AddProfile(Activator.CreateInstance(profile) as Profile);
}
}
}
public class PersonProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<Person, PersonDTO>();
Mapper.CreateMap<PersonDTO, Person>().ForMember(person => person.CbdId, opt => opt.Ignore());
}
}
/*public class PhotoIgnoreProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<Person, PersonDTO>().ForMember(ph => ph.Photos, opt => opt.Ignore());
}
}
public class PhotoIncludeProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<Person, PersonDTO>().ForMember(pe => pe.Photos, opt => opt.MapFrom(p => p.Photos));
}
}*/
public class PhotoProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<Photo, PhotoDTO>().ForMember(dto => dto.PersonDTO, opt => opt.MapFrom(photo => photo.Person));
Mapper.CreateMap<PhotoDTO, Photo>().ForMember(photo => photo.Person, opt => opt.MapFrom(dto => dto.PersonDTO));
}
}
And for Presentation layer:
public static class AutoMapperPLConfiguration
{
public static void Configure()
{
Mapper.Initialize(configuration =>
//configuration.AddProfile(new PersonViewProfile());
//configuration.AddProfile(new PhotoViewProfile());
GetConfiguration(Mapper.Configuration)
);
Mapper.AssertConfigurationIsValid();
}
private static void GetConfiguration(IConfiguration configuration)
{
// we use order by because we need photo mapping to be the first
var profiles =
typeof(PhotoViewProfile).Assembly.GetTypes().Where(type => typeof(Profile).IsAssignableFrom(type)).OrderByDescending(type => type.Name);
foreach (Type profile in profiles)
{
configuration.AddProfile(Activator.CreateInstance(profile) as Profile);
}
}
}
public class PersonViewProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<PersonDTO, PersonViewModel>()
.ForMember(model => model.Photos, opt => opt.MapFrom(dto => dto.Photos));
Mapper.CreateMap<PersonViewModel, PersonDTO>()
.ForMember(dto => dto.Photos, opt => opt.MapFrom(model => model.Photos));
}
}
public class PersonShortViewProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
Mapper.CreateMap<IPagedList<PersonDTO>, IPagedList<PersonListViewModel.PersonShortViewModel>>()
.AfterMap((s, d) =>
Mapper
.Map<IEnumerable<PersonDTO>, IEnumerable<PersonListViewModel.PersonShortViewModel>>(s, d))
.ConvertUsing<PagedListConverter<PersonDTO, PersonListViewModel.PersonShortViewModel>>();
Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
}
}
public class PhotoViewProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<PhotoDTO, PhotoViewModel>().ForSourceMember(dto => dto.PersonDTO, opt => opt.Ignore());
Mapper.CreateMap<PhotoViewModel, PhotoDTO>().ForMember(dto => dto.PersonDTO, opt => opt.Ignore());
}
}
Also I have such extensions for using Expressions
and converting to PagedList
:
public class PagedListConverter<TIn, TOut> : ITypeConverter<IPagedList<TIn>, IPagedList<TOut>>
{
/// <summary>
/// Performs conversion from source to destination type
/// </summary>
/// <param name="context">Resolution context</param>
/// <returns>
/// Destination object
/// </returns>
public IPagedList<TOut> Convert(ResolutionContext context)
{
var source = (IPagedList<TIn>) context.SourceValue;
var mapped = Mapper.Map<IList<TOut>>(source);
return new StaticPagedList<TOut>(mapped,source.GetMetaData());
}
}
/// <summary>
/// An <see cref="ExpressionVisitor"/> implementation which uses <see href="http://automapper.org">AutoMapper</see> to remap property access from elements of type <typeparamref name="TSource"/> to elements of type <typeparamref name="TDestination"/>.
/// </summary>
/// <typeparam name="TSource">The type of the source element.</typeparam>
/// <typeparam name="TDestination">The type of the destination element.</typeparam>
public class AutoMapVisitor<TSource, TDestination> : ExpressionVisitor
{
private readonly ParameterExpression _newParameter;
private readonly TypeMap _typeMap = Mapper.FindTypeMapFor<TSource, TDestination>();
/// <summary>
/// Initialises a new instance of the <see cref="AutoMapVisitor{TSource, TDestination}"/> class.
/// </summary>
/// <param name="newParameter">The new <see cref="ParameterExpression"/> to access.</param>
public AutoMapVisitor(ParameterExpression newParameter)
{
Contract.Requires(newParameter != null);
_newParameter = newParameter;
Contract.Assume(_typeMap != null);
}
[ContractInvariantMethod]
[SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "Required for code contracts.")]
private void ObjectInvariant()
{
Contract.Invariant(_typeMap != null);
Contract.Invariant(_newParameter != null);
}
/// <summary>
/// Visits the children of the <see cref="T:System.Linq.Expressions.MemberExpression"/>.
/// </summary>
/// <returns>
/// The modified expression, if it or any subexpression was modified; otherwise, returns the original expression.
/// </returns>
/// <param name="node">The expression to visit.</param>
protected override Expression VisitMember(MemberExpression node)
{
var propertyMaps = _typeMap.GetPropertyMaps();
Contract.Assume(propertyMaps != null);
// Find any mapping for this member
var propertyMap = propertyMaps.SingleOrDefault(map => map.SourceMember == node.Member);
if (propertyMap == null)
{
return base.VisitMember(node);
}
var destinationProperty = propertyMap.DestinationProperty;
Contract.Assume(destinationProperty != null);
var destinationMember = destinationProperty.MemberInfo;
Contract.Assume(destinationMember != null);
// Check the new member is a property too
var property = destinationMember as PropertyInfo;
if (property == null)
{
return base.VisitMember(node);
}
// Access the new property
var newPropertyAccess = Expression.Property(_newParameter, property);
return base.VisitMember(newPropertyAccess);
}
}
/// <summary>
/// A class which contains extension methods for <see cref="Expression"/> and <see cref="Expression{TDelegate}"/> instances.
/// </summary>
public static class ExpressionExtensions
{
/// <summary>
/// Remaps all property access from type <typeparamref name="TSource"/> to <typeparamref name="TDestination"/> in <paramref name="expression"/>.
/// </summary>
/// <typeparam name="TSource">The type of the source element.</typeparam>
/// <typeparam name="TDestination">The type of the destination element.</typeparam>
/// <typeparam name="TResult">The type of the result from the lambda expression.</typeparam>
/// <param name="expression">The <see cref="Expression{TDelegate}"/> to remap the property access in.</param>
/// <returns>An <see cref="Expression{TDelegate}"/> equivalent to <paramref name="expression"/>, but applying to elements of type <typeparamref name="TDestination"/> instead of <typeparamref name="TSource"/>.</returns>
public static Expression<Func<TDestination, TResult>> RemapForType<TSource, TDestination, TResult>(
this Expression<Func<TSource, TResult>> expression)
{
Contract.Requires(expression != null);
Contract.Ensures(Contract.Result<Expression<Func<TDestination, TResult>>>() != null);
var newParameter = Expression.Parameter(typeof (TDestination));
Contract.Assume(newParameter != null);
var visitor = new AutoMapVisitor<TSource, TDestination>(newParameter);
var remappedBody = visitor.Visit(expression.Body);
if (remappedBody == null)
{
throw new InvalidOperationException("Unable to remap expression");
}
return Expression.Lambda<Func<TDestination, TResult>>(remappedBody, newParameter);
}
}
Use it like this:
//...
PersonListViewModel list = new PersonListViewModel();
Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>().IgnoreAllNonExisting();
if (predicate == null)
{
//Mapper.CreateMap<PersonDTO, PersonListViewModel.PersonShortViewModel>();
list.Persons =
Mapper.Map<IPagedList<PersonListViewModel.PersonShortViewModel>>(
DataService.GetPersons()
.OrderBy(person => person.Pib)
.ToPagedList(pageIndex, pagingSettings.PageSize));
}
else
{
list.Persons =
Mapper.Map<IPagedList<PersonListViewModel.PersonShortViewModel>>(
DataService.GetPersonsBy(predicate.RemapForType<PersonViewModel, PersonDTO, bool>())
.OrderBy(person => person.Pib)
.ToPagedList(pageIndex, pagingSettings.PageSize));
}
//...
So, that is not all, but other code seems not be matter at all. But if it will, please ask me for it and I'll add it here also.
And instantiate mapper configuration here:
internal static class Program
{
/// <summary>
/// Main point.
/// </summary>
[STAThread]
private static void Main()
{
AutoMapperBLLConfiguration.Configure();
AutoMapperPLConfiguration.Configure();
// etc.
}
}
My main question is: Why AutoMapper
seems like rewriting configuration, because after instantiating it I still need to do Mapper.CreateMap<>
before each operation? And how can I create different configs for similar entities in one place? E.g. now it shows this error:
Missing type map configuration or unsupported mapping.
Mapping types:
Person -> PersonDTO
Reestr.DAL.Entities.Person -> Reestr.BLL.DTO.PersonDTO
Destination path:
IEnumerable`1[0]
Source value:
System.Data.Entity.DynamicProxies.Person_81BD716087EE14CF5E255587795725BC7C06DC2382A1A8EBF33C29A04F551C34
How can I separate AutoMapper
create configuration betweeen different layers (as you saw it was commented line in DataService
constructor)?
And could you please help me with some architecture logic, because I'm only beginner and I want my program to be the best practice/ I'm fighting with this issues for 3 days...Thank you!