Replaced BuddyType attribute with ModelMetadatType and SimpleMapper with Automapper
This commit is contained in:
parent
89f2e3e32a
commit
2e84d68973
|
|
@ -2,6 +2,7 @@
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNet.Authorization;
|
using Microsoft.AspNet.Authorization;
|
||||||
using Microsoft.AspNet.Mvc;
|
using Microsoft.AspNet.Mvc;
|
||||||
|
using AutoMapper;
|
||||||
using MusicStore.Infrastructure;
|
using MusicStore.Infrastructure;
|
||||||
using MusicStore.Models;
|
using MusicStore.Models;
|
||||||
using MusicStore.Spa.Infrastructure;
|
using MusicStore.Spa.Infrastructure;
|
||||||
|
|
@ -26,12 +27,12 @@ namespace MusicStore.Apis
|
||||||
await _storeContext.Artists.LoadAsync();
|
await _storeContext.Artists.LoadAsync();
|
||||||
|
|
||||||
var albums = await _storeContext.Albums
|
var albums = await _storeContext.Albums
|
||||||
//.Include(a => a.Genre)
|
// .Include(a => a.Genre)
|
||||||
//.Include(a => a.Artist)
|
// .Include(a => a.Artist)
|
||||||
.ToPagedListAsync(page, pageSize, sortBy,
|
.ToPagedListAsync(page, pageSize, sortBy,
|
||||||
a => a.Title, // sortExpression
|
a => a.Title, // sortExpression
|
||||||
SortDirection.Ascending, // defaultSortDirection
|
SortDirection.Ascending, // defaultSortDirection
|
||||||
a => SimpleMapper.Map(a, new AlbumResultDto())); // selector
|
a => Mapper.Map(a, new AlbumResultDto())); // selector
|
||||||
|
|
||||||
return Json(albums);
|
return Json(albums);
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +47,7 @@ namespace MusicStore.Apis
|
||||||
.OrderBy(a => a.Title)
|
.OrderBy(a => a.Title)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
return Json(albums.Select(a => SimpleMapper.Map(a, new AlbumResultDto())));
|
return Json(albums.Select(a => Mapper.Map(a, new AlbumResultDto())));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("mostPopular")]
|
[HttpGet("mostPopular")]
|
||||||
|
|
@ -60,7 +61,7 @@ namespace MusicStore.Apis
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
// TODO: Move the .Select() to end of albums query when EF supports it
|
// TODO: Move the .Select() to end of albums query when EF supports it
|
||||||
return Json(albums.Select(a => SimpleMapper.Map(a, new AlbumResultDto())));
|
return Json(albums.Select(a => Mapper.Map(a, new AlbumResultDto())));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{albumId:int}")]
|
[HttpGet("{albumId:int}")]
|
||||||
|
|
@ -76,7 +77,7 @@ namespace MusicStore.Apis
|
||||||
.Where(a => a.AlbumId == albumId)
|
.Where(a => a.AlbumId == albumId)
|
||||||
.SingleOrDefaultAsync();
|
.SingleOrDefaultAsync();
|
||||||
|
|
||||||
var albumResult = SimpleMapper.Map(album, new AlbumResultDto());
|
var albumResult = Mapper.Map(album, new AlbumResultDto());
|
||||||
|
|
||||||
// TODO: Get these from the related entities when EF supports that again, i.e. when .Include() works
|
// TODO: Get these from the related entities when EF supports that again, i.e. when .Include() works
|
||||||
//album.Artist.Name = (await _storeContext.Artists.SingleOrDefaultAsync(a => a.ArtistId == album.ArtistId)).Name;
|
//album.Artist.Name = (await _storeContext.Artists.SingleOrDefaultAsync(a => a.ArtistId == album.ArtistId)).Name;
|
||||||
|
|
@ -99,8 +100,8 @@ namespace MusicStore.Apis
|
||||||
|
|
||||||
// Save the changes to the DB
|
// Save the changes to the DB
|
||||||
var dbAlbum = new Album();
|
var dbAlbum = new Album();
|
||||||
_storeContext.Albums.Add(SimpleMapper.Map(album, dbAlbum));
|
_storeContext.Albums.Add(Mapper.Map(album, dbAlbum));
|
||||||
await _storeContext.SaveChangesAsync();
|
await _storeContext.SaveChangesAsync();
|
||||||
|
|
||||||
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
||||||
|
|
||||||
|
|
@ -133,7 +134,7 @@ namespace MusicStore.Apis
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save the changes to the DB
|
// Save the changes to the DB
|
||||||
SimpleMapper.Map(album, dbAlbum);
|
Mapper.Map(album, dbAlbum);
|
||||||
await _storeContext.SaveChangesAsync();
|
await _storeContext.SaveChangesAsync();
|
||||||
|
|
||||||
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
||||||
|
|
@ -166,44 +167,44 @@ namespace MusicStore.Apis
|
||||||
Message = "Album deleted successfully."
|
Message = "Album deleted successfully."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[BuddyType(typeof(Album))]
|
[ModelMetadataType(typeof(Album))]
|
||||||
public class AlbumChangeDto
|
public class AlbumChangeDto
|
||||||
|
{
|
||||||
|
public int GenreId { get; set; }
|
||||||
|
|
||||||
|
public int ArtistId { get; set; }
|
||||||
|
|
||||||
|
public string Title { get; set; }
|
||||||
|
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
public string AlbumArtUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AlbumResultDto : AlbumChangeDto
|
||||||
|
{
|
||||||
|
public AlbumResultDto()
|
||||||
{
|
{
|
||||||
public int GenreId { get; set; }
|
Artist = new ArtistResultDto();
|
||||||
|
Genre = new GenreResultDto();
|
||||||
public int ArtistId { get; set; }
|
|
||||||
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
|
|
||||||
public string AlbumArtUrl { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AlbumResultDto : AlbumChangeDto
|
public int AlbumId { get; set; }
|
||||||
{
|
|
||||||
public AlbumResultDto()
|
|
||||||
{
|
|
||||||
Artist = new ArtistResultDto();
|
|
||||||
Genre = new GenreResultDto();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int AlbumId { get; set; }
|
public ArtistResultDto Artist { get; private set; }
|
||||||
|
|
||||||
public ArtistResultDto Artist { get; private set; }
|
public GenreResultDto Genre { get; private set; }
|
||||||
|
}
|
||||||
|
|
||||||
public GenreResultDto Genre { get; private set; }
|
public class ArtistResultDto
|
||||||
}
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class ArtistResultDto
|
public class GenreResultDto
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
public class GenreResultDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.AspNet.Mvc;
|
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
|
||||||
|
|
||||||
namespace MusicStore.Spa.Infrastructure
|
|
||||||
{
|
|
||||||
public class BuddyModelMetadataProvider : DataAnnotationsModelMetadataProvider
|
|
||||||
{
|
|
||||||
protected override CachedDataAnnotationsModelMetadata CreateMetadataPrototype(IEnumerable<object> attributes, Type containerType, Type modelType, string propertyName)
|
|
||||||
{
|
|
||||||
var realTypeMetadata = base.CreateMetadataPrototype(attributes, containerType, modelType, propertyName);
|
|
||||||
var buddyType = BuddyTypeAttribute.GetBuddyType(modelType);
|
|
||||||
|
|
||||||
if (buddyType != null)
|
|
||||||
{
|
|
||||||
var buddyMetadata = base.CreateMetadataPrototype(attributes, containerType, buddyType, propertyName);
|
|
||||||
foreach (var realProperty in realTypeMetadata.Properties)
|
|
||||||
{
|
|
||||||
var buddyProperty = buddyMetadata.Properties.SingleOrDefault(bp => string.Equals(bp.PropertyName, realProperty.PropertyName, StringComparison.Ordinal));
|
|
||||||
if (buddyProperty != null)
|
|
||||||
{
|
|
||||||
// TODO: Only overwrite if the real type doesn't explicitly set it
|
|
||||||
realProperty.IsReadOnly = buddyProperty.IsReadOnly;
|
|
||||||
realProperty.IsRequired = buddyProperty.IsRequired;
|
|
||||||
realProperty.DisplayName = buddyProperty.DisplayName;
|
|
||||||
realProperty.DisplayFormatString = buddyProperty.DisplayFormatString;
|
|
||||||
realProperty.SimpleDisplayProperty = buddyProperty.SimpleDisplayProperty;
|
|
||||||
realProperty.DataTypeName = buddyProperty.DataTypeName;
|
|
||||||
realProperty.Description = buddyProperty.Description;
|
|
||||||
realProperty.EditFormatString = buddyProperty.EditFormatString;
|
|
||||||
realProperty.NullDisplayText = buddyProperty.NullDisplayText;
|
|
||||||
realProperty.ShowForDisplay = buddyProperty.ShowForDisplay;
|
|
||||||
realProperty.ShowForEdit = buddyProperty.ShowForEdit;
|
|
||||||
realProperty.TemplateHint = buddyProperty.TemplateHint;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return realTypeMetadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace MusicStore.Spa.Infrastructure
|
|
||||||
{
|
|
||||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
|
||||||
public sealed class BuddyTypeAttribute : Attribute
|
|
||||||
{
|
|
||||||
private readonly Type _metadataBuddyType;
|
|
||||||
private readonly Type _validatorBuddyType;
|
|
||||||
|
|
||||||
public BuddyTypeAttribute(Type buddyType)
|
|
||||||
{
|
|
||||||
_metadataBuddyType = buddyType;
|
|
||||||
_validatorBuddyType = buddyType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type MetadataBuddyType
|
|
||||||
{
|
|
||||||
get { return _metadataBuddyType; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type ValidatorBuddyType
|
|
||||||
{
|
|
||||||
get { return _validatorBuddyType; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Type GetBuddyType(Type type)
|
|
||||||
{
|
|
||||||
var attribute = type.GetTypeInfo().GetCustomAttribute<BuddyTypeAttribute>();
|
|
||||||
|
|
||||||
if (attribute != null)
|
|
||||||
{
|
|
||||||
return attribute.MetadataBuddyType;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
|
||||||
|
|
||||||
namespace MusicStore.Spa.Infrastructure
|
|
||||||
{
|
|
||||||
public class BuddyValidatorProvider : DataAnnotationsModelValidatorProvider
|
|
||||||
{
|
|
||||||
protected override IEnumerable<IModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<object> attributes)
|
|
||||||
{
|
|
||||||
var buddyType = BuddyTypeAttribute.GetBuddyType(metadata.ContainerType ?? metadata.ModelType);
|
|
||||||
|
|
||||||
if (buddyType != null)
|
|
||||||
{
|
|
||||||
var buddyProperty = buddyType.GetTypeInfo().GetDeclaredProperty(metadata.PropertyName);
|
|
||||||
if (buddyProperty != null)
|
|
||||||
{
|
|
||||||
var buddyTypeAttributes = buddyProperty.GetCustomAttributes();
|
|
||||||
// TODO: De-dupe?
|
|
||||||
attributes = attributes.Concat(buddyTypeAttributes);
|
|
||||||
return base.GetValidators(metadata, attributes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Enumerable.Empty<IModelValidator>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace MusicStore.Spa.Infrastructure
|
|
||||||
{
|
|
||||||
public class SimpleMapper
|
|
||||||
{
|
|
||||||
private static readonly Expression _emptyExp = Expression.Empty();
|
|
||||||
private static ConcurrentDictionary<Tuple<Type, Type>, Delegate> _mapCache = new ConcurrentDictionary<Tuple<Type, Type>, Delegate>();
|
|
||||||
|
|
||||||
public static TDest Map<TSource, TDest>(TSource source, TDest dest)
|
|
||||||
{
|
|
||||||
var map = (Func<TSource, TDest, TDest>)_mapCache.GetOrAdd(Tuple.Create(typeof(TSource), typeof(TDest)), _ => MakeMapMethod<TSource, TDest>());
|
|
||||||
return map(source, dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Func<TSource, TDest, TDest> MakeMapMethod<TSource, TDest>()
|
|
||||||
{
|
|
||||||
// TODO: Support convention-based mapping, e.g. AlbumTitle <- Album.Title
|
|
||||||
// TODO: Support mapping to/from fields
|
|
||||||
|
|
||||||
var sourceProps = typeof(TSource).GetRuntimeProperties().ToDictionary(p => p.Name);
|
|
||||||
var destProps = typeof(TDest).GetRuntimeProperties().ToDictionary(p => p.Name);
|
|
||||||
|
|
||||||
var destArg = Expression.Parameter(typeof(TDest), "dest");
|
|
||||||
var srcArg = Expression.Parameter(typeof(TSource), "src");
|
|
||||||
|
|
||||||
var assignments = MakeAssignments(typeof(TSource), typeof(TDest), srcArg, destArg);
|
|
||||||
|
|
||||||
if (!assignments.Any())
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(string.Format("No matching properties were found between the types {0} and {1}", typeof(TSource), typeof(TDest)));
|
|
||||||
}
|
|
||||||
|
|
||||||
var assignmentsBlock = Expression.Block(assignments);
|
|
||||||
var blockExp = Expression.Block(typeof(TDest), assignmentsBlock, destArg);
|
|
||||||
var map = Expression.Lambda<Func<TSource, TDest, TDest>>(blockExp, srcArg, destArg);
|
|
||||||
|
|
||||||
return map.Compile();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<Expression> MakeAssignments(Type sourceType, Type destType, Expression sourcePropertyExp, Expression destPropertyExp)
|
|
||||||
{
|
|
||||||
var sourceProps = sourceType.GetRuntimeProperties().ToDictionary(p => p.Name);
|
|
||||||
var destProps = destType.GetRuntimeProperties().ToDictionary(p => p.Name);
|
|
||||||
var assignments = new List<Expression>();
|
|
||||||
|
|
||||||
foreach (var srcProp in sourceProps)
|
|
||||||
{
|
|
||||||
if (!srcProp.Value.GetMethod.IsPublic) continue;
|
|
||||||
|
|
||||||
var destProp = destProps.ContainsKey(srcProp.Key) ? destProps[srcProp.Key] : null;
|
|
||||||
if (destProp != null && destProp.SetMethod != null)
|
|
||||||
{
|
|
||||||
var destPropType = destProp.PropertyType;
|
|
||||||
var srcPropType = srcProp.Value.PropertyType;
|
|
||||||
|
|
||||||
var srcPropExp = Expression.Property(sourcePropertyExp, srcProp.Value);
|
|
||||||
var destPropExp = Expression.Property(destPropertyExp, destProp);
|
|
||||||
|
|
||||||
if (destPropType.GetTypeInfo().IsAssignableFrom(srcPropType.GetTypeInfo()) && destProp.SetMethod.IsPublic)
|
|
||||||
{
|
|
||||||
var assignmentExp = Expression.Assign(destPropExp, srcPropExp);
|
|
||||||
// dest.Prop = src.Prop;
|
|
||||||
assignments.Add(assignmentExp);
|
|
||||||
}
|
|
||||||
else if (destProp.GetMethod.IsPublic)
|
|
||||||
{
|
|
||||||
// The properties aren't assignable but they may have members that are
|
|
||||||
var deepAssignmentExp = MakeAssignments(srcPropType, destPropType, srcPropExp, destPropExp);
|
|
||||||
if (deepAssignmentExp.Any())
|
|
||||||
{
|
|
||||||
// Check if dest is null and if so skip for now
|
|
||||||
// - if (dest.Foo != null && src.Foo != null) {
|
|
||||||
// - dest.Foo.Bar = src.Foo.Bar;
|
|
||||||
// - }
|
|
||||||
var nullCheckExp = Expression.And(Expression.NotEqual(destPropExp, Expression.Constant(null)),
|
|
||||||
Expression.NotEqual(srcPropExp, Expression.Constant(null)));
|
|
||||||
var nullCheckExpBlock = Expression.IfThen(nullCheckExp, Expression.Block(deepAssignmentExp));
|
|
||||||
assignments.Add(nullCheckExpBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return assignments;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -5,9 +5,12 @@ using Microsoft.AspNet.Mvc;
|
||||||
using Microsoft.Data.Entity;
|
using Microsoft.Data.Entity;
|
||||||
using Microsoft.Framework.ConfigurationModel;
|
using Microsoft.Framework.ConfigurationModel;
|
||||||
using Microsoft.Framework.DependencyInjection;
|
using Microsoft.Framework.DependencyInjection;
|
||||||
|
using AutoMapper;
|
||||||
|
using MusicStore.Apis;
|
||||||
using MusicStore.Models;
|
using MusicStore.Models;
|
||||||
using MusicStore.Spa.Infrastructure;
|
using MusicStore.Spa.Infrastructure;
|
||||||
|
|
||||||
|
|
||||||
namespace MusicStore.Spa
|
namespace MusicStore.Spa
|
||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
|
|
@ -19,7 +22,7 @@ namespace MusicStore.Spa
|
||||||
.AddEnvironmentVariables();
|
.AddEnvironmentVariables();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IConfiguration Configuration { get; set; }
|
public Microsoft.Framework.ConfigurationModel.IConfiguration Configuration { get; set; }
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
|
|
@ -32,11 +35,6 @@ namespace MusicStore.Spa
|
||||||
// Add MVC services to the service container
|
// Add MVC services to the service container
|
||||||
services.AddMvc();
|
services.AddMvc();
|
||||||
|
|
||||||
services.Configure<MvcOptions>(options =>
|
|
||||||
{
|
|
||||||
options.ModelValidatorProviders.Add(typeof(BuddyValidatorProvider));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add EF services to the service container
|
// Add EF services to the service container
|
||||||
services.AddEntityFramework()
|
services.AddEntityFramework()
|
||||||
.AddSqlServer()
|
.AddSqlServer()
|
||||||
|
|
@ -61,6 +59,14 @@ namespace MusicStore.Spa
|
||||||
options.AddPolicy("app-ManageStore", new AuthorizationPolicyBuilder().RequireClaim("app-ManageStore", "Allowed").Build());
|
options.AddPolicy("app-ManageStore", new AuthorizationPolicyBuilder().RequireClaim("app-ManageStore", "Allowed").Build());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Mapper.CreateMap<AlbumChangeDto, Album>();
|
||||||
|
Mapper.CreateMap<Album, AlbumChangeDto>();
|
||||||
|
Mapper.CreateMap<Album, AlbumResultDto>();
|
||||||
|
Mapper.CreateMap<AlbumResultDto, Album>();
|
||||||
|
Mapper.CreateMap<Artist, ArtistResultDto>();
|
||||||
|
Mapper.CreateMap<ArtistResultDto, Artist>();
|
||||||
|
Mapper.CreateMap<Genre, GenreResultDto>();
|
||||||
|
Mapper.CreateMap<GenreResultDto, Genre>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@
|
||||||
"Microsoft.AspNet.Authentication.Cookies": "1.0.0-*",
|
"Microsoft.AspNet.Authentication.Cookies": "1.0.0-*",
|
||||||
"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-*",
|
"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-*",
|
||||||
"Microsoft.Framework.ConfigurationModel": "1.0.0-*",
|
"Microsoft.Framework.ConfigurationModel": "1.0.0-*",
|
||||||
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*"
|
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*",
|
||||||
|
"AutoMapper": "4.0.0-ci1026"
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
|
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue