MusicStore.Spa changes:
- Added buddy validation provider Created SimpleMapper in MusicStore.Spa - Fixed EF mapping config - Made SimpleMapper to map DTOs to EF objects (and vice-versa) - Admin screens now return Artist/Genre names when showing Album details - Unhook grunt/npm/bower from project.json due to long-path issue in KRE build - Remove unneeded AngularHtmlHelper<T> class
This commit is contained in:
parent
1c3c7a0eef
commit
0352ef8a79
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.22112.0
|
||||
VisualStudioVersion = 14.0.22230.1
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{44621553-AA7D-4893-8834-79582A7D8348}"
|
||||
EndProject
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
using System.Linq;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using MusicStore.Infrastructure;
|
||||
using MusicStore.Models;
|
||||
using MusicStore.Spa.Infrastructure;
|
||||
|
||||
namespace MusicStore.Apis
|
||||
{
|
||||
|
|
@ -19,10 +21,16 @@ namespace MusicStore.Apis
|
|||
[HttpGet]
|
||||
public async Task<ActionResult> Paged(int page = 1, int pageSize = 50, string sortBy = null)
|
||||
{
|
||||
await _storeContext.Genres.LoadAsync();
|
||||
await _storeContext.Artists.LoadAsync();
|
||||
|
||||
var albums = await _storeContext.Albums
|
||||
//.Include(a => a.Genre)
|
||||
//.Include(a => a.Artist)
|
||||
.ToPagedListAsync(page, pageSize, sortBy, a => a.Title);
|
||||
.ToPagedListAsync(page, pageSize, sortBy,
|
||||
a => a.Title, // sortExpression
|
||||
SortDirection.Ascending, // defaultSortDirection
|
||||
a => SimpleMapper.Map(a, new AlbumResultDto())); // selector
|
||||
|
||||
return Json(albums);
|
||||
}
|
||||
|
|
@ -36,7 +44,7 @@ namespace MusicStore.Apis
|
|||
.OrderBy(a => a.Title)
|
||||
.ToListAsync();
|
||||
|
||||
return Json(albums);
|
||||
return Json(albums.Select(a => SimpleMapper.Map(a, new AlbumResultDto())));
|
||||
}
|
||||
|
||||
[HttpGet("mostPopular")]
|
||||
|
|
@ -48,60 +56,70 @@ namespace MusicStore.Apis
|
|||
.Take(count)
|
||||
.ToListAsync();
|
||||
|
||||
return Json(albums);
|
||||
// TODO: Move the .Select() to end of albums query when EF supports it
|
||||
return Json(albums.Select(a => SimpleMapper.Map(a, new AlbumResultDto())));
|
||||
}
|
||||
|
||||
[HttpGet("{albumId:int}")]
|
||||
public async Task<ActionResult> Details(int albumId)
|
||||
{
|
||||
// TODO: Remove this when EF supports related entity loading
|
||||
await _storeContext.Artists.ToListAsync();
|
||||
await _storeContext.Genres.ToListAsync();
|
||||
await _storeContext.Genres.LoadAsync();
|
||||
await _storeContext.Artists.LoadAsync();
|
||||
|
||||
// TODO: Make async when EF supports SingleOrDefaultAsync
|
||||
var album = _storeContext.Albums
|
||||
var album = await _storeContext.Albums
|
||||
//.Include(a => a.Artist)
|
||||
//.Include(a => a.Genre)
|
||||
.SingleOrDefault(a => a.AlbumId == albumId);
|
||||
.Where(a => a.AlbumId == albumId)
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
var albumResult = SimpleMapper.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;
|
||||
//album.Genre.Name = (await _storeContext.Genres.SingleOrDefaultAsync(g => g.GenreId == album.GenreId)).Name;
|
||||
|
||||
// TODO: Add null checking and return 404 in that case
|
||||
|
||||
return Json(album);
|
||||
return Json(albumResult);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize("app-ManageStore", "Allowed")]
|
||||
public async Task<ActionResult> CreateAlbum()
|
||||
public async Task<ActionResult> CreateAlbum([FromBody]AlbumChangeDto album)
|
||||
{
|
||||
var album = new Album();
|
||||
|
||||
//if (!await TryUpdateModelAsync(album, excludeProperties: new[] { "Genre", "Artist", "OrderDetails" }))
|
||||
if (!await TryUpdateModelAsync(album))
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
// Return the model errors
|
||||
return new ApiResult(ModelState);
|
||||
}
|
||||
|
||||
// Save the changes to the DB
|
||||
await _storeContext.Albums.AddAsync(album);
|
||||
var dbAlbum = new Album();
|
||||
await _storeContext.Albums.AddAsync(SimpleMapper.Map(album, dbAlbum));
|
||||
await _storeContext.SaveChangesAsync();
|
||||
|
||||
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
||||
|
||||
return new ApiResult
|
||||
{
|
||||
Data = album.AlbumId,
|
||||
Data = dbAlbum.AlbumId,
|
||||
Message = "Album created successfully."
|
||||
};
|
||||
}
|
||||
|
||||
[HttpPut("{albumId:int}/update")]
|
||||
[Authorize("app-ManageStore", "Allowed")]
|
||||
public async Task<ActionResult> UpdateAlbum(int albumId)
|
||||
public async Task<ActionResult> UpdateAlbum(int albumId, [FromBody]AlbumChangeDto album)
|
||||
{
|
||||
var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId);
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
// Return the model errors
|
||||
return new ApiResult(ModelState);
|
||||
}
|
||||
|
||||
if (album == null)
|
||||
var dbAlbum = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId);
|
||||
|
||||
if (dbAlbum == null)
|
||||
{
|
||||
return new ApiResult
|
||||
{
|
||||
|
|
@ -110,14 +128,8 @@ namespace MusicStore.Apis
|
|||
};
|
||||
}
|
||||
|
||||
//if (!TryUpdateModel(album, prefix: null, includeProperties: null, excludeProperties: new[] { "Genre", "Artist", "OrderDetails" }))
|
||||
if (!await TryUpdateModelAsync(album))
|
||||
{
|
||||
// Return the model errors
|
||||
return new ApiResult(ModelState);
|
||||
}
|
||||
|
||||
// Save the changes to the DB
|
||||
SimpleMapper.Map(album, dbAlbum);
|
||||
await _storeContext.SaveChangesAsync();
|
||||
|
||||
// TODO: Handle missing record, key violations, concurrency issues, etc.
|
||||
|
|
@ -132,8 +144,8 @@ namespace MusicStore.Apis
|
|||
[Authorize("app-ManageStore", "Allowed")]
|
||||
public async Task<ActionResult> DeleteAlbum(int albumId)
|
||||
{
|
||||
//var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId);
|
||||
var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId);
|
||||
var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId);
|
||||
//var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId);
|
||||
|
||||
if (album != null)
|
||||
{
|
||||
|
|
@ -150,5 +162,44 @@ namespace MusicStore.Apis
|
|||
Message = "Album deleted successfully."
|
||||
};
|
||||
}
|
||||
|
||||
[BuddyType(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()
|
||||
{
|
||||
Artist = new ArtistResultDto();
|
||||
Genre = new GenreResultDto();
|
||||
}
|
||||
|
||||
public int AlbumId { get; set; }
|
||||
|
||||
public ArtistResultDto Artist { get; private set; }
|
||||
|
||||
public GenreResultDto Genre { get; private set; }
|
||||
}
|
||||
|
||||
public class ArtistResultDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class GenreResultDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
|
||||
public static HtmlString ngTextBoxFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, IDictionary<string, object> htmlAttributes)
|
||||
{
|
||||
var helper = html as AngularHtmlHelper<TModel>;
|
||||
if (helper == null)
|
||||
{
|
||||
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
||||
}
|
||||
|
||||
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, helper.ViewData, helper.ModelMetadataProvider);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, html.MetadataProvider);
|
||||
var ngAttributes = new Dictionary<string, object>();
|
||||
|
||||
ngAttributes["type"] = "text";
|
||||
|
|
@ -92,9 +86,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
}
|
||||
|
||||
// Add attributes for Angular validation
|
||||
//var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext)
|
||||
// .SelectMany(v => v.GetClientValidationRules());
|
||||
var clientValidators = helper.GetClientValidators(null, metadata);
|
||||
var clientValidators = html.GetClientValidationRules(metadata, null);
|
||||
|
||||
foreach (var validator in clientValidators)
|
||||
{
|
||||
|
|
@ -222,15 +214,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
|
||||
public static HtmlString ngDropDownListFor<TModel, TProperty, TDisplayProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> propertyExpression, Expression<Func<TModel, TDisplayProperty>> displayExpression, string source, string nullOption, IDictionary<string, object> htmlAttributes)
|
||||
{
|
||||
var helper = html as AngularHtmlHelper<TModel>;
|
||||
if (helper == null)
|
||||
{
|
||||
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
||||
}
|
||||
|
||||
var propertyExpressionText = ExpressionHelper.GetExpressionText(propertyExpression);
|
||||
var displayExpressionText = ExpressionHelper.GetExpressionText(displayExpression);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(propertyExpression, helper.ViewData, helper.ModelMetadataProvider);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(propertyExpression, html.ViewData, html.MetadataProvider);
|
||||
var tag = new TagBuilder("select");
|
||||
|
||||
var valueFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyExpressionText);
|
||||
|
|
@ -255,7 +241,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
tag.InnerHtml = nullOptionTag.ToString();
|
||||
}
|
||||
|
||||
var clientValidators = helper.GetClientValidators(null, metadata);
|
||||
var clientValidators = html.GetClientValidationRules(metadata, null);
|
||||
var isRequired = clientValidators.SingleOrDefault(cv => string.Equals(cv.ValidationType, "required", StringComparison.OrdinalIgnoreCase)) != null;
|
||||
if (isRequired)
|
||||
{
|
||||
|
|
@ -279,19 +265,13 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
|
||||
public static HtmlString ngValidationMessageFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string formName, IDictionary<string, object> htmlAttributes)
|
||||
{
|
||||
var helper = html as AngularHtmlHelper<TModel>;
|
||||
if (helper == null)
|
||||
{
|
||||
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
||||
}
|
||||
|
||||
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, helper.ModelMetadataProvider);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, html.MetadataProvider);
|
||||
var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
|
||||
|
||||
//var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext)
|
||||
// .SelectMany(v => v.GetClientValidationRules());
|
||||
var clientValidators = helper.GetClientValidators(null, metadata);
|
||||
var clientValidators = html.GetClientValidationRules(metadata, null);
|
||||
var tags = new List<TagBuilder>();
|
||||
|
||||
// Get validation messages from data type
|
||||
|
|
@ -334,14 +314,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
|||
|
||||
public static string ngValidationClassFor<TModel, TProperty>(this IHtmlHelper<TModel> html, Expression<Func<TModel, TProperty>> expression, string formName, string className)
|
||||
{
|
||||
var helper = html as AngularHtmlHelper<TModel>;
|
||||
if (helper == null)
|
||||
{
|
||||
throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper<T> for IHtmlHelper<T>.");
|
||||
}
|
||||
|
||||
var expressionText = ExpressionHelper.GetExpressionText(expression);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, helper.ModelMetadataProvider);
|
||||
var metadata = ExpressionMetadataProvider.FromLambdaExpression(expression, html.ViewData, html.MetadataProvider);
|
||||
var modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText);
|
||||
var ngClassFormat = "{{ '{0}' : ({1}.submitAttempted || {1}.{2}.$dirty || {1}.{2}.visited) && {1}.{2}.$invalid }}";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
|
||||
namespace Microsoft.AspNet.Mvc.Rendering
|
||||
{
|
||||
public class AngularHtmlHelper<TModel> : HtmlHelper<TModel>
|
||||
{
|
||||
public AngularHtmlHelper(
|
||||
IHtmlGenerator generator,
|
||||
ICompositeViewEngine viewEngine,
|
||||
IModelMetadataProvider metadataProvider)
|
||||
: base(generator, viewEngine, metadataProvider)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
// TODO: These members are required to give helper extensions access to required protected members
|
||||
|
||||
public IModelMetadataProvider ModelMetadataProvider
|
||||
{
|
||||
get
|
||||
{
|
||||
return MetadataProvider;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ModelClientValidationRule> GetClientValidators(string name, ModelMetadata metadata)
|
||||
{
|
||||
return GetClientValidationRules(metadata, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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<Attribute> 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.SimpleDisplayText = buddyProperty.SimpleDisplayText;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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<Attribute> 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
using System;
|
||||
|
||||
namespace MusicStore.Spa.Infrastructure
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
|
||||
internal sealed class NotNullAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -65,8 +65,15 @@ namespace MusicStore.Infrastructure
|
|||
return new PagedList<T>(data, pagingConfig.Page, pagingConfig.PageSize, query.Count());
|
||||
}
|
||||
|
||||
public static async Task<IPagedList<TModel>> ToPagedListAsync<TModel, TProperty>(this IQueryable<TModel> query, int page, int pageSize, string sortExpression, Expression<Func<TModel, TProperty>> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending)
|
||||
public static Task<IPagedList<TModel>> ToPagedListAsync<TModel, TProperty>(this IQueryable<TModel> query, int page, int pageSize, string sortExpression, Expression<Func<TModel, TProperty>> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending)
|
||||
where TModel : class
|
||||
{
|
||||
return ToPagedListAsync<TModel, TProperty, TModel>(query, page, pageSize, sortExpression, defaultSortExpression, defaultSortDirection, null);
|
||||
}
|
||||
|
||||
public static async Task<IPagedList<TResult>> ToPagedListAsync<TModel, TProperty, TResult>(this IQueryable<TModel> query, int page, int pageSize, string sortExpression, Expression<Func<TModel, TProperty>> defaultSortExpression, SortDirection defaultSortDirection, Func<TModel, TResult> selector)
|
||||
where TModel : class
|
||||
where TResult : class
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
|
|
@ -99,7 +106,11 @@ namespace MusicStore.Infrastructure
|
|||
|
||||
var count = await query.CountAsync();
|
||||
|
||||
return new PagedList<TModel>(data, pagingConfig.Page, pagingConfig.PageSize, count);
|
||||
var resultData = selector != null
|
||||
? data.Select(selector)
|
||||
: data.Cast<TResult>();
|
||||
|
||||
return new PagedList<TResult>(resultData, pagingConfig.Page, pagingConfig.PageSize, count);
|
||||
}
|
||||
|
||||
private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,93 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ namespace MusicStore.Models
|
|||
|
||||
[Required]
|
||||
[StringLength(160, MinimumLength = 2)]
|
||||
[ForcedModelError("fail")]
|
||||
public string Title { get; set; }
|
||||
|
||||
[Required]
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@ namespace MusicStore.Models
|
|||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
// TODO: All this configuration needs to be done manually right now.
|
||||
// We can remove this once EF supports the conventions again.
|
||||
// Configure pluralization
|
||||
builder.Entity<Album>().ForRelational().Table("Albums");
|
||||
builder.Entity<Artist>().ForRelational().Table("Artists");
|
||||
builder.Entity<Order>().ForRelational().Table("Orders");
|
||||
|
|
@ -35,46 +34,16 @@ namespace MusicStore.Models
|
|||
builder.Entity<CartItem>().ForRelational().Table("CartItems");
|
||||
builder.Entity<OrderDetail>().ForRelational().Table("OrderDetails");
|
||||
|
||||
builder.Entity<Album>().Key(a => a.AlbumId);
|
||||
builder.Entity<Artist>().Key(a => a.ArtistId);
|
||||
|
||||
builder.Entity<Order>(b =>
|
||||
{
|
||||
b.Key(o => o.OrderId);
|
||||
b.Property(o => o.OrderId)
|
||||
.ForRelational()
|
||||
.Column("[Order]");
|
||||
});
|
||||
|
||||
builder.Entity<Genre>().Key(g => g.GenreId);
|
||||
builder.Entity<CartItem>().Key(ci => ci.CartItemId);
|
||||
builder.Entity<OrderDetail>().Key(od => od.OrderDetailId);
|
||||
|
||||
// TODO: Remove this when we start using auto generated values
|
||||
builder.Entity<Artist>().Property(a => a.ArtistId).GenerateValueOnAdd(generateValue: false);
|
||||
builder.Entity<Album>().Property(a => a.ArtistId).GenerateValueOnAdd(generateValue: false);
|
||||
builder.Entity<Genre>().Property(g => g.GenreId).GenerateValueOnAdd(generateValue: false);
|
||||
|
||||
builder.Entity<Album>(b =>
|
||||
{
|
||||
b.ForeignKey<Genre>(a => a.GenreId);
|
||||
b.ForeignKey<Artist>(a => a.ArtistId);
|
||||
});
|
||||
|
||||
builder.Entity<OrderDetail>(b =>
|
||||
{
|
||||
b.ForeignKey<Album>(a => a.AlbumId);
|
||||
b.ForeignKey<Order>(a => a.OrderId);
|
||||
});
|
||||
|
||||
var genre = builder.Model.GetEntityType(typeof(Genre));
|
||||
var album = builder.Model.GetEntityType(typeof(Album));
|
||||
var artist = builder.Model.GetEntityType(typeof(Artist));
|
||||
var orderDetail = builder.Model.GetEntityType(typeof(OrderDetail));
|
||||
genre.AddNavigation("Albums", album.ForeignKeys.Single(k => k.ReferencedEntityType == genre), pointsToPrincipal: false);
|
||||
album.AddNavigation("OrderDetails", orderDetail.ForeignKeys.Single(k => k.ReferencedEntityType == album), pointsToPrincipal: false);
|
||||
album.AddNavigation("Genre", album.ForeignKeys.Single(k => k.ReferencedEntityType == genre), pointsToPrincipal: true);
|
||||
album.AddNavigation("Artist", album.ForeignKeys.Single(k => k.ReferencedEntityType == artist), pointsToPrincipal: true);
|
||||
// TODO: Remove this once convention-based relations work again
|
||||
builder.Entity<Album>().ManyToOne(a => a.Artist);
|
||||
builder.Entity<Album>().ManyToOne(a => a.Genre, g => g.Albums);
|
||||
builder.Entity<Order>().OneToMany(o => o.OrderDetails);
|
||||
builder.Entity<Album>().OneToMany(a => a.OrderDetails, od => od.Album);
|
||||
|
||||
base.OnModelCreating(builder);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@
|
|||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'" Label="Configuration">
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<AssemblyName>MusicStore.Spa</AssemblyName>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Label="Configuration">
|
||||
<RootNamespace>MusicStore.Spa</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<DevelopmentServerPort>5101</DevelopmentServerPort>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ using Microsoft.AspNet.Builder;
|
|||
using Microsoft.AspNet.FileSystems;
|
||||
using Microsoft.AspNet.Http;
|
||||
using Microsoft.AspNet.Identity;
|
||||
using Microsoft.AspNet.Mvc;
|
||||
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||
using Microsoft.AspNet.Mvc.Rendering;
|
||||
using Microsoft.AspNet.Routing;
|
||||
using Microsoft.AspNet.Security.Cookies;
|
||||
|
|
@ -11,6 +13,7 @@ using Microsoft.Data.Entity;
|
|||
using Microsoft.Framework.ConfigurationModel;
|
||||
using Microsoft.Framework.DependencyInjection;
|
||||
using MusicStore.Models;
|
||||
using MusicStore.Spa.Infrastructure;
|
||||
|
||||
namespace MusicStore.Spa
|
||||
{
|
||||
|
|
@ -36,6 +39,11 @@ 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()
|
||||
|
|
@ -48,7 +56,7 @@ namespace MusicStore.Spa
|
|||
services.AddDefaultIdentity<MusicStoreContext, ApplicationUser, IdentityRole>(Configuration);
|
||||
|
||||
// Add application services to the service container
|
||||
services.AddTransient(typeof(IHtmlHelper<>), typeof(AngularHtmlHelper<>));
|
||||
//services.AddTransient<IModelMetadataProvider, BuddyModelMetadataProvider>();
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app)
|
||||
|
|
|
|||
|
|
@ -106,6 +106,8 @@ module MusicStore.Admin.Catalog {
|
|||
|
||||
// Reload the view with the new album ID
|
||||
this._location.path("/albums/" + albumId + "/edit").replace();
|
||||
|
||||
// TODO: Should we reload the data from the server?
|
||||
} else {
|
||||
this.alert = alert;
|
||||
this.disabled = false;
|
||||
|
|
|
|||
|
|
@ -1,36 +1,52 @@
|
|||
{
|
||||
"webroot": "wwwroot",
|
||||
"exclude": [ "wwwroot/**/*.*", "bower_components/**/*.*", "node_modules/**/*.*", "grunt/**/*.*" ],
|
||||
"packExclude": [ "bower.json", "package.json", "gruntfile.js", "bower_components/**/*.*", "node_modules/**/*.*", "grunt/**/*.*" ],
|
||||
"authors": [
|
||||
"Microsoft"
|
||||
],
|
||||
"description": "Music store application on K as a SPA",
|
||||
"compilationOptions": { "define": [ "DEBUG" ] },
|
||||
"dependencies": {
|
||||
"Kestrel": "1.0.0-*",
|
||||
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
|
||||
"Microsoft.AspNet.Mvc": "6.0.0-*",
|
||||
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
|
||||
"Microsoft.AspNet.StaticFiles": "1.0.0-*",
|
||||
"EntityFramework.InMemory": "7.0.0-*",
|
||||
"EntityFramework.SqlServer": "7.0.0-*",
|
||||
"Microsoft.AspNet.Security.Cookies": "1.0.0-*",
|
||||
"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-*",
|
||||
"Microsoft.Framework.ConfigurationModel": "1.0.0-*",
|
||||
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*"
|
||||
},
|
||||
"commands": {
|
||||
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
|
||||
"Kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5104",
|
||||
"run": "run server.urls=http://localhost:5103"
|
||||
},
|
||||
"scripts": {
|
||||
"postrestore": [ "npm install" ],
|
||||
"prepare": [ "grunt bower:install" ]
|
||||
},
|
||||
"frameworks": {
|
||||
"aspnet50": { },
|
||||
"aspnetcore50": { }
|
||||
}
|
||||
}
|
||||
"webroot": "wwwroot",
|
||||
"exclude": [
|
||||
"wwwroot",
|
||||
"bower_components",
|
||||
"node_modules",
|
||||
"grunt"
|
||||
],
|
||||
"packExclude": [
|
||||
"bower.json",
|
||||
"package.json",
|
||||
"gruntfile.js",
|
||||
"bower_components",
|
||||
"node_modules",
|
||||
"grunt"
|
||||
],
|
||||
"authors": [
|
||||
"Microsoft"
|
||||
],
|
||||
"description": "Music store application on K as a SPA",
|
||||
"compilationOptions": {
|
||||
"define": [
|
||||
"DEBUG"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"Kestrel": "1.0.0-*",
|
||||
"Microsoft.AspNet.Server.IIS": "1.0.0-*",
|
||||
"Microsoft.AspNet.Mvc": "6.0.0-*",
|
||||
"Microsoft.AspNet.Server.WebListener": "1.0.0-*",
|
||||
"Microsoft.AspNet.StaticFiles": "1.0.0-*",
|
||||
"EntityFramework.InMemory": "7.0.0-*",
|
||||
"EntityFramework.SqlServer": "7.0.0-*",
|
||||
"Microsoft.AspNet.Security.Cookies": "1.0.0-*",
|
||||
"Microsoft.AspNet.Identity.EntityFramework": "3.0.0-*",
|
||||
"Microsoft.Framework.ConfigurationModel": "1.0.0-*",
|
||||
"Microsoft.Framework.ConfigurationModel.Json": "1.0.0-*"
|
||||
},
|
||||
"commands": {
|
||||
"WebListener": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5102",
|
||||
"Kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5104",
|
||||
"run": "run server.urls=http://localhost:5103"
|
||||
},
|
||||
/*"scripts": {
|
||||
"postrestore": [ "npm install" ],
|
||||
"prepare": [ "grunt bower:install" ]
|
||||
},*/
|
||||
"frameworks": {
|
||||
"aspnet50": {},
|
||||
"aspnetcore50": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +1,8 @@
|
|||
/// <autosync enabled="true" />
|
||||
/// <reference path="../wwwroot/Scripts/bootstrap.js" />
|
||||
/// <reference path="../wwwroot/Scripts/jquery-2.0.3.js" />
|
||||
/// <reference path="../wwwroot/Scripts/jquery.signalR-2.0.1.js" />
|
||||
/// <reference path="../wwwroot/Scripts/jquery.validate.js" />
|
||||
/// <reference path="../wwwroot/Scripts/jquery.validate.unobtrusive.js" />
|
||||
/// <reference path="../wwwroot/Scripts/modernizr-2.6.2.js" />
|
||||
/// <reference path="../wwwroot/Scripts/respond.js" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue