Replaced BuddyType attribute with ModelMetadatType and SimpleMapper with Automapper

This commit is contained in:
Kirthi Krishnamraju 2015-01-22 16:32:27 -08:00
parent 89f2e3e32a
commit 2e84d68973
7 changed files with 57 additions and 257 deletions

View File

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Mvc;
using AutoMapper;
using MusicStore.Infrastructure;
using MusicStore.Models;
using MusicStore.Spa.Infrastructure;
@ -26,12 +27,12 @@ namespace MusicStore.Apis
await _storeContext.Artists.LoadAsync();
var albums = await _storeContext.Albums
//.Include(a => a.Genre)
//.Include(a => a.Artist)
.ToPagedListAsync(page, pageSize, sortBy,
// .Include(a => a.Genre)
// .Include(a => a.Artist)
.ToPagedListAsync(page, pageSize, sortBy,
a => a.Title, // sortExpression
SortDirection.Ascending, // defaultSortDirection
a => SimpleMapper.Map(a, new AlbumResultDto())); // selector
a => Mapper.Map(a, new AlbumResultDto())); // selector
return Json(albums);
}
@ -46,7 +47,7 @@ namespace MusicStore.Apis
.OrderBy(a => a.Title)
.ToListAsync();
return Json(albums.Select(a => SimpleMapper.Map(a, new AlbumResultDto())));
return Json(albums.Select(a => Mapper.Map(a, new AlbumResultDto())));
}
[HttpGet("mostPopular")]
@ -60,7 +61,7 @@ namespace MusicStore.Apis
.ToListAsync();
// 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}")]
@ -76,7 +77,7 @@ namespace MusicStore.Apis
.Where(a => a.AlbumId == albumId)
.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
//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
var dbAlbum = new Album();
_storeContext.Albums.Add(SimpleMapper.Map(album, dbAlbum));
await _storeContext.SaveChangesAsync();
_storeContext.Albums.Add(Mapper.Map(album, dbAlbum));
await _storeContext.SaveChangesAsync();
// TODO: Handle missing record, key violations, concurrency issues, etc.
@ -133,7 +134,7 @@ namespace MusicStore.Apis
}
// Save the changes to the DB
SimpleMapper.Map(album, dbAlbum);
Mapper.Map(album, dbAlbum);
await _storeContext.SaveChangesAsync();
// TODO: Handle missing record, key violations, concurrency issues, etc.
@ -166,44 +167,44 @@ namespace MusicStore.Apis
Message = "Album deleted successfully."
};
}
}
[BuddyType(typeof(Album))]
public class AlbumChangeDto
[ModelMetadataType(typeof(Album))]
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; }
public int ArtistId { get; set; }
public string Title { get; set; }
public decimal Price { get; set; }
public string AlbumArtUrl { get; set; }
Artist = new ArtistResultDto();
Genre = new GenreResultDto();
}
public class AlbumResultDto : AlbumChangeDto
{
public AlbumResultDto()
{
Artist = new ArtistResultDto();
Genre = new GenreResultDto();
}
public int AlbumId { get; set; }
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 string Name { get; set; }
}
public class GenreResultDto
{
public string Name { get; set; }
}
public class GenreResultDto
{
public string Name { get; set; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>();
}
}
}

View File

@ -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;
}
}
}

View File

@ -5,9 +5,12 @@ using Microsoft.AspNet.Mvc;
using Microsoft.Data.Entity;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
using AutoMapper;
using MusicStore.Apis;
using MusicStore.Models;
using MusicStore.Spa.Infrastructure;
namespace MusicStore.Spa
{
public class Startup
@ -19,7 +22,7 @@ namespace MusicStore.Spa
.AddEnvironmentVariables();
}
public IConfiguration Configuration { get; set; }
public Microsoft.Framework.ConfigurationModel.IConfiguration Configuration { get; set; }
public void ConfigureServices(IServiceCollection services)
{
@ -32,11 +35,6 @@ namespace MusicStore.Spa
// Add MVC services to the service container
services.AddMvc();
services.Configure<MvcOptions>(options =>
{
options.ModelValidatorProviders.Add(typeof(BuddyValidatorProvider));
});
// Add EF services to the service container
services.AddEntityFramework()
.AddSqlServer()
@ -61,6 +59,14 @@ namespace MusicStore.Spa
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)

View File

@ -34,7 +34,8 @@
"Microsoft.AspNet.Authentication.Cookies": "1.0.0-*",
"Microsoft.AspNet.Identity.EntityFramework": "3.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": {
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
@ -49,4 +50,4 @@
"aspnet50": {},
"aspnetcore50": {}
}
}
}