diff --git a/MusicStore.sln b/MusicStore.sln index c62bc5a6c8..5e79201722 100644 --- a/MusicStore.sln +++ b/MusicStore.sln @@ -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 diff --git a/src/MusicStore.Spa/Apis/AlbumsApiController.cs b/src/MusicStore.Spa/Apis/AlbumsApiController.cs index 53a207ee21..f92a13952a 100644 --- a/src/MusicStore.Spa/Apis/AlbumsApiController.cs +++ b/src/MusicStore.Spa/Apis/AlbumsApiController.cs @@ -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 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 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 CreateAlbum() + public async Task 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 UpdateAlbum(int albumId) + public async Task 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 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; } + } } } diff --git a/src/MusicStore.Spa/Helpers/AngularExtensions.cs b/src/MusicStore.Spa/Helpers/AngularExtensions.cs index 15e2aa90e3..037c1128cf 100644 --- a/src/MusicStore.Spa/Helpers/AngularExtensions.cs +++ b/src/MusicStore.Spa/Helpers/AngularExtensions.cs @@ -39,14 +39,8 @@ namespace Microsoft.AspNet.Mvc.Rendering public static HtmlString ngTextBoxFor(this IHtmlHelper html, Expression> expression, IDictionary htmlAttributes) { - var helper = html as AngularHtmlHelper; - if (helper == null) - { - throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper for IHtmlHelper."); - } - 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(); 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(this IHtmlHelper html, Expression> propertyExpression, Expression> displayExpression, string source, string nullOption, IDictionary htmlAttributes) { - var helper = html as AngularHtmlHelper; - if (helper == null) - { - throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper for IHtmlHelper."); - } - 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(this IHtmlHelper html, Expression> expression, string formName, IDictionary htmlAttributes) { - var helper = html as AngularHtmlHelper; - if (helper == null) - { - throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper for IHtmlHelper."); - } - 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(); // Get validation messages from data type @@ -334,14 +314,8 @@ namespace Microsoft.AspNet.Mvc.Rendering public static string ngValidationClassFor(this IHtmlHelper html, Expression> expression, string formName, string className) { - var helper = html as AngularHtmlHelper; - if (helper == null) - { - throw new InvalidOperationException("You need to configure the services container to return AngularHtmlHelper for IHtmlHelper."); - } - 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 }}"; diff --git a/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs b/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs deleted file mode 100644 index 069719b10b..0000000000 --- a/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using Microsoft.AspNet.Mvc.ModelBinding; - -namespace Microsoft.AspNet.Mvc.Rendering -{ - public class AngularHtmlHelper : HtmlHelper - { - 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 GetClientValidators(string name, ModelMetadata metadata) - { - return GetClientValidationRules(metadata, name); - } - } -} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/BuddyModelMetadataProvider.cs b/src/MusicStore.Spa/Infrastructure/BuddyModelMetadataProvider.cs new file mode 100644 index 0000000000..1830658e81 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/BuddyModelMetadataProvider.cs @@ -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 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; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/BuddyTypeAttribute.cs b/src/MusicStore.Spa/Infrastructure/BuddyTypeAttribute.cs new file mode 100644 index 0000000000..e758523d98 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/BuddyTypeAttribute.cs @@ -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(); + + if (attribute != null) + { + return attribute.MetadataBuddyType; + } + + return null; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/BuddyValidatorProvider.cs b/src/MusicStore.Spa/Infrastructure/BuddyValidatorProvider.cs new file mode 100644 index 0000000000..61f513ca49 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/BuddyValidatorProvider.cs @@ -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 GetValidators(ModelMetadata metadata, IEnumerable 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(); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/NotNullAttribute.cs b/src/MusicStore.Spa/Infrastructure/NotNullAttribute.cs new file mode 100644 index 0000000000..beb3b8e12d --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/NotNullAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace MusicStore.Spa.Infrastructure +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + internal sealed class NotNullAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/PagedList.cs b/src/MusicStore.Spa/Infrastructure/PagedList.cs index 78bd7d45fd..c08b6970a5 100644 --- a/src/MusicStore.Spa/Infrastructure/PagedList.cs +++ b/src/MusicStore.Spa/Infrastructure/PagedList.cs @@ -65,8 +65,15 @@ namespace MusicStore.Infrastructure return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, query.Count()); } - public static async Task> ToPagedListAsync(this IQueryable query, int page, int pageSize, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) + public static Task> ToPagedListAsync(this IQueryable query, int page, int pageSize, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) where TModel : class + { + return ToPagedListAsync(query, page, pageSize, sortExpression, defaultSortExpression, defaultSortDirection, null); + } + + public static async Task> ToPagedListAsync(this IQueryable query, int page, int pageSize, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection, Func 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(data, pagingConfig.Page, pagingConfig.PageSize, count); + var resultData = selector != null + ? data.Select(selector) + : data.Cast(); + + return new PagedList(resultData, pagingConfig.Page, pagingConfig.PageSize, count); } private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig) diff --git a/src/MusicStore.Spa/Infrastructure/SimpleMapper.cs b/src/MusicStore.Spa/Infrastructure/SimpleMapper.cs new file mode 100644 index 0000000000..609b6ae782 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/SimpleMapper.cs @@ -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, Delegate> _mapCache = new ConcurrentDictionary, Delegate>(); + + public static TDest Map(TSource source, TDest dest) + { + var map = (Func)_mapCache.GetOrAdd(Tuple.Create(typeof(TSource), typeof(TDest)), _ => MakeMapMethod()); + return map(source, dest); + } + + private static Func MakeMapMethod() + { + // 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>(blockExp, srcArg, destArg); + + return map.Compile(); + } + + private static IEnumerable 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(); + + 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; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/Album.cs b/src/MusicStore.Spa/Models/Album.cs index 35f03d9f82..22cc69b4ad 100644 --- a/src/MusicStore.Spa/Models/Album.cs +++ b/src/MusicStore.Spa/Models/Album.cs @@ -20,6 +20,7 @@ namespace MusicStore.Models [Required] [StringLength(160, MinimumLength = 2)] + [ForcedModelError("fail")] public string Title { get; set; } [Required] diff --git a/src/MusicStore.Spa/Models/MusicStoreContext.cs b/src/MusicStore.Spa/Models/MusicStoreContext.cs index 4f56956806..87552d9a3a 100644 --- a/src/MusicStore.Spa/Models/MusicStoreContext.cs +++ b/src/MusicStore.Spa/Models/MusicStoreContext.cs @@ -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().ForRelational().Table("Albums"); builder.Entity().ForRelational().Table("Artists"); builder.Entity().ForRelational().Table("Orders"); @@ -35,46 +34,16 @@ namespace MusicStore.Models builder.Entity().ForRelational().Table("CartItems"); builder.Entity().ForRelational().Table("OrderDetails"); - builder.Entity().Key(a => a.AlbumId); - builder.Entity().Key(a => a.ArtistId); - - builder.Entity(b => - { - b.Key(o => o.OrderId); - b.Property(o => o.OrderId) - .ForRelational() - .Column("[Order]"); - }); - - builder.Entity().Key(g => g.GenreId); - builder.Entity().Key(ci => ci.CartItemId); - builder.Entity().Key(od => od.OrderDetailId); - // TODO: Remove this when we start using auto generated values builder.Entity().Property(a => a.ArtistId).GenerateValueOnAdd(generateValue: false); builder.Entity().Property(a => a.ArtistId).GenerateValueOnAdd(generateValue: false); builder.Entity().Property(g => g.GenreId).GenerateValueOnAdd(generateValue: false); - builder.Entity(b => - { - b.ForeignKey(a => a.GenreId); - b.ForeignKey(a => a.ArtistId); - }); - - builder.Entity(b => - { - b.ForeignKey(a => a.AlbumId); - b.ForeignKey(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().ManyToOne(a => a.Artist); + builder.Entity().ManyToOne(a => a.Genre, g => g.Albums); + builder.Entity().OneToMany(o => o.OrderDetails); + builder.Entity().OneToMany(a => a.OrderDetails, od => od.Album); base.OnModelCreating(builder); } diff --git a/src/MusicStore.Spa/MusicStore.Spa.kproj b/src/MusicStore.Spa/MusicStore.Spa.kproj index f0d4b6c56d..2abbea9491 100644 --- a/src/MusicStore.Spa/MusicStore.Spa.kproj +++ b/src/MusicStore.Spa/MusicStore.Spa.kproj @@ -13,6 +13,12 @@ + + MusicStore.Spa + + + MusicStore.Spa + 2.0 5101 diff --git a/src/MusicStore.Spa/Startup.cs b/src/MusicStore.Spa/Startup.cs index b73074262a..fc23be71e0 100644 --- a/src/MusicStore.Spa/Startup.cs +++ b/src/MusicStore.Spa/Startup.cs @@ -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(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(Configuration); // Add application services to the service container - services.AddTransient(typeof(IHtmlHelper<>), typeof(AngularHtmlHelper<>)); + //services.AddTransient(); } public void Configure(IApplicationBuilder app) diff --git a/src/MusicStore.Spa/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts b/src/MusicStore.Spa/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts index f46fcab8ca..479f71e11c 100644 --- a/src/MusicStore.Spa/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts +++ b/src/MusicStore.Spa/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts @@ -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; diff --git a/src/MusicStore.Spa/project.json b/src/MusicStore.Spa/project.json index 3cfac1bfbb..bdaf3cd6ef 100644 --- a/src/MusicStore.Spa/project.json +++ b/src/MusicStore.Spa/project.json @@ -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": {} + } +} \ No newline at end of file diff --git a/src/MusicStore/Scripts/_references.js b/src/MusicStore/Scripts/_references.js index 3154c1b5fa..1e06d0efc2 100644 --- a/src/MusicStore/Scripts/_references.js +++ b/src/MusicStore/Scripts/_references.js @@ -1 +1,8 @@ /// +/// +/// +/// +/// +/// +/// +///