diff --git a/.gitignore b/.gitignore index f27c267e5a..1250ca4465 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,6 @@ App_Data/ bower_components node_modules *.sln.ide -*/*.Spa/public/* \ No newline at end of file +*/*.Spa/public/* +*/*.Spa/wwwroot/* +*.ng.ts \ No newline at end of file diff --git a/MusicStore.sln b/MusicStore.sln index cb0f0b62f7..fef99b818b 100644 --- a/MusicStore.sln +++ b/MusicStore.sln @@ -11,6 +11,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore", "src\MusicStor EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvcMusicStore.Spa", "src\MvcMusicStore.Spa\MvcMusicStore.Spa.csproj", "{408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore.Spa", "src\MusicStore.Spa\MusicStore.Spa.kproj", "{9BCEB29B-7E09-4B4C-A466-498EFC602331}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,16 @@ Global {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|Mixed Platforms.Build.0 = Release|Any CPU {408AC102-7FB1-4ADD-A16A-9AACBAFFC2F7}.Release|x86.ActiveCfg = Release|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Debug|x86.ActiveCfg = Debug|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Release|Any CPU.Build.0 = Release|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {9BCEB29B-7E09-4B4C-A466-498EFC602331}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/MusicStore.Spa/Apis/AlbumsApiController.cs b/src/MusicStore.Spa/Apis/AlbumsApiController.cs new file mode 100644 index 0000000000..083d81ae27 --- /dev/null +++ b/src/MusicStore.Spa/Apis/AlbumsApiController.cs @@ -0,0 +1,169 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using MusicStore.Infrastructure; +using MusicStore.Models; + +namespace MusicStore.Apis +{ + public class AlbumsApiController : BaseController + { + private readonly MusicStoreContext _storeContext; + + public AlbumsApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + //[Route("api/albums")] + public async Task Paged(int page = 1, int pageSize = 50, string sortBy = null) + { + var pagedAlbums = await _storeContext.Albums + .Include(a => a.Genre) + .Include(a => a.Artist) + .SortBy(sortBy, a => a.Title) + .ToPagedListAsync(page, pageSize); + + return new SmartJsonResult + { + Data = pagedAlbums + }; + } + + //[Route("api/albums/all")] + public async Task All() + { + return new SmartJsonResult + { + Data = await _storeContext.Albums + .Include(a => a.Genre) + .Include(a => a.Artist) + .OrderBy(a => a.Title) + .ToListAsync() + }; + } + + //[Route("api/albums/mostPopular")] + public async Task MostPopular(int count = 6) + { + count = count > 0 && count < 20 ? count : 6; + + return new SmartJsonResult + { + Data = await _storeContext.Albums + .OrderByDescending(a => a.OrderDetails.Count()) + .Take(count) + .ToListAsync() + }; + } + + //[Route("api/albums/{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(); + + // TODO: Make async when EF supports SingleOrDefaultAsync + var album = _storeContext.Albums + .Include(a => a.Artist) + .Include(a => a.Genre) + .SingleOrDefault(a => a.AlbumId == albumId); + + // TODO: Add null checking and return 404 in that case + + return new SmartJsonResult + { + Data = album + }; + } + + //[Route("api/albums")] + [HttpPost] + //[Authorize(Roles = "Administrator")] + [Authorize(ClaimTypes.Role, "Administrator")] + public async Task CreateAlbum() + { + var album = new Album(); + + //if (!await TryUpdateModelAsync(album, excludeProperties: new[] { "Genre", "Artist", "OrderDetails" })) + if (!await TryUpdateModelAsync(album)) + { + // Return the model errors + return new ApiResult(ModelState); + } + + // Save the changes to the DB + await _storeContext.Albums.AddAsync(album); + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Data = album.AlbumId, + Message = "Album created successfully." + }; + } + + //[Route("api/albums/{albumId:int}/update")] + [HttpPut] + //[Authorize(Roles = "Administrator")] + [Authorize(ClaimTypes.Role, "Administrator")] + public async Task UpdateAlbum(int albumId) + { + var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId); + + if (album == null) + { + return new ApiResult + { + StatusCode = 404, + Message = string.Format("The album with ID {0} was not found.", albumId) + }; + } + + //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 + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Message = "Album updated successfully." + }; + } + + //[Route("api/albums/{albumId:int}")] + [HttpDelete] + //[Authorize(Roles = "Administrator")] + [Authorize(ClaimTypes.Role, "Administrator")] + public async Task DeleteAlbum(int albumId) + { + var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); + + if (album != null) + { + _storeContext.Albums.Remove(album); + + // Save the changes to the DB + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + } + + return new ApiResult + { + Message = "Album deleted successfully." + }; + } + } +} diff --git a/src/MusicStore.Spa/Apis/ArtistsApiController.cs b/src/MusicStore.Spa/Apis/ArtistsApiController.cs new file mode 100644 index 0000000000..ff6b34f49c --- /dev/null +++ b/src/MusicStore.Spa/Apis/ArtistsApiController.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using MusicStore.Models; + +namespace MusicStore.Apis +{ + public class ArtistsApiController : BaseController + { + private readonly MusicStoreContext _storeContext; + + public ArtistsApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + //[Route("api/artists/lookup")] + public async Task Lookup() + { + return new SmartJsonResult + { + Data = await _storeContext.Artists.OrderBy(a => a.Name).ToListAsync() + }; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Apis/GenresApiController.cs b/src/MusicStore.Spa/Apis/GenresApiController.cs new file mode 100644 index 0000000000..e2e155e6b1 --- /dev/null +++ b/src/MusicStore.Spa/Apis/GenresApiController.cs @@ -0,0 +1,70 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using MusicStore.Models; + +namespace MusicStore.Apis +{ + public class GenresApiController : BaseController + { + private readonly MusicStoreContext _storeContext; + + public GenresApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + //[Route("api/genres/lookup")] + public async Task Lookup() + { + return new SmartJsonResult + { + Data = await _storeContext.Genres + .Select(g => new { g.GenreId, g.Name }) + .ToListAsync() + }; + } + + //[Route("api/genres/menu")] + public async Task GenreMenuList(int count = 9) + { + count = count > 0 && count < 20 ? count : 9; + + return new SmartJsonResult + { + Data = await _storeContext.Genres + .OrderByDescending(g => g.Albums.Sum(a => a.OrderDetails.Sum(od => od.Quantity))) + .Take(count) + .ToListAsync() + }; + } + + //[Route("api/genres")] + public async Task GenreList() + { + return new SmartJsonResult + { + Data = await _storeContext.Genres + .Include(g => g.Albums) + .OrderBy(g => g.Name) + .ToListAsync() + }; + } + + //[Route("api/genres/{genreId:int}/albums")] + public async Task GenreAlbums(int genreId) + { + var albums = await _storeContext.Albums + .Where(a => a.GenreId == genreId) + .Include(a => a.Genre) + .Include(a => a.Artist) + //.OrderBy(a => a.Genre.Name) + .ToListAsync(); + + return new SmartJsonResult + { + Data = albums + }; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/Site.less b/src/MusicStore.Spa/Client/Site.less new file mode 100644 index 0000000000..64438d2afd --- /dev/null +++ b/src/MusicStore.Spa/Client/Site.less @@ -0,0 +1,82 @@ +@import '../bower_components/bootstrap/less/bootstrap.less'; + +[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { + display: none !important; +} + +.nav, .pagination, .carousel, .panel-title a { + cursor: pointer; +} + +body { + padding-top: 50px; + padding-bottom: 20px; +} + +/* Set padding to keep content from hitting the edges */ +.body-content { + padding-left: 15px; + padding-right: 15px; +} + +/* Set width on the form input elements since they're 100% wide by default */ +input, +select, +textarea { + /*max-width: 280px;*/ +} + +/* styles for validation helpers */ +.field-validation-error { + color: #b94a48; +} + +.field-validation-valid { + display: none; +} + +input.input-validation-error { + border: 1px solid #b94a48; +} + +input[type="checkbox"].input-validation-error { + border: 0 none; +} + +.validation-summary-errors { + color: #b94a48; +} + +.validation-summary-valid { + display: none; +} + + +/* Music Store additions */ + +ul#album-list li { + height: 160px; +} + +ul#album-list li img:hover { + box-shadow: 1px 1px 7px #777; +} + +ul#album-list li img { + max-width: 100px; + max-height: 100px; + box-shadow: 1px 1px 5px #999; + border: none; + padding: 0; +} + +ul#album-list li a, ul#album-details li a { + text-decoration:none; +} + +ul#album-list li a:hover { + background: none; + -webkit-text-shadow: 1px 1px 2px #bbb; + text-shadow: 1px 1px 2px #bbb; + color: #363430; +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/favicon.ico b/src/MusicStore.Spa/Client/favicon.ico new file mode 100644 index 0000000000..a3a799985c Binary files /dev/null and b/src/MusicStore.Spa/Client/favicon.ico differ diff --git a/src/MusicStore.Spa/Client/images/home-showcase.png b/src/MusicStore.Spa/Client/images/home-showcase.png new file mode 100644 index 0000000000..258c19d3cd Binary files /dev/null and b/src/MusicStore.Spa/Client/images/home-showcase.png differ diff --git a/src/MusicStore.Spa/Client/images/logo.png b/src/MusicStore.Spa/Client/images/logo.png new file mode 100644 index 0000000000..d334c86256 Binary files /dev/null and b/src/MusicStore.Spa/Client/images/logo.png differ diff --git a/src/MusicStore.Spa/Client/images/logo.svg b/src/MusicStore.Spa/Client/images/logo.svg new file mode 100644 index 0000000000..ec3cd6aa5b --- /dev/null +++ b/src/MusicStore.Spa/Client/images/logo.svg @@ -0,0 +1,303 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MusicStore.Spa/Client/images/placeholder.png b/src/MusicStore.Spa/Client/images/placeholder.png new file mode 100644 index 0000000000..1f73dbb43d Binary files /dev/null and b/src/MusicStore.Spa/Client/images/placeholder.png differ diff --git a/src/MusicStore.Spa/Client/images/placeholder.svg b/src/MusicStore.Spa/Client/images/placeholder.svg new file mode 100644 index 0000000000..07d58202df --- /dev/null +++ b/src/MusicStore.Spa/Client/images/placeholder.svg @@ -0,0 +1,112 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml new file mode 100644 index 0000000000..c33efb494e --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml @@ -0,0 +1,24 @@ + + + \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts new file mode 100644 index 0000000000..9e711d12af --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModalController.ts @@ -0,0 +1,30 @@ +module MusicStore.Admin.Catalog { + export interface IAlbumDeleteModalViewModel { + album: Models.IAlbum; + ok(); + cancel(); + } + + // We don't register this controller with Angular's DI system because the $modal service + // will create and resolve its dependencies directly + + //@NgController(skip=true) + export class AlbumDeleteModalController implements IAlbumDeleteModalViewModel { + private _modalInstance: ng.ui.bootstrap.IModalServiceInstance; + + constructor($modalInstance: ng.ui.bootstrap.IModalServiceInstance, album: Models.IAlbum) { + this._modalInstance = $modalInstance; + this.album = album; + } + + public album: Models.IAlbum; + + public ok() { + this._modalInstance.close(true); + } + + public cancel() { + this._modalInstance.dismiss("cancel"); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml new file mode 100644 index 0000000000..43060109d1 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml @@ -0,0 +1,58 @@ +@model MvcMusicStore.Models.Album + +
+

Album Details

+
+ +
+
+ @Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Artist.Name }}

+
+
+ +
+ @Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Genre.Name }}

+
+
+ +
+ @Html.LabelFor(m => m.Title, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Title }}

+
+
+ +
+ @Html.LabelFor(m => m.Price, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.Price | currency }}

+
+
+ +
+ @Html.LabelFor(m => m.AlbumArtUrl, new { @class = "col-md-2 control-label" }) +
+

{{ viewModel.album.AlbumArtUrl }}

+
+
+ +
+ +
+

Album Art

+
+
+ +
+
+ Edit + + Back to List +
+
+
+
diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts new file mode 100644 index 0000000000..d70c340470 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumDetailsController.ts @@ -0,0 +1,58 @@ +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + deleteAlbum(); + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _albumApi: AlbumApi.IAlbumApiService; + private _viewAlert: ViewAlert.IViewAlertService; + + constructor($routeParams: IAlbumDetailsRouteParams, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService) { + + this._modal = $modal; + this._location = $location; + this._albumApi = albumApi; + this._viewAlert = viewAlert; + + albumApi.getAlbumDetails($routeParams.albumId).then(album => this.album = album); + } + + public album: Models.IAlbum; + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml new file mode 100644 index 0000000000..f997c2def4 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml @@ -0,0 +1,99 @@ +@model MvcMusicStore.Models.Album + +
+

Album {{ viewModel.mode | titlecase }}

+
+ + + {{ viewModel.alert.message }} +
    +
  • {{ modelError.ErrorMessage }}
  • +
+
+ +
+
+ @Html.LabelFor(m => m.Artist, new { @class = "col-md-2 control-label" }) +
+
+
+ @Html.ngDropDownListFor(m => m.ArtistId, m => m.Artist.Name, source: "viewModel.artists", nullOption: "-- choose Artist --", + htmlAttributes: new { @class = "form-control", ng_model = "viewModel.album.ArtistId", ng_disabled = "viewModel.disabled || viewModel.artists.length < 2" }) +
+
+ @Html.ngValidationMessageFor(m => m.ArtistId, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Genre, new { @class = "col-md-2 control-label" }) +
+
+
+ @Html.ngDropDownListFor(m => m.GenreId, m => m.Genre.Name, source: "viewModel.genres", nullOption: "-- choose Genre --", + htmlAttributes: new { @class = "form-control", ng_model = "viewModel.album.GenreId", ng_disabled = "viewModel.disabled || viewModel.genres.length < 2" }) +
+
+ @Html.ngValidationMessageFor(m => m.GenreId, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Title, new { @class = "control-label col-md-2" }) +
+
+
+ @Html.ngTextBoxFor(m => m.Title, new { @class = "form-control", ng_model = "viewModel.album.Title", ng_disabled = "viewModel.disabled" }) +
+
+ @Html.ngValidationMessageFor(model => model.Title, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" }) +
+
+
+
+ $ + @Html.ngTextBoxFor(m => m.Price, new { @class = "form-control", ng_model = "viewModel.album.Price", ng_disabled = "viewModel.disabled" }) +
+
+
+ @Html.ngValidationMessageFor(model => model.Price, "editAlbum", new { @class = "help-block field-validation-error" }) +
+
+ +
+ @Html.LabelFor(m => m.AlbumArtUrl, new { @class = "control-label col-md-2" }) +
+
+
+ @Html.ngTextBoxFor(m => m.AlbumArtUrl, new { @class = "form-control", ng_model = "viewModel.album.AlbumArtUrl", ng_disabled = "viewModel.disabled" }) +
+
+ @Html.ngValidationMessageFor(model => model.AlbumArtUrl, "editAlbum", new { @class = "field-validation-error" }) +
+
+ +
+
+ Album Art +
+
+ +
+
+ + + Back to List +
+
+
+
\ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts new file mode 100644 index 0000000000..f46fcab8ca --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumEditController.ts @@ -0,0 +1,188 @@ +module MusicStore.Admin.Catalog { + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + mode: string; + albumId: number; + } + + interface IAlbumDetailsViewModel { + mode: string; // edit or new + disabled: boolean; + album: Models.IAlbum; + alert: Models.IAlert; + artists: Array; + genres: Array; + save(); + clearAlert(); + } + + class AlbumEditController implements IAlbumDetailsViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _artistApi: ArtistApi.IArtistApiService; + private _genreApi: GenreApi.IGenreApiService; + private _viewAlert: ViewAlert.IViewAlertService; + private _modal: ng.ui.bootstrap.IModalService; + private _location: ng.ILocationService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor($routeParams: IAlbumDetailsRouteParams, + albumApi: AlbumApi.IAlbumApiService, + artistApi: ArtistApi.IArtistApiService, + genreApi: GenreApi.IGenreApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $location: ng.ILocationService, + $timeout: ng.ITimeoutService, + $q: ng.IQService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._artistApi = artistApi; + this._genreApi = genreApi; + this._viewAlert = viewAlert; + this._modal = $modal; + this._location = $location; + this._timeout = $timeout; + this._log = $log; + + this.mode = $routeParams.mode; + + this.alert = viewAlert.alert; + + artistApi.getArtistsLookup().then(artists => this.artists = artists); + genreApi.getGenresLookup().then(genres => this.genres = genres); + + if (this.mode.toLowerCase() === "edit") { + // TODO: Handle album load failure + albumApi.getAlbumDetails($routeParams.albumId).then(album => { + this.album = album; + + // Pre-load the lookup arrays with the current values if not set yet + this.genres = this.genres || [album.Genre]; + this.artists = this.artists || [album.Artist]; + + this.disabled = false; + }); + } else { + this.disabled = false; + } + } + + public mode: string; + + public disabled = true; + + public album: Models.IAlbum; + + public alert: Models.IAlert; + + public artists: Array; + + public genres: Array; + + public save() { + this.disabled = true; + + var apiMethod = this.mode.toLowerCase() === "edit" ? this._albumApi.updateAlbum : this._albumApi.createAlbum; + apiMethod = apiMethod.bind(this._albumApi); + + apiMethod(this.album).then( + // Success + response => { + var alert = { + type: Models.AlertType.success, + message: response.data.Message + }; + + // TODO: Do we need to destroy this timeout on controller unload? + this._timeout(() => this.alert !== alert || this.clearAlert(), 3000); + + if (this.mode.toLowerCase() === "new") { + this._log.info("Created album successfully!"); + + var albumId: number = response.data.Data; + + this._viewAlert.alert = alert; + + // Reload the view with the new album ID + this._location.path("/albums/" + albumId + "/edit").replace(); + } else { + this.alert = alert; + this.disabled = false; + this._log.info("Updated album " + this.album.AlbumId + " successfully!"); + } + }, + // Error + response => { + // TODO: Make this common logic, e.g. base controller class, injected helper service, etc. + if (response.status === 400) { + // We made a bad request + if (response.data && response.data.ModelErrors) { + // The server says the update failed validation + // TODO: Map errors back to client validators and/or summary + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message, + modelErrors: response.data.ModelErrors + }; + this.disabled = false; + } else { + // Some other bad request, just show the message + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } + } else if (response.status === 404) { + // The album wasn't found, probably deleted. Leave the form disabled and show error message. + this.alert = { + type: Models.AlertType.danger, + message: response.data.Message + }; + } else if (response.status === 401) { + // We need to authenticate again + // TODO: Should we just redirect to login page, show a message with a link, or something else + this.alert = { + type: Models.AlertType.danger, + message: "Your session has timed out. Please log in and try again." + }; + } else if (!response.status) { + // Request timed out or no response from server or worse + this._log.error("Error updating album " + this.album.AlbumId); + this._log.error(response); + this.alert = { type: Models.AlertType.danger, message: "An unexpected error occurred. Please try again." }; + this.disabled = false; + } + }); + } + + public deleteAlbum() { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => this.album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(this.album.AlbumId).then(result => { + // Navigate back to the list + this._viewAlert.alert = { + type: Models.AlertType.success, + message: result.data.Message + }; + this._location.path("/albums").replace(); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml new file mode 100644 index 0000000000..14d945d6d2 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml @@ -0,0 +1,77 @@ +@model MvcMusicStore.Models.Album + +
+

Albums

+

+ Create new +

+ + + {{ viewModel.alert.message }} +
    +
  • {{ modelError.ErrorMessage }}
  • +
+
+ + + + + + + + + + + + + + + + + + + + +
+ @Html.DisplayNameFor(m => m.Genre) + + + + + @Html.DisplayNameFor(m => m.Artist) + + + + + @Html.DisplayNameFor(m => m.Title) + + + + + @Html.DisplayNameFor(m => m.Price) + + + +
+ {{ album.Genre.Name }} + + {{ album.Artist.Name | truncate:25 }} + + {{ album.Title | truncate:25 }} + + {{ album.Price | currency }} + +
+ Details + Edit + Delete +
+
+ +

+ {{ viewModel.totalCount }} total albums +

+
\ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts new file mode 100644 index 0000000000..420e32c4c9 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/Catalog/AlbumListController.ts @@ -0,0 +1,123 @@ +module MusicStore.Admin.Catalog { + interface IAlbumListViewModel { + albums: Array; + totalCount: number; + currentPage: number; + pageSize: number; + loadPage(page?: number); + deleteAlbum(album: Models.IAlbum); + clearAlert(); + } + + class AlbumListController implements IAlbumListViewModel { + private _albumApi: AlbumApi.IAlbumApiService; + private _modal: ng.ui.bootstrap.IModalService; + private _timeout: ng.ITimeoutService; + private _log: ng.ILogService; + + constructor(albumApi: AlbumApi.IAlbumApiService, + viewAlert: ViewAlert.IViewAlertService, + $modal: ng.ui.bootstrap.IModalService, + $timeout: ng.ITimeoutService, + $log: ng.ILogService) { + + this._albumApi = albumApi; + this._modal = $modal; + this._timeout = $timeout; + this._log = $log; + + this.currentPage = 1; + this.pageSize = 50; + this.loadPage(1); + this.sortColumn = "Title"; + + this.showAlert(viewAlert.alert, 3000); + viewAlert.alert = null; + } + + public alert: Models.IAlert; + + public albums: Array; + + public totalCount: number; + + public currentPage: number; + + public pageSize: number; + + public sortColumn: string; + + public sortDescending: boolean; + + public loadPage(page?: number) { + page = page || this.currentPage; + var sortByExpression = this.getSortByExpression(); + this._albumApi.getAlbums(page, this.pageSize, sortByExpression).then(result => { + this.albums = result.Data; + this.currentPage = result.Page; + this.totalCount = result.TotalCount; + }); + } + + public sortBy(column: string) { + if (this.sortColumn === column) { + // Just flip the direction + this.sortDescending = !this.sortDescending; + } else { + this.sortColumn = column; + this.sortDescending = false; + } + + this.loadPage(); + } + + public deleteAlbum(album: Models.IAlbum) { + var deleteModal = this._modal.open({ + templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDeleteModal.cshtml", + controller: "MusicStore.Admin.Catalog.AlbumDeleteModalController as viewModel", + resolve: { + album: () => album + } + }); + + deleteModal.result.then(shouldDelete => { + if (!shouldDelete) { + return; + } + + this._albumApi.deleteAlbum(album.AlbumId).then(result => { + this.loadPage(); + + this.showAlert({ + type: Models.AlertType.success, + message: result.data.Message + }, 3000); + }); + }); + } + + public clearAlert() { + this.alert = null; + } + + private showAlert(alert: Models.IAlert, closeAfter?: number) { + if (!alert) { + return; + } + + this.alert = alert; + + // TODO: Do we need to destroy this timeout on controller unload? + if (closeAfter) { + this._timeout(() => this.alert !== alert || this.clearAlert(), closeAfter); + } + } + + private getSortByExpression() { + if (this.sortDescending) { + return this.sortColumn + " DESC"; + } + return this.sortColumn; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts new file mode 100644 index 0000000000..ab415980c8 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Admin/MusicStore.Admin.app.ts @@ -0,0 +1,43 @@ +/// + +module MusicStore.Admin { + + var dependencies = [ + "ngRoute", + "ui.bootstrap", + MusicStore.InlineData, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.Visited, + MusicStore.TitleCase, + MusicStore.Truncate, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.ArtistApi, + MusicStore.ViewAlert, + MusicStore.Admin.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + // Configure routes + $routeProvider + .when("/albums/:albumId/details", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumDetails.cshtml" }) + .when("/albums/:albumId/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums/:mode", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumEdit.cshtml" }) + .when("/albums", { templateUrl: "ng-apps/MusicStore.Admin/Catalog/AlbumList.cshtml" }) + .otherwise({ redirectTo: "/albums" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + //function BUG:run() { + + //} +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html new file mode 100644 index 0000000000..1a5be7674f --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetails.html @@ -0,0 +1,26 @@ +
+

{{ viewModel.album.Title }}

+ +

+ +

+ +
+

+ Genre: + {{ viewModel.album.Genre.Name }} +

+

+ Artist: + {{ viewModel.album.Artist.Name }} +

+

+ Price: + {{ viewModel.album.Price | currency }} +

+

+ + Add to cart +

+
+
\ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts new file mode 100644 index 0000000000..4f9c52f651 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/AlbumDetailsController.ts @@ -0,0 +1,22 @@ +module MusicStore.Store.Catalog { + interface IAlbumDetailsViewModel { + album: Models.IAlbum; + } + + interface IAlbumDetailsRouteParams extends ng.route.IRouteParamsService { + albumId: number; + } + + class AlbumDetailsController implements IAlbumDetailsViewModel { + public album: Models.IAlbum; + + constructor($routeParams: IAlbumDetailsRouteParams, albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this, + albumId = $routeParams.albumId; + + albumApi.getAlbumDetails(albumId).then(album => { + viewModel.album = album; + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html new file mode 100644 index 0000000000..203d2e4164 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetails.html @@ -0,0 +1,12 @@ +
+

{{ viewModel.genre.Name }} Albums

+ + +
\ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts new file mode 100644 index 0000000000..f31d158e9a --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreDetailsController.ts @@ -0,0 +1,21 @@ +module MusicStore.Store.Catalog { + interface IGenreDetailsViewModel { + albums: Array; + } + + interface IGenreDetailsRouteParams extends ng.route.IRouteParamsService { + genreId: number; + } + + class GenreDetailsController implements IGenreDetailsViewModel { + public albums: Array; + + constructor($routeParams: IGenreDetailsRouteParams, genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenreAlbums($routeParams.genreId).success(result => { + viewModel.albums = result; + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html new file mode 100644 index 0000000000..0c78a4bb60 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreList.html @@ -0,0 +1,12 @@ +
+

Browse Genres

+ +

+ Select from {{ viewModel.genres.length }} genres: +

+ +
\ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts new file mode 100644 index 0000000000..1f187cf204 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Catalog/GenreListController.ts @@ -0,0 +1,17 @@ +module MusicStore.Store.Catalog { + interface IGenreListViewModel { + genres: Array; + } + + class GenreListController implements IGenreListViewModel { + public genres: Array; + + constructor(genreApi: GenreApi.IGenreApiService) { + var viewModel = this; + + genreApi.getGenresList().success(function (genres) { + viewModel.genres = genres; + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html new file mode 100644 index 0000000000..ca35168092 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/Home.html @@ -0,0 +1,15 @@ +
+

MVC Music Store

+ +
+ + \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts new file mode 100644 index 0000000000..fb17e77da1 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/Home/HomeController.ts @@ -0,0 +1,17 @@ +module MusicStore.Store.Home { + interface IHomeViewModel { + albums: Array + } + + class HomeController implements IHomeViewModel { + public albums: Array; + + constructor(albumApi: AlbumApi.IAlbumApiService) { + var viewModel = this; + + albumApi.getMostPopularAlbums().then(albums => { + viewModel.albums = albums; + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts new file mode 100644 index 0000000000..bcd82fca00 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/MusicStore.Store/MusicStore.Store.app.ts @@ -0,0 +1,39 @@ +/// + +module MusicStore.Store { + + var dependencies = [ + "ngRoute", + MusicStore.InlineData, + MusicStore.PreventSubmit, + MusicStore.GenreMenu, + MusicStore.UrlResolver, + MusicStore.UserDetails, + MusicStore.LoginLink, + MusicStore.GenreApi, + MusicStore.AlbumApi, + MusicStore.Visited, + MusicStore.Store.Home, + MusicStore.Store.Catalog + ]; + + // Use this method to register work which needs to be performed on module loading. + // Note only providers can be injected as dependencies here. + function configuration($routeProvider: ng.route.IRouteProvider, $logProvider: ng.ILogProvider) { + // TODO: Enable debug logging based on server config + // TODO: Capture all logged errors and send back to server + $logProvider.debugEnabled(true); + + $routeProvider + .when("/", { templateUrl: "ng-apps/MusicStore.Store/Home/Home.html" }) + .when("/albums/genres", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreList.html" }) + .when("/albums/genres/:genreId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/GenreDetails.html" }) + .when("/albums/:albumId", { templateUrl: "ng-apps/MusicStore.Store/Catalog/AlbumDetails.html" }) + .otherwise({ redirectTo: "/" }); + } + + // Use this method to register work which should be performed when the injector is done loading all modules. + function run($log: ng.ILogService, userDetails: UserDetails.IUserDetailsService) { + $log.log(userDetails.getUserDetails()); + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts b/src/MusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts new file mode 100644 index 0000000000..d68bfd607d --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/AlbumApi/AlbumApiService.ts @@ -0,0 +1,98 @@ +module MusicStore.AlbumApi { + export interface IAlbumApiService { + getAlbums(page?: number, pageSize?: number, sortBy?: string): ng.IPromise>; + getAlbumDetails(albumId: number): ng.IPromise; + getMostPopularAlbums(count?: number): ng.IPromise>; + createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig): ng.IHttpPromise; + deleteAlbum(albumId: number, config?: ng.IRequestConfig): ng.IHttpPromise; + } + + class AlbumApiService implements IAlbumApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getAlbums(page?: number, pageSize?: number, sortBy?: string) { + var url = this._urlResolver.resolveUrl("~/api/albums"), + query: any = {}, + querySeparator = "?", + inlineData; + + if (page) { + query.page = page; + } + + if (pageSize) { + query.pageSize = pageSize; + } + + if (sortBy) { + query.sortBy = sortBy; + } + + for (var key in query) { + if (query.hasOwnProperty(key)) { + url += querySeparator + key + "=" + encodeURIComponent(query[key]); + if (querySeparator === "?") { + querySeparator = "&"; + } + } + } + + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getAlbumDetails(albumId: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/" + albumId); + return this._http.get(url).then(result => result.data); + } + + public getMostPopularAlbums(count?: number) { + var url = this._urlResolver.resolveUrl("~/api/albums/mostPopular"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + if (count && count > 0) { + url += "?count=" + count; + } + + return this._http.get(url).then(result => result.data); + } + } + + public createAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums"); + return this._http.post(url, album, config || { timeout: 10000 }); + } + + public updateAlbum(album: Models.IAlbum, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + album.AlbumId + "/update"); + return this._http.put(url, album, config || { timeout: 10000 }); + } + + public deleteAlbum(albumId: number, config?: ng.IRequestConfig) { + var url = this._urlResolver.resolveUrl("api/albums/" + albumId); + return this._http.delete(url, config || { timeout: 10000 }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts b/src/MusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts new file mode 100644 index 0000000000..45a59f3563 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/ArtistApi/ArtistApiService.ts @@ -0,0 +1,33 @@ +module MusicStore.ArtistApi { + export interface IArtistApiService { + getArtistsLookup(): ng.IPromise>; + } + + class ArtistsApiService implements IArtistApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getArtistsLookup() { + var url = this._urlResolver.resolveUrl("~/api/artists/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts b/src/MusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts new file mode 100644 index 0000000000..b92d794161 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/GenreApi/GenreApiService.ts @@ -0,0 +1,57 @@ +module MusicStore.GenreApi { + export interface IGenreApiService { + getGenresLookup(): ng.IPromise>; + getGenresMenu(): ng.IPromise>; + getGenresList(): ng.IHttpPromise>; + getGenreAlbums(genreId: number): ng.IHttpPromise>; + } + + class GenreApiService implements IGenreApiService { + private _inlineData: ng.ICacheObject; + private _q: ng.IQService; + private _http: ng.IHttpService; + private _urlResolver: UrlResolver.IUrlResolverService; + + constructor($cacheFactory: ng.ICacheFactoryService, + $q: ng.IQService, + $http: ng.IHttpService, + urlResolver: UrlResolver.IUrlResolverService) { + this._inlineData = $cacheFactory.get("inlineData"); + this._q = $q; + this._http = $http; + this._urlResolver = urlResolver; + } + + public getGenresLookup() { + var url = this._urlResolver.resolveUrl("~/api/genres/lookup"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresMenu() { + var url = this._urlResolver.resolveUrl("~/api/genres/menu"), + inlineData = this._inlineData ? this._inlineData.get(url) : null; + + if (inlineData) { + return this._q.when(inlineData); + } else { + return this._http.get(url).then(result => result.data); + } + } + + public getGenresList() { + var url = this._urlResolver.resolveUrl("~/api/genres"); + return this._http.get(url); + } + + public getGenreAlbums(genreId: number) { + var url = this._urlResolver.resolveUrl("~/api/genres/" + genreId + "/albums"); + return this._http.get(url); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html new file mode 100644 index 0000000000..888ae81db7 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenu.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts new file mode 100644 index 0000000000..2ccfb0f8b1 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuController.ts @@ -0,0 +1,22 @@ +module MusicStore.GenreMenu { + interface IGenreMenuViewModel { + genres: Array; + urlBase: string; + } + + class GenreMenuController implements IGenreMenuViewModel { + constructor(genreApi: GenreApi.IGenreApiService, urlResolver: UrlResolver.IUrlResolverService) { + var viewModel = this; + + genreApi.getGenresMenu().then(genres => { + viewModel.genres = genres; + }); + + viewModel.urlBase = urlResolver.base; + } + + public genres: Array; + + public urlBase: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts new file mode 100644 index 0000000000..47f9ed2691 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/GenreMenu/GenreMenuDirective.ts @@ -0,0 +1,13 @@ +module MusicStore.GenreMenu { + + //@NgDirective('appGenreMenu') + class GenreMenuDirective implements ng.IDirective { + public replace = true; + public restrict = "A"; + public templateUrl; + + constructor(urlResolver: UrlResolver.IUrlResolverService) { + this.templateUrl = urlResolver.resolveUrl("~/ng-apps/components/GenreMenu/GenreMenu.html"); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts b/src/MusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts new file mode 100644 index 0000000000..ade0a68c1d --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/InlineData/InlineDataDirective.ts @@ -0,0 +1,31 @@ +module MusicStore.InlineData { + interface InlineDataAttributes extends ng.IAttributes { + type: string; + for: string; + } + + //@NgDirective('appInlineData') + class InlineDataDirective implements ng.IDirective { + private _cache: ng.ICacheObject; + private _log: ng.ILogService; + + constructor($cacheFactory: ng.ICacheFactoryService, $log: ng.ILogService) { + this._cache = $cacheFactory.get("inlineData") || $cacheFactory("inlineData"); + this._log = $log; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: InlineDataAttributes) { + var data = attrs.type === "application/json" + ? angular.fromJson(element.text()) + : element.text(); + + this._log.info("appInlineData: Inline data element found for " + attrs.for); + + this._cache.put(attrs.for, data); + + //element.remove(); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts b/src/MusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts new file mode 100644 index 0000000000..051f1acd42 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/LoginLink/LoginLinkDirective.ts @@ -0,0 +1,33 @@ +module MusicStore.LoginLink { + interface LoginLinkAttributes extends ng.IAttributes { + href: string; + } + + //@NgDirective('appLoginLink') + class LoginLinkDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor(urlResolver: UrlResolver.IUrlResolverService, $window: ng.IWindowService) { + this._window = $window; + } + + public restrict = "A"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: LoginLinkAttributes) { + if (!element.is("a[href]")) { + return; + } + + // Grab the original login URL + var loginUrl = attrs.href; + + element.click(event => { + // Update the returnUrl querystring value to current path + var currentUrl = this._window.location.pathname + this._window.location.search + this._window.location.hash, + newUrl = loginUrl + "?returnUrl=" + encodeURIComponent(currentUrl); + + element.prop("href", newUrl); + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts new file mode 100644 index 0000000000..fd8bce8f62 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlbum.ts @@ -0,0 +1,16 @@ +module MusicStore.Models { + export interface IAlbum { + AlbumId: number; + GenreId: number; + ArtistId: number; + + Title: string; + AlbumArtUrl: string; + Price: number; + + Artist: IArtist; + Genre: IGenre; + + DetailsUrl: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts new file mode 100644 index 0000000000..f3f3578ab2 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IAlert.ts @@ -0,0 +1,25 @@ +module MusicStore.Models { + export interface IAlert { + type: AlertType; + message: string; + } + + export interface IModelErrorAlert extends IAlert { + modelErrors: Array; + } + + export class AlertType { + constructor(public value: string) { + } + + public toString() { + return this.value; + } + + // Values + static success = new AlertType("success"); + static info = new AlertType("info"); + static warning = new AlertType("warning"); + static danger = new AlertType("danger"); + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts new file mode 100644 index 0000000000..c69a3ca54b --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IApiResult.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IApiResult { + Message?: string; + Data?: any; + ModelErrors?: Array; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts new file mode 100644 index 0000000000..9d40978f30 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IArtist.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IArtist { + ArtistId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts new file mode 100644 index 0000000000..6b60385fc7 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenre.ts @@ -0,0 +1,7 @@ +module MusicStore.Models { + export interface IGenre { + GenreId: number; + Name: string; + Description: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts new file mode 100644 index 0000000000..1cc50c8e30 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IGenreLookup.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IGenreLookup { + GenreId: number; + Name: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts new file mode 100644 index 0000000000..294ceeb80e --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IModelError.ts @@ -0,0 +1,6 @@ +module MusicStore.Models { + export interface IModelError { + FieldName: string; + ErrorMessage: string; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts new file mode 100644 index 0000000000..e109a6ec1f --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IPagedList.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IPagedList { + Data: Array; + Page: number; + PageSize: number; + TotalCount: number; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts b/src/MusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts new file mode 100644 index 0000000000..ddac9992f4 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Models/IUserDetails.ts @@ -0,0 +1,8 @@ +module MusicStore.Models { + export interface IUserDetails { + isAuthenticated: boolean; + userName: string; + userId: string; + roles: Array; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts b/src/MusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts new file mode 100644 index 0000000000..e2d3423de2 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/PreventSubmit/PreventSubmitDirective.ts @@ -0,0 +1,24 @@ +module MusicStore.PreventSubmit { + interface IPreventSubmitAttributes extends ng.IAttributes { + name: string; + appPreventSubmit: string; + } + + //@NgDirective('appPreventSubmit') + class PreventSubmitDirective implements ng.IDirective { + private _preventSubmit: any; + + public restrict = "A"; + + public link(scope: any, element: ng.IAugmentedJQuery, attrs: IPreventSubmitAttributes) { + // TODO: Just make this directive apply to all
tags and no-op if no action attr + + element.submit(e => { + if (scope.$eval(attrs.appPreventSubmit)) { + e.preventDefault(); + return false; + } + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts b/src/MusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts new file mode 100644 index 0000000000..65d9832c0e --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/TitleCase/TitleCaseFilter.ts @@ -0,0 +1,18 @@ +module MusicStore.TitleCase { + + //@NgFilter('titlecase') + function titleCase(input: string) { + var out = "", + lastChar = ""; + + for (var i = 0; i < input.length; i++) { + out = out + (lastChar === " " || lastChar === "" + ? input.charAt(i).toUpperCase() + : input.charAt(i)); + + lastChar = input.charAt(i); + } + + return out; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts b/src/MusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts new file mode 100644 index 0000000000..554631858e --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Truncate/TruncateFilter.ts @@ -0,0 +1,15 @@ +module MusicStore.Truncate { + + //@NgFilter + function truncate(input: string, length: number) { + if (!input) { + return input; + } + + if (input.length <= length) { + return input; + } else { + return input.substr(0, length).trim() + "…"; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts b/src/MusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts new file mode 100644 index 0000000000..fd47021825 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/UrlResolver/UrlResolverService.ts @@ -0,0 +1,39 @@ +module MusicStore.UrlResolver { + export interface IUrlResolverService { + base: string; + resolveUrl(relativeUrl: string); + } + + class UrlResolverService implements IUrlResolverService { + private _base: string; + + constructor($rootElement: ng.IAugmentedJQuery) { + this._base = $rootElement.attr("data-url-base"); + + // Add trailing slash if not present + if (this._base === "" || this._base.substr(this._base.length - 1) !== "/") { + this._base = this._base + "/"; + } + } + + public get base() { + return this._base; + } + + public resolveUrl(relativeUrl: string) { + var firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "~") { + relativeUrl = relativeUrl.substr(1); + } + + firstChar = relativeUrl.substr(0, 1); + + if (firstChar === "/") { + relativeUrl = relativeUrl.substr(1); + } + + return this._base + relativeUrl; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts b/src/MusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts new file mode 100644 index 0000000000..8f0229275b --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/UserDetails/UserDetailsService.ts @@ -0,0 +1,34 @@ +module MusicStore.UserDetails { + export interface IUserDetailsService { + getUserDetails(): Models.IUserDetails; + getUserDetails(elementId: string): Models.IUserDetails; + } + + class UserDetailsService implements IUserDetailsService { + private _document: ng.IDocumentService; + private _userDetails: Models.IUserDetails; + + constructor($document: ng.IDocumentService) { + this._document = $document; + } + + public getUserDetails(elementId = "userDetails") { + if (!this._userDetails) { + //var el = this._document.querySelector("[data-json-id='" + elementId + "']"); + var el = this._document.find("#" + elementId + "[type='application/json']"); + + if (el.length) { + this._userDetails = angular.fromJson(el.text()); + } else { + this._userDetails = { + isAuthenticated: false, + userId: null, + userName: null, + roles: [] + }; + } + } + return this._userDetails; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts b/src/MusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts new file mode 100644 index 0000000000..488dc39010 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/ViewMessage/ViewAlertService.ts @@ -0,0 +1,9 @@ +module MusicStore.ViewAlert { + export interface IViewAlertService { + alert: Models.IAlert; + } + + class ViewAlertService implements IViewAlertService { + public alert: Models.IAlert; + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts b/src/MusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts new file mode 100644 index 0000000000..8459648052 --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/components/Visited/VisitedDirective.ts @@ -0,0 +1,49 @@ +module MusicStore.Visited { + interface IVisitedFormController extends ng.IFormController { + focus?: boolean; + visited?: boolean; + } + + //@NgDirective('input') + //@NgDirective('select') + class VisitedDirective implements ng.IDirective { + private _window: ng.IWindowService; + + constructor($window: ng.IWindowService) { + this._window = $window; + } + + public restrict = "E"; + + public require = "?ngModel"; + + public link(scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes, ctrl: IVisitedFormController) { + if (!ctrl) { + return; + } + + element.on("focus", event => { + element.addClass("has-focus"); + scope.$apply(() => ctrl.focus = true); + }); + + element.on("blur", event => { + element.removeClass("has-focus"); + element.addClass("has-visited"); + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + + element.closest("form").on("submit", function () { + element.addClass("has-visited"); + + scope.$apply(() => { + ctrl.focus = false; + ctrl.visited = true; + }); + }); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Client/ng-apps/references.ts b/src/MusicStore.Spa/Client/ng-apps/references.ts new file mode 100644 index 0000000000..102e291cec --- /dev/null +++ b/src/MusicStore.Spa/Client/ng-apps/references.ts @@ -0,0 +1,9 @@ +/// +/// +/// + +declare module ng { + export interface ILogProvider { + debugEnabled(enabled: boolean); + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Config.json b/src/MusicStore.Spa/Config.json new file mode 100644 index 0000000000..66154f8ab7 --- /dev/null +++ b/src/MusicStore.Spa/Config.json @@ -0,0 +1,12 @@ +{ + "DefaultAdminUsername": "Administrator", + "DefaultAdminPassword": "YouShouldChangeThisPassword1!", + "Data": { + "DefaultConnection": { + "Connectionstring": "Server=(localdb)\\MSSQLLocalDB;Database=MusicStore;Trusted_Connection=True;" + }, + "IdentityConnection": { + "Connectionstring": "Server=(localdb)\\MSSQLLocalDB;Database=MusicStoreIdentity;Trusted_Connection=True;" + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Controllers/AccountController.cs b/src/MusicStore.Spa/Controllers/AccountController.cs new file mode 100644 index 0000000000..5f14bad26d --- /dev/null +++ b/src/MusicStore.Spa/Controllers/AccountController.cs @@ -0,0 +1,185 @@ +using System.Security.Principal; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Security; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using MusicStore.Models; + +namespace MusicStore.Controllers +{ + [Authorize] + public class AccountController : Controller + { + public AccountController(UserManager userManager, SignInManager signInManager) + { + UserManager = userManager; + SignInManager = signInManager; + } + + public UserManager UserManager { get; private set; } + + public SignInManager SignInManager { get; private set; } + + // + // GET: /Account/Login + [AllowAnonymous] + //Bug: https://github.com/aspnet/WebFx/issues/339 + [HttpGet] + public IActionResult Login(string returnUrl) + { + ViewBag.ReturnUrl = returnUrl; + return View(); + } + + // + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Login(LoginViewModel model, string returnUrl) + { + if (ModelState.IsValid == true) + { + var signInStatus = await SignInManager.PasswordSignInAsync(model.UserName, model.Password, model.RememberMe, shouldLockout: false); + switch (signInStatus) + { + case SignInStatus.Success: + return RedirectToLocal(returnUrl); + case SignInStatus.LockedOut: + ModelState.AddModelError("", "User is locked out, try again later."); + return View(model); + case SignInStatus.Failure: + default: + ModelState.AddModelError("", "Invalid username or password."); + return View(model); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/Register + [AllowAnonymous] + //Bug: https://github.com/aspnet/WebFx/issues/339 + [HttpGet] + public IActionResult Register() + { + return View(); + } + + // + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public async Task Register(RegisterViewModel model) + { + //Bug: https://github.com/aspnet/WebFx/issues/247 + //if (ModelState.IsValid == true) + { + var user = new ApplicationUser { UserName = model.UserName }; + var result = await UserManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); + return RedirectToAction("Index", "Home"); + } + else + { + AddErrors(result); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/Manage + //Bug: https://github.com/aspnet/WebFx/issues/339 + [HttpGet] + public IActionResult Manage(ManageMessageId? message) + { + ViewBag.StatusMessage = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + ViewBag.ReturnUrl = Url.Action("Manage"); + return View(); + } + + // + // POST: /Account/Manage + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Manage(ManageUserViewModel model) + { + ViewBag.ReturnUrl = Url.Action("Manage"); + //Bug: https://github.com/aspnet/WebFx/issues/247 + //if (ModelState.IsValid == true) + { + var user = await GetCurrentUserAsync(); + var result = await UserManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + return RedirectToAction("Manage", new { Message = ManageMessageId.ChangePasswordSuccess }); + } + else + { + AddErrors(result); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // POST: /Account/LogOff + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LogOff() + { + SignInManager.SignOut(); + return RedirectToAction("Index", "Home"); + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError("", error); + } + } + + private async Task GetCurrentUserAsync() + { + return await UserManager.FindByIdAsync(Context.User.Identity.GetUserId()); + } + + public enum ManageMessageId + { + ChangePasswordSuccess, + Error + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return RedirectToAction("Index", "Home"); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Controllers/HomeController.cs b/src/MusicStore.Spa/Controllers/HomeController.cs new file mode 100644 index 0000000000..307cee8165 --- /dev/null +++ b/src/MusicStore.Spa/Controllers/HomeController.cs @@ -0,0 +1,14 @@ +using Microsoft.AspNet.Mvc; + +namespace MusicStore.Controllers +{ + public class HomeController : Controller + { + // + // GET: /Home/ + public IActionResult Index() + { + return View(); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Controllers/TemplateController.cs b/src/MusicStore.Spa/Controllers/TemplateController.cs new file mode 100644 index 0000000000..9cdb96d3cc --- /dev/null +++ b/src/MusicStore.Spa/Controllers/TemplateController.cs @@ -0,0 +1,49 @@ +using System; +using Microsoft.AspNet.Mvc; + +namespace MusicStore.Controllers +{ + public class TemplateController : Controller + { + private static readonly string _templateBasePath = "~/Client/ng-apps/"; + + // GET: Template + //[Route("ng-apps/{*path}")] + public ActionResult Index(string path) + { + if (!IsValidPath(path)) + { + // TODO: Change this to NotFoundResult when it's available + return new HttpStatusCodeResult(404); + } + + return View(_templateBasePath + path); + } + + private static bool IsValidPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + return false; + } + + var last = '\0'; + for (var i = 0; i < path.Length; i++) + { + var c = path[i]; + if (Char.IsLetterOrDigit(c) + || (c == '/' && last != '/') + || c == '-' + || c == '_' + || (c == '.' && last != '.')) + { + last = c; + continue; + } + return false; + } + + return path.EndsWith(".cshtml", StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Default.html b/src/MusicStore.Spa/Default.html new file mode 100644 index 0000000000..0eb0b7c25a --- /dev/null +++ b/src/MusicStore.Spa/Default.html @@ -0,0 +1,11 @@ + + + + + + Welcome to ASP.NET vNext + + +

Welcome to ASP.NET vNext

+ + \ No newline at end of file diff --git a/src/MusicStore.Spa/Gruntfile.js b/src/MusicStore.Spa/Gruntfile.js new file mode 100644 index 0000000000..1264f19a6c --- /dev/null +++ b/src/MusicStore.Spa/Gruntfile.js @@ -0,0 +1,189 @@ +/// + +// node-debug (Resolve-Path ~\AppData\Roaming\npm\node_modules\grunt-cli\bin\grunt) task:target + +module.exports = function (grunt) { + /// + + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-copy'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-typescript'); + grunt.loadNpmTasks('grunt-tslint'); + grunt.loadNpmTasks('grunt-tsng'); + //grunt.loadNpmTasks('grunt-contrib-jshint'); + //grunt.loadNpmTasks('grunt-contrib-qunit'); + //grunt.loadNpmTasks('grunt-contrib-concat'); + + grunt.initConfig({ + staticFilePattern: '**/*.{js,css,map,html,htm,ico,jpg,jpeg,png,gif,eot,svg,ttf,woff}', + pkg: grunt.file.readJSON('package.json'), + uglify: { + options: { + banner: '/*! <%= pkg.name %> <%= grunt.template.today("dd-mm-yyyy") %> */\n' + }, + release: { + files: { + 'wwwroot/app.min.js': ['<%= typescript.dev.dest %>'] + } + } + }, + clean: { + options: { force: true }, + bower: ['wwwroot'], + assets: ['wwwroot'], + tsng: ['client/**/*.ng.ts'] + }, + copy: { + // This is to work around an issue with the dt-angular bower package https://github.com/dt-bower/dt-angular/issues/4 + fix: { + files: { + "bower_components/jquery/jquery.d.ts": ["bower_components/dt-jquery/jquery.d.ts"] + } + }, + bower: { + files: [ + { // JavaScript + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "modernizr/modernizr.js", + "jquery/dist/*.{js,map}", + "jquery.validation/jquery.validate.js", + "jquery.validation/additional-methods.js", + "bootstrap/dist/**/*.js", + "respond/dest/**/*.js", + "angular/*.{js,.js.map}", + "angular-route/*.{js,.js.map}", + "angular-bootstrap/ui-bootstrap*" + ], + dest: "wwwroot/js/", + options: { force: true } + }, + { // CSS + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "bootstrap/dist/**/*.css", + ], + dest: "wwwroot/css/", + options: { force: true } + }, + { // Fonts + expand: true, + flatten: true, + cwd: "bower_components/", + src: [ + "bootstrap/**/*.{woff,svg,eot,ttf}", + ], + dest: "wwwroot/fonts/", + options: { force: true } + } + ] + }, + assets: { + files: [ + { + expand: true, + cwd: "Client/", + src: [ + '<%= staticFilePattern %>' + ], + dest: "wwwroot/", + options: { force: true } + } + ] + } + }, + less: { + dev: { + options: { + cleancss: false + }, + files: { + "wwwroot/css/site.css": "Client/**/*.less" + } + }, + release: { + options: { + cleancss: true + }, + files: { + "wwwroot/css/site.css": "Client/**/*.less" + } + } + }, + tsng: { + options: { + extension: ".ng.ts" + }, + dev: { + files: [ + // TODO: Automate the generation of this config based on convention + { + src: ['Client/ng-apps/components/**/*.ts', 'Client/ng-apps/MusicStore.Store/**/*.ts', "!**/*.ng.ts"], + dest: "Client/ng-apps" // This needs to be the same across all sets so shared components work + }, + { + src: ['Client/ng-apps/components/**/*.ts', 'Client/ng-apps/MusicStore.Admin/**/*.ts', "!**/*.ng.ts"], + dest: "Client/ng-apps" // This needs to be the same across all sets so shared components work + } + ] + } + }, + tslint: { + options: { + configuration: grunt.file.readJSON("tslint.json") + }, + files: { + src: ['Client/**/*.ts', '!**/*.ng.ts'] + } + }, + typescript: { + options: { + module: 'amd', // or commonjs + target: 'es5', // or es3 + sourcemap: false + }, + dev: { + files: [ + // TODO: Automate the generation of this config based on convention + { + src: ['Client/ng-apps/components/**/*.ng.ts', 'Client/ng-apps/MusicStore.Store/**/*.ng.ts'], + dest: 'wwwroot/js/MusicStore.Store.js' + }, + { + src: ['Client/ng-apps/components/**/*.ng.ts', 'Client/ng-apps/MusicStore.Admin/**/*.ng.ts'], + dest: 'wwwroot/js/MusicStore.Admin.js' + } + ] + }, + release: { + options: { + sourcemap: true + }, + files: '<%= typescript.dev.files %>' + } + }, + watch: { + typescript: { + files: ['Client/**/*.ts', "!**/*.ng.ts"], + tasks: ['ts'] + }, + dev: { + files: ['bower_components/<%= staticFilePattern %>', 'Client/<%= staticFilePattern %>'], + tasks: ['dev'] + } + } + }); + + //grunt.registerTask('test', ['jshint', 'qunit']); + grunt.registerTask('ts', ['tslint', 'tsng', 'typescript:dev']); + grunt.registerTask('dev', ['clean', 'copy', 'less:dev', 'ts']); + grunt.registerTask('release', ['clean', 'copy', 'uglify', 'less:release', 'typescript:release']); + grunt.registerTask('default', ['dev']); +}; \ No newline at end of file diff --git a/src/MusicStore.Spa/Helpers/AngularExtensions.cs b/src/MusicStore.Spa/Helpers/AngularExtensions.cs new file mode 100644 index 0000000000..7465305434 --- /dev/null +++ b/src/MusicStore.Spa/Helpers/AngularExtensions.cs @@ -0,0 +1,356 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.AspNet.Mvc.Rendering.Expressions; +using Microsoft.AspNet.Routing; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class AngularExtensions + { + public static HtmlString ngPasswordFor(this IHtmlHelper html, Expression> expression) + { + return html.ngTextBoxFor(expression, new RouteValueDictionary { { "type", "password" } }); + } + + public static HtmlString ngPasswordFor(this IHtmlHelper html, Expression> expression, object htmlAttributes) + { + return html.ngTextBoxFor(expression, MergeAttributes( + new RouteValueDictionary { { "type", "password" } }, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes))); + } + + public static HtmlString ngPasswordFor(this IHtmlHelper html, Expression> expression, IDictionary htmlAttributes) + { + return html.ngTextBoxFor(expression, MergeAttributes( + new RouteValueDictionary { { "type", "password" } }, + htmlAttributes)); + } + + public static HtmlString ngTextBoxFor(this IHtmlHelper html, Expression> expression) + { + return html.ngTextBoxFor(expression, new RouteValueDictionary()); + } + + public static HtmlString ngTextBoxFor(this IHtmlHelper html, Expression> expression, object htmlAttributes) + { + return html.ngTextBoxFor(expression, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + 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 ngAttributes = new Dictionary(); + + // Angular binding to client-side model (scope). This is required for Angular validation to work. + ngAttributes["ng-model"] = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText); + + // Set input type + if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.EmailAddress), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "email"; + } + else if (metadata.ModelType == typeof(Uri) + || string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Url), StringComparison.OrdinalIgnoreCase) + || string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.ImageUrl), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "url"; + } + else if (IsNumberType(metadata.ModelType)) + { + ngAttributes["type"] = "number"; + if (IsIntegerType(metadata.ModelType)) + { + ngAttributes["step"] = "1"; + } + else + { + ngAttributes["step"] = "any"; + } + } + else if (metadata.ModelType == typeof(DateTime)) + { + if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.Date), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "date"; + } + else if (string.Equals(metadata.DataTypeName, Enum.GetName(typeof(DataType), DataType.DateTime), StringComparison.OrdinalIgnoreCase)) + { + ngAttributes["type"] = "datetime"; + } + } + + // Add attributes for Angular validation + //var clientValidators = metadata.GetValidators(html.ViewContext.Controller.ControllerContext) + // .SelectMany(v => v.GetClientValidationRules()); + var clientValidators = helper.GetClientValidators(null, metadata); + + foreach (var validator in clientValidators) + { + if (string.Equals(validator.ValidationType, "length")) + { + if (validator.ValidationParameters.ContainsKey("min")) + { + ngAttributes["ng-minlength"] = validator.ValidationParameters["min"]; + } + if (validator.ValidationParameters.ContainsKey("max")) + { + ngAttributes["ng-maxlength"] = validator.ValidationParameters["max"]; + } + } + else if (string.Equals(validator.ValidationType, "required")) + { + ngAttributes["required"] = null; + } + else if (string.Equals(validator.ValidationType, "range")) + { + if (validator.ValidationParameters.ContainsKey("min")) + { + ngAttributes["min"] = validator.ValidationParameters["min"]; + } + if (validator.ValidationParameters.ContainsKey("max")) + { + ngAttributes["max"] = validator.ValidationParameters["max"]; + } + } + else if (string.Equals(validator.ValidationType, "equalto")) + { + // CompareAttribute validator + var fieldToCompare = validator.ValidationParameters["other"]; // e.g. *.NewPassword + var other = validator.ValidationParameters["other"].ToString(); + if (other.StartsWith("*.")) + { + // The built-in CompareAttributeAdapter prepends *. to the property name so we strip it off here + other = other.Substring("*.".Length); + } + ngAttributes["app-equal-to"] = other; + // TODO: Actually write the Angular directive to use this + } + // TODO: Regex, Phone(regex) + } + + // Render! + return html.TextBoxFor(expression, null, MergeAttributes(ngAttributes, htmlAttributes)); + } + + //private static bool IsNumberType(Type type) + //{ + // switch (Type.GetTypeCode(type)) + // { + // case TypeCode.Int16: + // case TypeCode.Int32: + // case TypeCode.Int64: + // case TypeCode.UInt16: + // case TypeCode.UInt32: + // case TypeCode.UInt64: + // case TypeCode.Decimal: + // case TypeCode.Double: + // case TypeCode.Single: + // return true; + // } + // return false; + //} + + private static bool IsNumberType(Type type) + { + if (type == typeof(Int16) || + type == typeof(Int32) || + type == typeof(Int64) || + type == typeof(UInt16) || + type == typeof(UInt32) || + type == typeof(UInt64) || + type == typeof(Decimal) || + type == typeof(Double) || + type == typeof(Single)) + { + return true; + } + return false; + } + + //private static bool IsIntegerType(Type type) + //{ + // switch (Type.GetTypeCode(type)) + // { + // case TypeCode.Int16: + // case TypeCode.Int32: + // case TypeCode.Int64: + // case TypeCode.UInt16: + // case TypeCode.UInt32: + // case TypeCode.UInt64: + // return true; + // } + // return false; + //} + + private static bool IsIntegerType(Type type) + { + if (type == typeof(Int16) || + type == typeof(Int32) || + type == typeof(Int64) || + type == typeof(UInt16) || + type == typeof(UInt32) || + type == typeof(UInt64)) + { + return true; + } + return false; + } + + public static HtmlString ngDropDownListFor(this IHtmlHelper html, Expression> propertyExpression, Expression> displayExpression, string source, string nullOption, object htmlAttributes) + { + return ngDropDownListFor(html, propertyExpression, displayExpression, source, nullOption, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + 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 tag = new TagBuilder("select"); + + var valueFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(propertyExpressionText); + var displayFieldName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(displayExpressionText); + + var displayFieldNameParts = displayFieldName.Split('.'); + displayFieldName = displayFieldNameParts[displayFieldNameParts.Length - 1]; + + tag.Attributes["id"] = helper.GetFullHtmlFieldId(propertyExpressionText).ToString(); + tag.Attributes["name"] = valueFieldName; + tag.Attributes["ng-model"] = valueFieldName; + + var ngOptionsFormat = "a.{0} as a.{1} for a in {2}"; + var ngOptions = string.Format(ngOptionsFormat, valueFieldName, displayFieldName, source); + tag.Attributes["ng-options"] = ngOptions; + + if (nullOption != null) + { + var nullOptionTag = new TagBuilder("option"); + nullOptionTag.Attributes["value"] = string.Empty; + nullOptionTag.SetInnerText(nullOption); + tag.InnerHtml = nullOptionTag.ToString(); + } + + var clientValidators = helper.GetClientValidators(null, metadata); + var isRequired = clientValidators.SingleOrDefault(cv => string.Equals(cv.ValidationType, "required", StringComparison.OrdinalIgnoreCase)) != null; + if (isRequired) + { + tag.Attributes["required"] = string.Empty; + } + + tag.MergeAttributes(htmlAttributes, replaceExisting: true); + + return html.Raw(tag.ToString()); + } + + public static HtmlString ngValidationMessageFor(this IHtmlHelper htmlHelper, Expression> expression, string formName) + { + return ngValidationMessageFor(htmlHelper, expression, formName, ((IDictionary)new RouteValueDictionary())); + } + + public static HtmlString ngValidationMessageFor(this IHtmlHelper htmlHelper, Expression> expression, string formName, object htmlAttributes) + { + return ngValidationMessageFor(htmlHelper, expression, formName, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + 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 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 tags = new List(); + + // Get validation messages from data type + // TODO: How to get validation messages from model metadata? All methods/properties required seem protected internal :( + + foreach (var validator in clientValidators) + { + var tag = new TagBuilder("span"); + tag.Attributes["ng-cloak"] = string.Empty; + + if (string.Equals(validator.ValidationType, "required")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && {0}.{1}.$error.{2}", formName, modelName, "required"); + tag.SetInnerText(validator.ErrorMessage); + } + else if (string.Equals(validator.ValidationType, "length")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.minlength || {0}.{1}.$error.maxlength)", + formName, modelName); + tag.SetInnerText(validator.ErrorMessage); + } + else if (string.Equals(validator.ValidationType, "range")) + { + tag.Attributes["ng-show"] = string.Format("({0}.submitAttempted || {0}.{1}.$dirty || {0}.{1}.visited) && ({0}.{1}.$error.min || {0}.{1}.$error.max)", + formName, modelName); + tag.SetInnerText(validator.ErrorMessage); + } + // TODO: Regex, equalto, remote + else + { + continue; + } + + tag.MergeAttributes(htmlAttributes); + tags.Add(tag); + } + + return html.Raw(String.Concat(tags.Select(t => t.ToString()))); + } + + 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 modelName = html.ViewData.TemplateInfo.GetFullHtmlFieldName(expressionText); + var ngClassFormat = "{{ '{0}' : ({1}.submitAttempted || {1}.{2}.$dirty || {1}.{2}.visited) && {1}.{2}.$invalid }}"; + + return string.Format(ngClassFormat, className, formName, modelName); + } + + private static IDictionary MergeAttributes(IDictionary source, IDictionary target) + { + // Keys in target win over keys in source + foreach (var pair in source) + { + if (!target.ContainsKey(pair.Key)) + { + target[pair.Key] = pair.Value; + } + } + + return target; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs b/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs new file mode 100644 index 0000000000..9dfc211ff0 --- /dev/null +++ b/src/MusicStore.Spa/Helpers/AngularHtmlHelper'T.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public class AngularHtmlHelper : HtmlHelper + { + public AngularHtmlHelper(IViewEngine viewEngine, IModelMetadataProvider metadataProvider, IUrlHelper urlHelper, AntiForgery antiForgeryInstance, IActionBindingContextProvider actionBindingContextProvider) + : base(viewEngine, metadataProvider, urlHelper, antiForgeryInstance, actionBindingContextProvider) + { + + } + + // 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(name, metadata); + } + + public HtmlString GetFullHtmlFieldId(string expression) + { + return GenerateId(expression); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Helpers/GeneralExtensions.cs b/src/MusicStore.Spa/Helpers/GeneralExtensions.cs new file mode 100644 index 0000000000..76ee8c0605 --- /dev/null +++ b/src/MusicStore.Spa/Helpers/GeneralExtensions.cs @@ -0,0 +1,12 @@ +using System; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class GeneralExtensions + { + public static HtmlString Tag(this IHtmlHelper htmlHelper, TagBuilder tagBuilder) + { + return htmlHelper.Raw(tagBuilder.ToString()); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Helpers/JsonExtensions.cs b/src/MusicStore.Spa/Helpers/JsonExtensions.cs new file mode 100644 index 0000000000..f1a36a6ac4 --- /dev/null +++ b/src/MusicStore.Spa/Helpers/JsonExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Routing; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.Mvc.Rendering +{ + public static class JsonExtensions + { + public static HtmlString Json(this IHtmlHelper helper, TData data) + { + return Json(helper, data, new RouteValueDictionary()); + } + + public static HtmlString Json(this IHtmlHelper helper, TData data, object htmlAttributes) + { + return Json(helper, data, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + } + + public static HtmlString Json(this IHtmlHelper helper, TData data, IDictionary htmlAttributes) + { + var builder = new TagBuilder("script"); + builder.Attributes["type"] = "application/json"; + builder.MergeAttributes(htmlAttributes); + builder.InnerHtml = + (data is JsonString + ? data.ToString() + : JsonConvert.SerializeObject(data)) + .Replace("<", "\u003C").Replace(">", "\u003E"); + + return helper.Tag(builder); + } + + public static HtmlString InlineData(this IHtmlHelper helper, string actionName, string controllerName) + { + //var result = helper.Action(actionName, controllerName); + //var urlHelper = new UrlHelper(helper.ViewContext.RequestContext); + //var url = urlHelper.Action(actionName, controllerName); + + //return helper.Json(new JsonString(result), new RouteValueDictionary { + // { "app-inline-data", null }, + // { "for", url } + //}); + + return helper.Json(new JsonString(new object()), null); + } + } + + public class JsonString + { + public JsonString(object value) + : this(value.ToString()) + { + + } + + public JsonString(string value) + { + Value = value; + } + + public string Value { get; private set; } + + public override string ToString() + { + return Value; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/ApiResult.cs b/src/MusicStore.Spa/Infrastructure/ApiResult.cs new file mode 100644 index 0000000000..dbdb9e1ac5 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/ApiResult.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.Mvc +{ + public class ApiResult : ActionResult + { + public ApiResult(ModelStateDictionary modelState) + : this() + { + if (modelState.Any(m => m.Value.Errors.Count > 0)) + { + StatusCode = 400; + Message = "The model submitted was invalid. Please correct the specified errors and try again."; + ModelErrors = modelState + .SelectMany(m => m.Value.Errors.Select(me => new ModelError + { + FieldName = m.Key, + ErrorMessage = me.ErrorMessage + })); + } + } + + public ApiResult() + { + + } + + [JsonIgnore] + public int? StatusCode { get; set; } + + public string Message { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public object Data { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable ModelErrors { get; set; } + + public override void ExecuteResult(ActionContext context) + { + var json = new SmartJsonResult + { + StatusCode = StatusCode, + Data = this + }; + json.ExecuteResult(context); + } + + public class ModelError + { + public string FieldName { get; set; } + + public string ErrorMessage { get; set; } + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/BaseController.cs b/src/MusicStore.Spa/Infrastructure/BaseController.cs new file mode 100644 index 0000000000..0b13f3c738 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/BaseController.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.Mvc +{ + public class BaseController : Controller + { + private IEnumerable _modelBinders; + private IModelMetadataProvider _modelMetadataProvider; + private IEnumerable _validatorProviders; + private IEnumerable _valueProviderFactories; + + public BaseController() + { + + } + + public void Initialize( + IEnumerable modelBinders, + IModelMetadataProvider modelMetadataProvider, + IEnumerable validatorProviders, + IEnumerable valueProviderFactories) + { + _modelBinders = modelBinders; + _modelMetadataProvider = modelMetadataProvider; + _validatorProviders = validatorProviders; + _valueProviderFactories = valueProviderFactories; + } + + protected Task TryUpdateModelAsync(TModel model) + { + var binder = new CompositeModelBinder(_modelBinders); + var requestContext = new RequestContext(Context, ActionContext.RouteValues); + var bindingContext = new ModelBindingContext + { + MetadataProvider = _modelMetadataProvider, + Model = model, + ModelState = ModelState, + ValidatorProviders = _validatorProviders, + ModelBinder = binder, + HttpContext = Context, + ValueProvider = new CompositeValueProvider(_valueProviderFactories.Select( + vpf => vpf.GetValueProviderAsync(requestContext).Result)) + }; + + return binder.BindModelAsync(bindingContext); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/ForcedModelError.cs b/src/MusicStore.Spa/Infrastructure/ForcedModelError.cs new file mode 100644 index 0000000000..b4ae4a7518 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/ForcedModelError.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; + +namespace System.ComponentModel.DataAnnotations +{ + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class ForcedModelErrorAttribute : ValidationAttribute + { + public ForcedModelErrorAttribute(object failValue) + { + FailValue = failValue; + } + + public object FailValue { get; private set; } + + public override string FormatErrorMessage(string name) + { + return string.Format(CultureInfo.CurrentCulture, "The field {0} was forced to fail model validation.", name); + } + + public override bool IsValid(object value) + { + return value == null || !value.Equals(FailValue); + // BUG: #ifdefs not working in editor +#if DEBUG + return value == null || !value.Equals(FailValue); +#else + //return true; +#endif + } + } +} diff --git a/src/MusicStore.Spa/Infrastructure/PagedList.cs b/src/MusicStore.Spa/Infrastructure/PagedList.cs new file mode 100644 index 0000000000..59bc66ab40 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/PagedList.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MusicStore.Infrastructure +{ + public interface IPagedList + { + IEnumerable Data { get; } + + int Page { get; } + + int PageSize { get; } + + int TotalCount { get; } + } + + internal class PagedList : IPagedList + { + public PagedList(IEnumerable data, int page, int pageSize, int totalCount) + { + Data = data; + Page = page; + PageSize = pageSize; + TotalCount = totalCount; + } + + public IEnumerable Data { get; private set; } + + public int Page { get; private set; } + + public int PageSize { get; private set; } + + public int TotalCount{get; private set; } + } + + public static class PagedListExtensions + { + public static IPagedList ToPagedList(this IQueryable query, int page, int pageSize) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + + var data = query + .Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToList(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = query + .Take(pagingConfig.PageSize) + .ToList(); + } + + return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, query.Count()); + } + + public static async Task> ToPagedListAsync(this IQueryable query, int page, int pageSize) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + + var data = await query + .Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToListAsync(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = await query + .Take(pagingConfig.PageSize) + .ToListAsync(); + } + + return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, await query.CountAsync()); + } + + private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig) + { + if (pagingConfig.Page < 1) + { + pagingConfig.Page = 1; + } + + if (pagingConfig.PageSize < 10) + { + pagingConfig.PageSize = 10; + } + + if (pagingConfig.PageSize > 100) + { + pagingConfig.PageSize = 100; + } + + return pagingConfig.PageSize * (pagingConfig.Page - 1); + } + + internal class PagingConfig + { + public PagingConfig(int page, int pageSize) + { + Page = page; + PageSize = pageSize; + } + + public int Page { get; set; } + + public int PageSize { get; set; } + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/SmartJsonResult.cs b/src/MusicStore.Spa/Infrastructure/SmartJsonResult.cs new file mode 100644 index 0000000000..ce8f89f847 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/SmartJsonResult.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Microsoft.AspNet.Mvc +{ + public class SmartJsonResult : ActionResult + { + public SmartJsonResult() : base() + { + + } + + public JsonSerializerSettings Settings { get; set; } + + public object Data { get; set; } + + public int? StatusCode { get; set; } + + public override Task ExecuteResultAsync(ActionContext context) + { + //if (!context.IsChildAction) + //{ + // if (StatusCode.HasValue) + // { + // context.HttpContext.Response.StatusCode = StatusCode.Value; + // } + // context.HttpContext.Response.ContentType = "application/json"; + // context.HttpContext.Response.ContentEncoding = Encoding.UTF8; + //} + + return context.HttpContext.Response.WriteAsync(JsonConvert.SerializeObject(Data, Settings ?? new JsonSerializerSettings())); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/SortDirection.cs b/src/MusicStore.Spa/Infrastructure/SortDirection.cs new file mode 100644 index 0000000000..020b180053 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/SortDirection.cs @@ -0,0 +1,10 @@ +using System; + +namespace MusicStore.Infrastructure +{ + public enum SortDirection + { + Ascending, + Descending + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Infrastructure/SortExpression.cs b/src/MusicStore.Spa/Infrastructure/SortExpression.cs new file mode 100644 index 0000000000..f775e1e057 --- /dev/null +++ b/src/MusicStore.Spa/Infrastructure/SortExpression.cs @@ -0,0 +1,85 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.AspNet.Mvc.Rendering.Expressions; + +namespace MusicStore.Infrastructure +{ + public static class SortExpression + { + private const string SORT_DIRECTION_DESC = " DESC"; + + public static IQueryable SortBy(this IQueryable query, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) where TModel : class + { + return SortBy(query, sortExpression ?? Create(defaultSortExpression, defaultSortDirection)); + } + + public static string Create(Expression> expression, SortDirection sortDirection = SortDirection.Ascending) where TModel : class + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + // TODO: Validate the expression depth, etc. + + var sortExpression = expressionText; + + if (sortDirection == SortDirection.Descending) + { + sortExpression += SORT_DIRECTION_DESC; + } + + return sortExpression; + } + + public static IQueryable SortBy(this IQueryable source, string sortExpression) where T : class + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (String.IsNullOrWhiteSpace(sortExpression)) + { + return source; + } + + sortExpression = sortExpression.Trim(); + var isDescending = false; + + // DataSource control passes the sort parameter with a direction + // if the direction is descending + if (sortExpression.EndsWith(SORT_DIRECTION_DESC, StringComparison.OrdinalIgnoreCase)) + { + isDescending = true; + var descIndex = sortExpression.Length - SORT_DIRECTION_DESC.Length; + sortExpression = sortExpression.Substring(0, descIndex).Trim(); + } + + if (string.IsNullOrEmpty(sortExpression)) + { + return source; + } + + ParameterExpression parameter = Expression.Parameter(source.ElementType, String.Empty); + + // Build up the property expression, e.g.: (m => m.Foo.Bar) + var sortExpressionParts = sortExpression.Split('.'); + Expression propertyExpression = parameter; + foreach (var property in sortExpressionParts) + { + propertyExpression = Expression.Property(propertyExpression, property); + } + + LambdaExpression lambda = Expression.Lambda(propertyExpression, parameter); + + var methodName = (isDescending) ? "OrderByDescending" : "OrderBy"; + + Expression methodCallExpression = Expression.Call( + typeof(Queryable), + methodName, + new [] { source.ElementType, propertyExpression.Type }, + source.Expression, + Expression.Quote(lambda)); + + return (IQueryable)source.Provider.CreateQuery(methodCallExpression); + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/AccountViewModels.cs b/src/MusicStore.Spa/Models/AccountViewModels.cs new file mode 100644 index 0000000000..657a4f011d --- /dev/null +++ b/src/MusicStore.Spa/Models/AccountViewModels.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + } + + public class ManageUserViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class LoginViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/Album.cs b/src/MusicStore.Spa/Models/Album.cs new file mode 100644 index 0000000000..35f03d9f82 --- /dev/null +++ b/src/MusicStore.Spa/Models/Album.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class Album + { + public Album() + { + // TODO: Temporary hack to populate the orderdetails until EF does this automatically. + OrderDetails = new List(); + } + + [ScaffoldColumn(false)] + public int AlbumId { get; set; } + + public int GenreId { get; set; } + + public int ArtistId { get; set; } + + [Required] + [StringLength(160, MinimumLength = 2)] + public string Title { get; set; } + + [Required] + [Range(0.01, 100.00)] + [DataType(DataType.Currency)] + public decimal Price { get; set; } + + [Display(Name = "Album Art URL")] + [StringLength(1024)] + public string AlbumArtUrl { get; set; } + + public virtual Genre Genre { get; set; } + + public virtual Artist Artist { get; set; } + + public virtual ICollection OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/Artist.cs b/src/MusicStore.Spa/Models/Artist.cs new file mode 100644 index 0000000000..43d677c437 --- /dev/null +++ b/src/MusicStore.Spa/Models/Artist.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class Artist + { + public int ArtistId { get; set; } + + [Required] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/CartItem.cs b/src/MusicStore.Spa/Models/CartItem.cs new file mode 100644 index 0000000000..64550abf9a --- /dev/null +++ b/src/MusicStore.Spa/Models/CartItem.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class CartItem + { + [Key] + public int CartItemId { get; set; } + + [Required] + public string CartId { get; set; } + public int AlbumId { get; set; } + public int Count { get; set; } + + [DataType(DataType.DateTime)] + public DateTime DateCreated { get; set; } + + public virtual Album Album { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/Genre.cs b/src/MusicStore.Spa/Models/Genre.cs new file mode 100644 index 0000000000..a7fdb34f41 --- /dev/null +++ b/src/MusicStore.Spa/Models/Genre.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace MusicStore.Models +{ + public class Genre + { + public Genre() + { + Albums = new List(); + } + + public int GenreId { get; set; } + + [Required] + public string Name { get; set; } + + public string Description { get; set; } + + [JsonIgnore] + public virtual ICollection Albums { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/IdentityModels.cs b/src/MusicStore.Spa/Models/IdentityModels.cs new file mode 100644 index 0000000000..c58217a954 --- /dev/null +++ b/src/MusicStore.Spa/Models/IdentityModels.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.AspNet.Identity.Entity; +using Microsoft.Data.Entity; +using Microsoft.Framework.DependencyInjection; + +namespace MusicStore.Models +{ + public class ApplicationUser : User { } + + public class ApplicationDbContext : IdentitySqlContext + { + public ApplicationDbContext(IServiceProvider serviceProvider, IOptionsAccessor optionsAccessor) + : base(serviceProvider, optionsAccessor.Options.BuildConfiguration()) + { + + } + } + + public class IdentityDbContextOptions : DbContextOptions + { + public string DefaultAdminUserName { get; set; } + + public string DefaultAdminPassword { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/MusicStoreContext.cs b/src/MusicStore.Spa/Models/MusicStoreContext.cs new file mode 100644 index 0000000000..9b483dc5ec --- /dev/null +++ b/src/MusicStore.Spa/Models/MusicStoreContext.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Framework.DependencyInjection; + +namespace MusicStore.Models +{ + public class MusicStoreContext : DbContext + { + public MusicStoreContext(IServiceProvider serviceProvider, IOptionsAccessor optionsAccessor) + : base(serviceProvider, optionsAccessor.Options.BuildConfiguration()) + { + + } + + public DbSet Albums { get; set; } + public DbSet Artists { get; set; } + public DbSet Orders { get; set; } + public DbSet Genres { get; set; } + public DbSet CartItems { get; set; } + public DbSet OrderDetails { get; set; } + + 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. + builder.Entity().ToTable("Albums"); + builder.Entity().ToTable("Artists"); + builder.Entity().ToTable("Orders"); + builder.Entity().ToTable("Genres"); + builder.Entity().ToTable("CartItems"); + builder.Entity().ToTable("OrderDetails"); + + builder.Entity().Key(a => a.AlbumId); + builder.Entity().Key(a => a.ArtistId); + builder.Entity().Key(o => o.OrderId).StorageName("[Order]"); + builder.Entity().Key(g => g.GenreId); + builder.Entity().Key(ci => ci.CartItemId); + builder.Entity().Key(od => od.OrderDetailId); + + builder.Entity() + .ForeignKeys(kb => + { + kb.ForeignKey(a => a.GenreId); + kb.ForeignKey(a => a.ArtistId); + }); + builder.Entity() + .ForeignKeys(kb => + { + kb.ForeignKey(a => a.AlbumId); + kb.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(new Navigation(album.ForeignKeys.Single(k => k.ReferencedEntityType == genre), "Albums")); + album.AddNavigation(new Navigation(orderDetail.ForeignKeys.Single(k => k.ReferencedEntityType == album), "OrderDetails")); + album.AddNavigation(new Navigation(album.ForeignKeys.Single(k => k.ReferencedEntityType == genre), "Genre")); + album.AddNavigation(new Navigation(album.ForeignKeys.Single(k => k.ReferencedEntityType == artist), "Artist")); + } + } + + public class MusicStoreDbContextOptions : DbContextOptions + { + + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/Order.cs b/src/MusicStore.Spa/Models/Order.cs new file mode 100644 index 0000000000..a07ed5290f --- /dev/null +++ b/src/MusicStore.Spa/Models/Order.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + //[Bind(Include = "FirstName,LastName,Address,City,State,PostalCode,Country,Phone,Email")] + public class Order + { + public Order() + { + OrderDetails = new List(); + } + + [ScaffoldColumn(false)] + public int OrderId { get; set; } + + [ScaffoldColumn(false)] + public DateTime OrderDate { get; set; } + + [Required] + [ScaffoldColumn(false)] + public string Username { get; set; } + + [Required] + [Display(Name = "First Name")] + [StringLength(160)] + public string FirstName { get; set; } + + [Required] + [Display(Name = "Last Name")] + [StringLength(160)] + public string LastName { get; set; } + + [Required] + [StringLength(70, MinimumLength = 3)] + public string Address { get; set; } + + [Required] + [StringLength(40)] + public string City { get; set; } + + [Required] + [StringLength(40)] + public string State { get; set; } + + [Required] + [Display(Name = "Postal Code")] + [StringLength(10, MinimumLength = 5)] + public string PostalCode { get; set; } + + [Required] + [StringLength(40)] + public string Country { get; set; } + + [Required] + [StringLength(24)] + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } + + [Required] + [Display(Name = "Email Address")] + [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", + ErrorMessage = "Email is is not valid.")] + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + [ScaffoldColumn(false)] + public decimal Total { get; set; } + + public ICollection OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/OrderDetail.cs b/src/MusicStore.Spa/Models/OrderDetail.cs new file mode 100644 index 0000000000..29f87988df --- /dev/null +++ b/src/MusicStore.Spa/Models/OrderDetail.cs @@ -0,0 +1,14 @@ +namespace MusicStore.Models +{ + public class OrderDetail + { + public int OrderDetailId { get; set; } + public int OrderId { get; set; } + public int AlbumId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + + public virtual Album Album { get; set; } + public virtual Order Order { get; set; } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/SampleData.cs b/src/MusicStore.Spa/Models/SampleData.cs new file mode 100644 index 0000000000..3a8036dbc7 --- /dev/null +++ b/src/MusicStore.Spa/Models/SampleData.cs @@ -0,0 +1,966 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.SqlServer; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; + +namespace MusicStore.Models +{ + public static class SampleData + { + const string imgUrl = "/images/placeholder.png"; + + public static async Task InitializeMusicStoreDatabaseAsync(IServiceProvider serviceProvider) + { + using (var db = serviceProvider.GetService()) + { + var sqlServerDataStore = db.Configuration.DataStore as SqlServerDataStore; + if (sqlServerDataStore != null) + { + if (await db.Database.EnsureCreatedAsync()) + { + await InsertTestData(serviceProvider); + } + } + else + { + await InsertTestData(serviceProvider); + } + } + } + + public static async Task InitializeIdentityDatabaseAsync(IServiceProvider serviceProvider) + { + using (var db = serviceProvider.GetService()) + { + var sqlServerDataStore = db.Configuration.DataStore as SqlServerDataStore; + if (sqlServerDataStore != null) + { + if (await db.Database.EnsureCreatedAsync()) + { + await CreateAdminUser(serviceProvider); + } + } + else + { + await CreateAdminUser(serviceProvider); + } + } + } + + private static async Task CreateAdminUser(IServiceProvider serviceProvider) + { + var options = serviceProvider.GetService>().Options; + //const string adminRole = "Administrator"; + + var userManager = serviceProvider.GetService>(); + // TODO: Identity SQL does not support roles yet + //var roleManager = serviceProvider.GetService(); + //if (!await roleManager.RoleExistsAsync(adminRole)) + //{ + // await roleManager.CreateAsync(new IdentityRole(adminRole)); + //} + + var user = await userManager.FindByNameAsync(options.DefaultAdminUserName); + if (user == null) + { + user = new ApplicationUser { UserName = options.DefaultAdminUserName }; + await userManager.CreateAsync(user, options.DefaultAdminPassword); + //await userManager.AddToRoleAsync(user, adminRole); + await userManager.AddClaimAsync(user, new Claim("ManageStore", "Allowed")); + } + } + + private static async Task InsertTestData(IServiceProvider serviceProvider) + { + var albums = GetAlbums(imgUrl, Genres, Artists); + + await AddOrUpdateAsync(serviceProvider, g => g.GenreId, Genres.Select(genre => genre.Value)); + await AddOrUpdateAsync(serviceProvider, a => a.ArtistId, Artists.Select(artist => artist.Value)); + await AddOrUpdateAsync(serviceProvider, a => a.AlbumId, albums); + } + + // TODO [EF] This may be replaced by a first class mechanism in EF + private static async Task AddOrUpdateAsync( + IServiceProvider serviceProvider, + Func propertyToMatch, IEnumerable entities) + where TEntity : class + { + // Query in a separate context so that we can attach existing entities as modified + List existingData; + using (var db = serviceProvider.GetService()) + { + existingData = db.Set().ToList(); + } + + using (var db = serviceProvider.GetService()) + { + foreach (var item in entities) + { + db.ChangeTracker.Entry(item).State = existingData.Any(g => propertyToMatch(g).Equals(propertyToMatch(item))) + ? EntityState.Modified + : EntityState.Added; + } + + await db.SaveChangesAsync(); + } + } + + private static Album[] GetAlbums(string imgUrl, Dictionary genres, Dictionary artists) + { + var albums = new Album[] + { + new Album { Title = "The Best Of The Men At Work", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Men At Work"], AlbumArtUrl = imgUrl }, + new Album { Title = "...And Justice For All", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "עד גבול האור", Genre = genres["World"], Price = 8.99M, Artist = artists["אריק אינשטיין"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Light Syndrome", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Terry Bozzio, Tony Levin & Steve Stevens"], AlbumArtUrl = imgUrl }, + new Album { Title = "10,000 Days", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "11i", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Supreme Beings of Leisure"], AlbumArtUrl = imgUrl }, + new Album { Title = "1960", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Soul-Junk"], AlbumArtUrl = imgUrl }, + new Album { Title = "4x4=12 ", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["deadmau5"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Copland Celebration, Vol. I", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Symphony Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Lively Mind", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Paul Oakenfold"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Matter of Life and Death", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Real Dead One", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Real Live One", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Rush of Blood to the Head", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Coldplay"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Soprano Inspired", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Britten Sinfonia, Ivor Bolton & Lesley Garrett"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Winter Symphony", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Abbey Road", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ace Of Spades", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Motörhead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Achtung Baby", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Acústico MTV", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Adams, John: The Chairman Dances", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Edo de Waart & San Francisco Symphony"], AlbumArtUrl = imgUrl }, + new Album { Title = "Adrenaline", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deftones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ænima", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Afrociberdelia", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Science & Nação Zumbi"], AlbumArtUrl = imgUrl }, + new Album { Title = "After the Goldrush", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Neil Young"], AlbumArtUrl = imgUrl }, + new Album { Title = "Airdrawn Dagger", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Sasha"], AlbumArtUrl = imgUrl }, + new Album { Title = "Album Title Goes Here", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["deadmau5"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alcohol Fueled Brewtality Live! [Disc 1]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Label Society"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alcohol Fueled Brewtality Live! [Disc 2]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Label Society"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alive 2007", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Daft Punk"], AlbumArtUrl = imgUrl }, + new Album { Title = "All I Ask of You", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Amen (So Be It)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paddy Casey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Animal Vehicle", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Axis of Awesome"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ao Vivo [IMPORT]", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Zeca Pagodinho"], AlbumArtUrl = imgUrl }, + new Album { Title = "Apocalyptic Love", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Slash"], AlbumArtUrl = imgUrl }, + new Album { Title = "Appetite for Destruction", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Are You Experienced?", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Jimi Hendrix"], AlbumArtUrl = imgUrl }, + new Album { Title = "Arquivo II", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Arquivo Os Paralamas Do Sucesso", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "A-Sides", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Soundgarden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Audioslave", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Audioslave"], AlbumArtUrl = imgUrl }, + new Album { Title = "Automatic for the People", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["R.E.M."], AlbumArtUrl = imgUrl }, + new Album { Title = "Axé Bahia 2001", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Babel", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Mumford & Sons"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: Goldberg Variations", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Wilhelm Kempff"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: The Brandenburg Concertos", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Orchestra of The Age of Enlightenment"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: The Cello Suites", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Yo-Yo Ma"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: Toccata & Fugue in D Minor", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Ton Koopman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bad Motorfinger", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Soundgarden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Balls to the Wall", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Accept"], AlbumArtUrl = imgUrl }, + new Album { Title = "Banadeek Ta'ala", Genre = genres["World"], Price = 8.99M, Artist = artists["Amr Diab"], AlbumArtUrl = imgUrl }, + new Album { Title = "Barbie Girl", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Aqua"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bark at the Moon (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bartok: Violin & Viola Concertos", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Yehudi Menuhin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Barulhinho Bom", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Marisa Monte"], AlbumArtUrl = imgUrl }, + new Album { Title = "BBC Sessions [Disc 1] [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "BBC Sessions [Disc 2] [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Be Here Now", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Oasis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bedrock 11 Compiled & Mixed", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["John Digweed"], AlbumArtUrl = imgUrl }, + new Album { Title = "Berlioz: Symphonie Fantastique", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Michael Tilson Thomas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Beyond Good And Evil", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Cult"], AlbumArtUrl = imgUrl }, + new Album { Title = "Big Bad Wolf ", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Armand Van Helden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Big Ones", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Aerosmith"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Album", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Sabbath Vol. 4 (Remaster)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Sabbath"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Sabbath", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Sabbath"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blackwater Park", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Opeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blizzard of Ozz", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blood", Genre = genres["Rock"], Price = 8.99M, Artist = artists["In This Moment"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blue Moods", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Incognito"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blue", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Weezer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bongo Fury", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Frank Zappa & Captain Beefheart"], AlbumArtUrl = imgUrl }, + new Album { Title = "Boys & Girls", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alabama Shakes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Brave New World", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "B-Sides 1980-1990", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bunkka", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Paul Oakenfold"], AlbumArtUrl = imgUrl }, + new Album { Title = "By The Way", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Red Hot Chili Peppers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cake: B-Sides and Rarities", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Cake"], AlbumArtUrl = imgUrl }, + new Album { Title = "Californication", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Red Hot Chili Peppers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carmina Burana", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Boston Symphony Orchestra & Seiji Ozawa"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carried to Dust (Bonus Track Version)", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Calexico"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carry On", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Chris Cornell"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cássia Eller - Sem Limite [Disc 1]", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cássia Eller"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chemical Wedding", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Bruce Dickinson"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chill: Brazil (Disc 1)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Marcos Valle"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chill: Brazil (Disc 2)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Antônio Carlos Jobim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chocolate Starfish And The Hot Dog Flavored Water", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Limp Bizkit"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chronicle, Vol. 1", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Creedence Clearwater Revival"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chronicle, Vol. 2", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Creedence Clearwater Revival"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ciao, Baby", Genre = genres["Rock"], Price = 8.99M, Artist = artists["TheStart"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cidade Negra - Hits", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cidade Negra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Classic Munkle: Turbo Edition", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Munkle"], AlbumArtUrl = imgUrl }, + new Album { Title = "Classics: The Best of Sarah Brightman", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Coda", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Come Away With Me", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Norah Jones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Come Taste The Band", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Comfort Eagle", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Cake"], AlbumArtUrl = imgUrl }, + new Album { Title = "Common Reaction", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Uh Huh Her "], AlbumArtUrl = imgUrl }, + new Album { Title = "Compositores", Genre = genres["Rock"], Price = 8.99M, Artist = artists["O Terço"], AlbumArtUrl = imgUrl }, + new Album { Title = "Contraband", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Velvet Revolver"], AlbumArtUrl = imgUrl }, + new Album { Title = "Core", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Stone Temple Pilots"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cornerstone", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Styx"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cosmicolor", Genre = genres["Rap"], Price = 8.99M, Artist = artists["M-Flo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cross", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Justice"], AlbumArtUrl = imgUrl }, + new Album { Title = "Culture of Fear", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Thievery Corporation"], AlbumArtUrl = imgUrl }, + new Album { Title = "Da Lama Ao Caos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Science & Nação Zumbi"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dakshina", Genre = genres["World"], Price = 8.99M, Artist = artists["Deva Premal"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dark Side of the Moon", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "Death Magnetic", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deep End of Down", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Above the Fold"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deep Purple In Rock", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deixa Entrar", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Falamansa"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deja Vu", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Crosby, Stills, Nash, and Young"], AlbumArtUrl = imgUrl }, + new Album { Title = "Di Korpu Ku Alma", Genre = genres["World"], Price = 8.99M, Artist = artists["Lura"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diary of a Madman (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diary of a Madman", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dirt", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alice in Chains"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diver Down", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Djavan Ao Vivo - Vol. 02", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Djavan"], AlbumArtUrl = imgUrl }, + new Album { Title = "Djavan Ao Vivo - Vol. 1", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Djavan"], AlbumArtUrl = imgUrl }, + new Album { Title = "Drum'n'bass for Papa", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Plug"], AlbumArtUrl = imgUrl }, + new Album { Title = "Duluth", Genre = genres["Country"], Price = 8.99M, Artist = artists["Trampled By Turtles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dummy", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Portishead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Duos II", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Luciana Souza/Romero Lubambo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Earl Scruggs and Friends", Genre = genres["Country"], Price = 8.99M, Artist = artists["Earl Scruggs"], AlbumArtUrl = imgUrl }, + new Album { Title = "Eden", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "El Camino", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Black Keys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elegant Gypsy", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Al di Meola"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elements Of Life", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Tiësto"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elis Regina-Minha História", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Elis Regina"], AlbumArtUrl = imgUrl }, + new Album { Title = "Emergency On Planet Earth", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Jamiroquai"], AlbumArtUrl = imgUrl }, + new Album { Title = "Emotion", Genre = genres["World"], Price = 8.99M, Artist = artists["Papa Wemba"], AlbumArtUrl = imgUrl }, + new Album { Title = "English Renaissance", Genre = genres["Classical"], Price = 8.99M, Artist = artists["The King's Singers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Every Kind of Light", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Posies"], AlbumArtUrl = imgUrl }, + new Album { Title = "Faceless", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Godsmack"], AlbumArtUrl = imgUrl }, + new Album { Title = "Facelift", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alice in Chains"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fair Warning", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fear of a Black Planet", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Public Enemy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fear Of The Dark", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Feels Like Home", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Norah Jones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fireball", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fly", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "For Those About To Rock We Salute You", Genre = genres["Rock"], Price = 8.99M, Artist = artists["AC/DC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Four", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Blues Traveler"], AlbumArtUrl = imgUrl }, + new Album { Title = "Frank", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Amy Winehouse"], AlbumArtUrl = imgUrl }, + new Album { Title = "Further Down the Spiral", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Nine Inch Nails"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garage Inc. (Disc 1)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garage Inc. (Disc 2)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garbage", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Garbage"], AlbumArtUrl = imgUrl }, + new Album { Title = "Good News For People Who Love Bad News", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Modest Mouse"], AlbumArtUrl = imgUrl }, + new Album { Title = "Gordon", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Barenaked Ladies"], AlbumArtUrl = imgUrl }, + new Album { Title = "Górecki: Symphony No. 3", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Adrian Leaper & Doreen de Feis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Duck Sauce"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Lenny Kravitz"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Lenny Kravitz"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Kiss", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Kiss"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greetings from Michigan", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Sufjan Stevens"], AlbumArtUrl = imgUrl }, + new Album { Title = "Group Therapy", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Above & Beyond"], AlbumArtUrl = imgUrl }, + new Album { Title = "Handel: The Messiah (Highlights)", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Scholars Baroque Ensemble"], AlbumArtUrl = imgUrl }, + new Album { Title = "Haydn: Symphonies 99 - 104", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Royal Philharmonic Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Heart of the Night", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Spyro Gyra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Heart On", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Eagles of Death Metal"], AlbumArtUrl = imgUrl }, + new Album { Title = "Holy Diver", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Dio"], AlbumArtUrl = imgUrl }, + new Album { Title = "Homework", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Daft Punk"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hot Rocks, 1964-1971 (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Houses Of The Holy", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "How To Dismantle An Atomic Bomb", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Human", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Projected"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hunky Dory", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Bowie"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hymns", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Projected"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hysteria", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Def Leppard"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Absentia", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Porcupine Tree"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Between", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Paul Van Dyk"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Rainbows", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Radiohead"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Step", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Stevie Ray Vaughan & Double Trouble"], AlbumArtUrl = imgUrl }, + new Album { Title = "In the court of the Crimson King", Genre = genres["Rock"], Price = 8.99M, Artist = artists["King Crimson"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Through The Out Door", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Your Honor [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Your Honor [Disc 2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "Indestructible", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rancid"], AlbumArtUrl = imgUrl }, + new Album { Title = "Infinity", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Journey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Into The Light", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Coverdale"], AlbumArtUrl = imgUrl }, + new Album { Title = "Introspective", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Pet Shop Boys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Iron Maiden", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "ISAM", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Amon Tobin"], AlbumArtUrl = imgUrl }, + new Album { Title = "IV", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jagged Little Pill", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Alanis Morissette"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jagged Little Pill", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alanis Morissette"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jorge Ben Jor 25 Anos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Jorge Ben"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jota Quest-1995", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Jota Quest"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kick", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["INXS"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kill 'Em All", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kind of Blue", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "King For A Day Fool For A Lifetime", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Faith No More"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kiss", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Carly Rae Jepsen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Last Call", Genre = genres["Country"], Price = 8.99M, Artist = artists["Cayouche"], AlbumArtUrl = imgUrl }, + new Album { Title = "Le Freak", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Chic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Le Tigre", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Le Tigre"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin III", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Let There Be Rock", Genre = genres["Rock"], Price = 8.99M, Artist = artists["AC/DC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Little Earthquakes", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Tori Amos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live [Disc 1]", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live [Disc 2]", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live After Death", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live At Donington 1992 (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live At Donington 1992 (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live on Earth", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["The Cat Empire"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live On Two Legs [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Living After Midnight", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Judas Priest"], AlbumArtUrl = imgUrl }, + new Album { Title = "Living", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paddy Casey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Load", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Love Changes Everything", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "MacArthur Park Suite", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Donna Summer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Machine Head", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Magical Mystery Tour", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mais Do Mesmo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Legião Urbana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Maquinarama", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Skank"], AlbumArtUrl = imgUrl }, + new Album { Title = "Marasim", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Jagjit Singh"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mascagni: Cavalleria Rusticana", Genre = genres["Classical"], Price = 8.99M, Artist = artists["James Levine"], AlbumArtUrl = imgUrl }, + new Album { Title = "Master of Puppets", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mechanics & Mathematics", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Venus Hum"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mental Jewelry", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Live"], AlbumArtUrl = imgUrl }, + new Album { Title = "Metallics", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "meteora", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Linkin Park"], AlbumArtUrl = imgUrl }, + new Album { Title = "Meus Momentos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Gonzaguinha"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mezmerize", Genre = genres["Metal"], Price = 8.99M, Artist = artists["System Of A Down"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mezzanine", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Massive Attack"], AlbumArtUrl = imgUrl }, + new Album { Title = "Miles Ahead", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Milton Nascimento Ao Vivo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Milton Nascimento"], AlbumArtUrl = imgUrl }, + new Album { Title = "Minas", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Milton Nascimento"], AlbumArtUrl = imgUrl }, + new Album { Title = "Minha Historia", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Buarque"], AlbumArtUrl = imgUrl }, + new Album { Title = "Misplaced Childhood", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Marillion"], AlbumArtUrl = imgUrl }, + new Album { Title = "MK III The Final Concerts [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Morning Dance", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Spyro Gyra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Motley Crue Greatest Hits", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Mötley Crüe"], AlbumArtUrl = imgUrl }, + new Album { Title = "Moving Pictures", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rush"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mozart: Chamber Music", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Nash Ensemble"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mozart: Symphonies Nos. 40 & 41", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Berliner Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Murder Ballads", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Nick Cave and the Bad Seeds"], AlbumArtUrl = imgUrl }, + new Album { Title = "Music For The Jilted Generation", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["The Prodigy"], AlbumArtUrl = imgUrl }, + new Album { Title = "My Generation - The Very Best Of The Who", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Who"], AlbumArtUrl = imgUrl }, + new Album { Title = "My Name is Skrillex", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Skrillex"], AlbumArtUrl = imgUrl }, + new Album { Title = "Na Pista", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cláudio Zoli"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nevermind", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Nirvana"], AlbumArtUrl = imgUrl }, + new Album { Title = "New Adventures In Hi-Fi", Genre = genres["Rock"], Price = 8.99M, Artist = artists["R.E.M."], AlbumArtUrl = imgUrl }, + new Album { Title = "New Divide", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Linkin Park"], AlbumArtUrl = imgUrl }, + new Album { Title = "New York Dolls", Genre = genres["Punk"], Price = 8.99M, Artist = artists["New York Dolls"], AlbumArtUrl = imgUrl }, + new Album { Title = "News Of The World", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nielsen: The Six Symphonies", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Göteborgs Symfoniker & Neeme Järvi"], AlbumArtUrl = imgUrl }, + new Album { Title = "Night At The Opera", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Night Castle", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Trans-Siberian Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nkolo", Genre = genres["World"], Price = 8.99M, Artist = artists["Lokua Kanza"], AlbumArtUrl = imgUrl }, + new Album { Title = "No More Tears (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "No Prayer For The Dying", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "No Security", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "O Brother, Where Art Thou?", Genre = genres["Country"], Price = 8.99M, Artist = artists["Alison Krauss"], AlbumArtUrl = imgUrl }, + new Album { Title = "O Samba Poconé", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Skank"], AlbumArtUrl = imgUrl }, + new Album { Title = "O(+>", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Prince"], AlbumArtUrl = imgUrl }, + new Album { Title = "Oceania", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Smashing Pumpkins"], AlbumArtUrl = imgUrl }, + new Album { Title = "Off the Deep End", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Weird Al"], AlbumArtUrl = imgUrl }, + new Album { Title = "OK Computer", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Radiohead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Olodum", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Olodum"], AlbumArtUrl = imgUrl }, + new Album { Title = "One Love", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["David Guetta"], AlbumArtUrl = imgUrl }, + new Album { Title = "Operation: Mindcrime", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Queensrÿche"], AlbumArtUrl = imgUrl }, + new Album { Title = "Opiate", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Outbreak", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Dennis Chambers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pachelbel: Canon & Gigue", Genre = genres["Classical"], Price = 8.99M, Artist = artists["English Concert & Trevor Pinnock"], AlbumArtUrl = imgUrl }, + new Album { Title = "Paid in Full", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Eric B. and Rakim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Para Siempre", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Vicente Fernandez"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pause", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Four Tet"], AlbumArtUrl = imgUrl }, + new Album { Title = "Peace Sells... but Who's Buying", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Megadeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti [Disc 2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Piece Of Mind", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pinkerton", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Weezer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Plays Metallica By Four Cellos", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Apocalyptica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pop", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Powerslave", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prenda Minha", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Caetano Veloso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Presence", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pretty Hate Machine", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Nine Inch Nails"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prisoner", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Jezabels"], AlbumArtUrl = imgUrl }, + new Album { Title = "Privateering", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Mark Knopfler"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prokofiev: Romeo & Juliet", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Michael Tilson Thomas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prokofiev: Symphony No.1", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sergei Prokofiev & Yuri Temirkanov"], AlbumArtUrl = imgUrl }, + new Album { Title = "PSY's Best 6th Part 1", Genre = genres["Pop"], Price = 8.99M, Artist = artists["PSY"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purcell: The Fairy Queen", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Classical Players"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purpendicular", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purple", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Stone Temple Pilots"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quanta Gente Veio Ver (Live)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Gilberto Gil"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quanta Gente Veio ver--Bônus De Carnaval", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Gilberto Gil"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quiet Songs", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Aisha Duo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raices", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Los Tigres del Norte"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raising Hell", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Run DMC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raoul and the Kings of Spain ", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tears For Fears"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rattle And Hum", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raul Seixas", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Raul Seixas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Recovery [Explicit]", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Eminem"], AlbumArtUrl = imgUrl }, + new Album { Title = "Reign In Blood", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Slayer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Relayed", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Yes"], AlbumArtUrl = imgUrl }, + new Album { Title = "ReLoad", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Respighi:Pines of Rome", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Eugene Ormandy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Restless and Wild", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Accept"], AlbumArtUrl = imgUrl }, + new Album { Title = "Retrospective I (1974-1980)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rush"], AlbumArtUrl = imgUrl }, + new Album { Title = "Revelations", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Audioslave"], AlbumArtUrl = imgUrl }, + new Album { Title = "Revolver", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ride the Lighting ", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ride The Lightning", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ring My Bell", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Anita Ward"], AlbumArtUrl = imgUrl }, + new Album { Title = "Riot Act", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rise of the Phoenix", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Before the Dawn"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD1]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD2]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Roda De Funk", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Funk Como Le Gusta"], AlbumArtUrl = imgUrl }, + new Album { Title = "Room for Squares", Genre = genres["Pop"], Price = 8.99M, Artist = artists["John Mayer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Root Down", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Jimmy Smith"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rounds", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Four Tet"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rubber Factory", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Black Keys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rust in Peace", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Megadeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sambas De Enredo 2001", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Santana - As Years Go By", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Santana Live", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Saturday Night Fever", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Bee Gees"], AlbumArtUrl = imgUrl }, + new Album { Title = "Scary Monsters and Nice Sprites", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Skrillex"], AlbumArtUrl = imgUrl }, + new Album { Title = "Scheherazade", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Chicago Symphony Orchestra & Fritz Reiner"], AlbumArtUrl = imgUrl }, + new Album { Title = "SCRIABIN: Vers la flamme", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Christopher O'Riley"], AlbumArtUrl = imgUrl }, + new Album { Title = "Second Coming", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Stone Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serie Sem Limite (Disc 1)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Tim Maia"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serie Sem Limite (Disc 2)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Tim Maia"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serious About Men", Genre = genres["Rap"], Price = 8.99M, Artist = artists["The Rubberbandits"], AlbumArtUrl = imgUrl }, + new Album { Title = "Seventh Son of a Seventh Son", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Short Bus", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Filter"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sibelius: Finlandia", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Berliner Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Singles Collection", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Bowie"], AlbumArtUrl = imgUrl }, + new Album { Title = "Six Degrees of Inner Turbulence", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Dream Theater"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slave To The Empire", Genre = genres["Metal"], Price = 8.99M, Artist = artists["T&N"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slaves And Masters", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slouching Towards Bethlehem", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Robert James"], AlbumArtUrl = imgUrl }, + new Album { Title = "Smash", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Offspring"], AlbumArtUrl = imgUrl }, + new Album { Title = "Something Special", Genre = genres["Country"], Price = 8.99M, Artist = artists["Dolly Parton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Somewhere in Time", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Song(s) You Know By Heart", Genre = genres["Country"], Price = 8.99M, Artist = artists["Jimmy Buffett"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sound of Music", Genre = genres["Punk"], Price = 8.99M, Artist = artists["Adicts"], AlbumArtUrl = imgUrl }, + new Album { Title = "South American Getaway", Genre = genres["Classical"], Price = 8.99M, Artist = artists["The 12 Cellists of The Berlin Philharmonic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sozinho Remix Ao Vivo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Caetano Veloso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Speak of the Devil", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Spiritual State", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Nujabes"], AlbumArtUrl = imgUrl }, + new Album { Title = "St. Anger", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Still Life", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Opeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stop Making Sense", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Talking Heads"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stormbringer", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stranger than Fiction", Genre = genres["Punk"], Price = 8.99M, Artist = artists["Bad Religion"], AlbumArtUrl = imgUrl }, + new Album { Title = "Strauss: Waltzes", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Eugene Ormandy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Supermodified", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Amon Tobin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Supernatural", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Surfing with the Alien (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Joe Satriani"], AlbumArtUrl = imgUrl }, + new Album { Title = "Switched-On Bach", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Wendy Carlos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Symphony", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Szymanowski: Piano Works, Vol. 1", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Martin Roscoe"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tchaikovsky: The Nutcracker", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Symphony Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ted Nugent", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ted Nugent"], AlbumArtUrl = imgUrl }, + new Album { Title = "Teflon Don", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Rick Ross"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tell Another Joke at the Ol' Choppin' Block", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Danielson Famile"], AlbumArtUrl = imgUrl }, + new Album { Title = "Temple of the Dog", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Temple of the Dog"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ten", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Texas Flood", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Stevie Ray Vaughan"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Battle Rages On", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Beast Live", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paul D'Ianno"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of 1980-1990", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of 1990–2000", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of Beethoven", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Nicolaus Esterhazy Sinfonia"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of Billy Cobham", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Billy Cobham"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of Ed Motta", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Ed Motta"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of Van Halen, Vol. I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Bridge", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Melanie Fiona"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cage", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tygers of Pan Tang"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Chicago Transit Authority", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Chicago "], AlbumArtUrl = imgUrl }, + new Album { Title = "The Chronic", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Dr. Dre"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Colour And The Shape", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Crane Wife", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["The Decemberists"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cream Of Clapton", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cure", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Cure"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Dark Side Of The Moon", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Divine Conspiracy", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Epica"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Doors", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Doors"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Dream of the Blue Turtles", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Sting"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Essential Miles Davis [Disc 1]", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Essential Miles Davis [Disc 2]", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Final Concerts (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Final Frontier", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Head and the Heart", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Head and the Heart"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Joshua Tree", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Last Night of the Proms", Genre = genres["Classical"], Price = 8.99M, Artist = artists["BBC Concert Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Lumineers", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Lumineers"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Number of The Beast", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Number of The Beast", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Police Greatest Hits", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Police"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Song Remains The Same (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Song Remains The Same (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Southern Harmony and Musical Companion", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Spade", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Butch Walker & The Black Widows"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Stone Roses", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Stone Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Suburbs", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Arcade Fire"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Three Tenors Disc1/Disc2", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Carreras, Pavarotti, Domingo"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Trees They Grow So High", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Wall", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "The X Factor", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Them Crooked Vultures", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Them Crooked Vultures"], AlbumArtUrl = imgUrl }, + new Album { Title = "This Is Happening", Genre = genres["Rock"], Price = 8.99M, Artist = artists["LCD Soundsystem"], AlbumArtUrl = imgUrl }, + new Album { Title = "Thunder, Lightning, Strike", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Go! Team"], AlbumArtUrl = imgUrl }, + new Album { Title = "Time to Say Goodbye", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Time, Love & Tenderness", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Michael Bolton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tomorrow Starts Today", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Mobile"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tribute", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tuesday Night Music Club", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Sheryl Crow"], AlbumArtUrl = imgUrl }, + new Album { Title = "Umoja", Genre = genres["Rock"], Price = 8.99M, Artist = artists["BLØF"], AlbumArtUrl = imgUrl }, + new Album { Title = "Under the Pink", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Tori Amos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Undertow", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Un-Led-Ed", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Dread Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Kiss"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Untrue", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Burial"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion II", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Van Halen III", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Van Halen", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Version 2.0", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Garbage"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vinicius De Moraes", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Vinícius De Moraes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Virtual XI", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Voodoo Lounge", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vozes do MPB", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vs.", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wagner: Favourite Overtures", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sir Georg Solti & Wiener Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Walking Into Clarksdale", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Page & Plant"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wapi Yo", Genre = genres["World"], Price = 8.99M, Artist = artists["Lokua Kanza"], AlbumArtUrl = imgUrl }, + new Album { Title = "War", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Warner 25 Anos", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Antônio Carlos Jobim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wasteland R&Btheque", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Raunchy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Watermark", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Enya"], AlbumArtUrl = imgUrl }, + new Album { Title = "We Were Exploding Anyway", Genre = genres["Rock"], Price = 8.99M, Artist = artists["65daysofstatic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Weill: The Seven Deadly Sins", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Orchestre de l'Opéra de Lyon"], AlbumArtUrl = imgUrl }, + new Album { Title = "White Pony", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deftones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Who's Next", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Who"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wish You Were Here", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "With Oden on Our Side", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Amon Amarth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Worlds", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Aaron Goldberg"], AlbumArtUrl = imgUrl }, + new Album { Title = "Worship Music", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Anthrax"], AlbumArtUrl = imgUrl }, + new Album { Title = "X&Y", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Coldplay"], AlbumArtUrl = imgUrl }, + new Album { Title = "Xinti", Genre = genres["World"], Price = 8.99M, Artist = artists["Sara Tavares"], AlbumArtUrl = imgUrl }, + new Album { Title = "Yano", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Yano"], AlbumArtUrl = imgUrl }, + new Album { Title = "Yesterday Once More Disc 1/Disc 2", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Carpenters"], AlbumArtUrl = imgUrl }, + new Album { Title = "Zooropa", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Zoso", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + }; + + // TODO [EF] Swap to store generated keys when available + int albumId = 1; + foreach (var album in albums) + { + album.AlbumId = albumId++; + album.ArtistId = album.Artist.ArtistId; + album.GenreId = album.Genre.GenreId; + } + + return albums; + } + + private static Dictionary artists; + public static Dictionary Artists + { + get + { + if (artists == null) + { + var artistsList = new Artist[] + { + new Artist { Name = "65daysofstatic" }, + new Artist { Name = "Aaron Goldberg" }, + new Artist { Name = "Above & Beyond" }, + new Artist { Name = "Above the Fold" }, + new Artist { Name = "AC/DC" }, + new Artist { Name = "Accept" }, + new Artist { Name = "Adicts" }, + new Artist { Name = "Adrian Leaper & Doreen de Feis" }, + new Artist { Name = "Aerosmith" }, + new Artist { Name = "Aisha Duo" }, + new Artist { Name = "Al di Meola" }, + new Artist { Name = "Alabama Shakes" }, + new Artist { Name = "Alanis Morissette" }, + new Artist { Name = "Alberto Turco & Nova Schola Gregoriana" }, + new Artist { Name = "Alice in Chains" }, + new Artist { Name = "Alison Krauss" }, + new Artist { Name = "Amon Amarth" }, + new Artist { Name = "Amon Tobin" }, + new Artist { Name = "Amr Diab" }, + new Artist { Name = "Amy Winehouse" }, + new Artist { Name = "Anita Ward" }, + new Artist { Name = "Anthrax" }, + new Artist { Name = "Antônio Carlos Jobim" }, + new Artist { Name = "Apocalyptica" }, + new Artist { Name = "Aqua" }, + new Artist { Name = "Armand Van Helden" }, + new Artist { Name = "Arcade Fire" }, + new Artist { Name = "Audioslave" }, + new Artist { Name = "Bad Religion" }, + new Artist { Name = "Barenaked Ladies" }, + new Artist { Name = "BBC Concert Orchestra" }, + new Artist { Name = "Bee Gees" }, + new Artist { Name = "Before the Dawn" }, + new Artist { Name = "Berliner Philharmoniker" }, + new Artist { Name = "Billy Cobham" }, + new Artist { Name = "Black Label Society" }, + new Artist { Name = "Black Sabbath" }, + new Artist { Name = "BLØF" }, + new Artist { Name = "Blues Traveler" }, + new Artist { Name = "Boston Symphony Orchestra & Seiji Ozawa" }, + new Artist { Name = "Britten Sinfonia, Ivor Bolton & Lesley Garrett" }, + new Artist { Name = "Bruce Dickinson" }, + new Artist { Name = "Buddy Guy" }, + new Artist { Name = "Burial" }, + new Artist { Name = "Butch Walker & The Black Widows" }, + new Artist { Name = "Caetano Veloso" }, + new Artist { Name = "Cake" }, + new Artist { Name = "Calexico" }, + new Artist { Name = "Carly Rae Jepsen" }, + new Artist { Name = "Carreras, Pavarotti, Domingo" }, + new Artist { Name = "Cássia Eller" }, + new Artist { Name = "Cayouche" }, + new Artist { Name = "Chic" }, + new Artist { Name = "Chicago " }, + new Artist { Name = "Chicago Symphony Orchestra & Fritz Reiner" }, + new Artist { Name = "Chico Buarque" }, + new Artist { Name = "Chico Science & Nação Zumbi" }, + new Artist { Name = "Choir Of Westminster Abbey & Simon Preston" }, + new Artist { Name = "Chris Cornell" }, + new Artist { Name = "Christopher O'Riley" }, + new Artist { Name = "Cidade Negra" }, + new Artist { Name = "Cláudio Zoli" }, + new Artist { Name = "Coldplay" }, + new Artist { Name = "Creedence Clearwater Revival" }, + new Artist { Name = "Crosby, Stills, Nash, and Young" }, + new Artist { Name = "Daft Punk" }, + new Artist { Name = "Danielson Famile" }, + new Artist { Name = "David Bowie" }, + new Artist { Name = "David Coverdale" }, + new Artist { Name = "David Guetta" }, + new Artist { Name = "deadmau5" }, + new Artist { Name = "Deep Purple" }, + new Artist { Name = "Def Leppard" }, + new Artist { Name = "Deftones" }, + new Artist { Name = "Dennis Chambers" }, + new Artist { Name = "Deva Premal" }, + new Artist { Name = "Dio" }, + new Artist { Name = "Djavan" }, + new Artist { Name = "Dolly Parton" }, + new Artist { Name = "Donna Summer" }, + new Artist { Name = "Dr. Dre" }, + new Artist { Name = "Dread Zeppelin" }, + new Artist { Name = "Dream Theater" }, + new Artist { Name = "Duck Sauce" }, + new Artist { Name = "Earl Scruggs" }, + new Artist { Name = "Ed Motta" }, + new Artist { Name = "Edo de Waart & San Francisco Symphony" }, + new Artist { Name = "Elis Regina" }, + new Artist { Name = "Eminem" }, + new Artist { Name = "English Concert & Trevor Pinnock" }, + new Artist { Name = "Enya" }, + new Artist { Name = "Epica" }, + new Artist { Name = "Eric B. and Rakim" }, + new Artist { Name = "Eric Clapton" }, + new Artist { Name = "Eugene Ormandy" }, + new Artist { Name = "Faith No More" }, + new Artist { Name = "Falamansa" }, + new Artist { Name = "Filter" }, + new Artist { Name = "Foo Fighters" }, + new Artist { Name = "Four Tet" }, + new Artist { Name = "Frank Zappa & Captain Beefheart" }, + new Artist { Name = "Fretwork" }, + new Artist { Name = "Funk Como Le Gusta" }, + new Artist { Name = "Garbage" }, + new Artist { Name = "Gerald Moore" }, + new Artist { Name = "Gilberto Gil" }, + new Artist { Name = "Godsmack" }, + new Artist { Name = "Gonzaguinha" }, + new Artist { Name = "Göteborgs Symfoniker & Neeme Järvi" }, + new Artist { Name = "Guns N' Roses" }, + new Artist { Name = "Gustav Mahler" }, + new Artist { Name = "In This Moment" }, + new Artist { Name = "Incognito" }, + new Artist { Name = "INXS" }, + new Artist { Name = "Iron Maiden" }, + new Artist { Name = "Jagjit Singh" }, + new Artist { Name = "James Levine" }, + new Artist { Name = "Jamiroquai" }, + new Artist { Name = "Jimi Hendrix" }, + new Artist { Name = "Jimmy Buffett" }, + new Artist { Name = "Jimmy Smith" }, + new Artist { Name = "Joe Satriani" }, + new Artist { Name = "John Digweed" }, + new Artist { Name = "John Mayer" }, + new Artist { Name = "Jorge Ben" }, + new Artist { Name = "Jota Quest" }, + new Artist { Name = "Journey" }, + new Artist { Name = "Judas Priest" }, + new Artist { Name = "Julian Bream" }, + new Artist { Name = "Justice" }, + new Artist { Name = "Orchestre de l'Opéra de Lyon" }, + new Artist { Name = "King Crimson" }, + new Artist { Name = "Kiss" }, + new Artist { Name = "LCD Soundsystem" }, + new Artist { Name = "Le Tigre" }, + new Artist { Name = "Led Zeppelin" }, + new Artist { Name = "Legião Urbana" }, + new Artist { Name = "Lenny Kravitz" }, + new Artist { Name = "Les Arts Florissants & William Christie" }, + new Artist { Name = "Limp Bizkit" }, + new Artist { Name = "Linkin Park" }, + new Artist { Name = "Live" }, + new Artist { Name = "Lokua Kanza" }, + new Artist { Name = "London Symphony Orchestra" }, + new Artist { Name = "Los Tigres del Norte" }, + new Artist { Name = "Luciana Souza/Romero Lubambo" }, + new Artist { Name = "Lulu Santos" }, + new Artist { Name = "Lura" }, + new Artist { Name = "Marcos Valle" }, + new Artist { Name = "Marillion" }, + new Artist { Name = "Marisa Monte" }, + new Artist { Name = "Mark Knopfler" }, + new Artist { Name = "Martin Roscoe" }, + new Artist { Name = "Massive Attack" }, + new Artist { Name = "Maurizio Pollini" }, + new Artist { Name = "Megadeth" }, + new Artist { Name = "Mela Tenenbaum, Pro Musica Prague & Richard Kapp" }, + new Artist { Name = "Melanie Fiona" }, + new Artist { Name = "Men At Work" }, + new Artist { Name = "Metallica" }, + new Artist { Name = "M-Flo" }, + new Artist { Name = "Michael Bolton" }, + new Artist { Name = "Michael Tilson Thomas" }, + new Artist { Name = "Miles Davis" }, + new Artist { Name = "Milton Nascimento" }, + new Artist { Name = "Mobile" }, + new Artist { Name = "Modest Mouse" }, + new Artist { Name = "Mötley Crüe" }, + new Artist { Name = "Motörhead" }, + new Artist { Name = "Mumford & Sons" }, + new Artist { Name = "Munkle" }, + new Artist { Name = "Nash Ensemble" }, + new Artist { Name = "Neil Young" }, + new Artist { Name = "New York Dolls" }, + new Artist { Name = "Nick Cave and the Bad Seeds" }, + new Artist { Name = "Nicolaus Esterhazy Sinfonia" }, + new Artist { Name = "Nine Inch Nails" }, + new Artist { Name = "Nirvana" }, + new Artist { Name = "Norah Jones" }, + new Artist { Name = "Nujabes" }, + new Artist { Name = "O Terço" }, + new Artist { Name = "Oasis" }, + new Artist { Name = "Olodum" }, + new Artist { Name = "Opeth" }, + new Artist { Name = "Orchestra of The Age of Enlightenment" }, + new Artist { Name = "Os Paralamas Do Sucesso" }, + new Artist { Name = "Ozzy Osbourne" }, + new Artist { Name = "Paddy Casey" }, + new Artist { Name = "Page & Plant" }, + new Artist { Name = "Papa Wemba" }, + new Artist { Name = "Paul D'Ianno" }, + new Artist { Name = "Paul Oakenfold" }, + new Artist { Name = "Paul Van Dyk" }, + new Artist { Name = "Pearl Jam" }, + new Artist { Name = "Pet Shop Boys" }, + new Artist { Name = "Pink Floyd" }, + new Artist { Name = "Plug" }, + new Artist { Name = "Porcupine Tree" }, + new Artist { Name = "Portishead" }, + new Artist { Name = "Prince" }, + new Artist { Name = "Projected" }, + new Artist { Name = "PSY" }, + new Artist { Name = "Public Enemy" }, + new Artist { Name = "Queen" }, + new Artist { Name = "Queensrÿche" }, + new Artist { Name = "R.E.M." }, + new Artist { Name = "Radiohead" }, + new Artist { Name = "Rancid" }, + new Artist { Name = "Raul Seixas" }, + new Artist { Name = "Raunchy" }, + new Artist { Name = "Red Hot Chili Peppers" }, + new Artist { Name = "Rick Ross" }, + new Artist { Name = "Robert James" }, + new Artist { Name = "London Classical Players" }, + new Artist { Name = "Royal Philharmonic Orchestra" }, + new Artist { Name = "Run DMC" }, + new Artist { Name = "Rush" }, + new Artist { Name = "Santana" }, + new Artist { Name = "Sara Tavares" }, + new Artist { Name = "Sarah Brightman" }, + new Artist { Name = "Sasha" }, + new Artist { Name = "Scholars Baroque Ensemble" }, + new Artist { Name = "Scorpions" }, + new Artist { Name = "Sergei Prokofiev & Yuri Temirkanov" }, + new Artist { Name = "Sheryl Crow" }, + new Artist { Name = "Sir Georg Solti & Wiener Philharmoniker" }, + new Artist { Name = "Skank" }, + new Artist { Name = "Skrillex" }, + new Artist { Name = "Slash" }, + new Artist { Name = "Slayer" }, + new Artist { Name = "Soul-Junk" }, + new Artist { Name = "Soundgarden" }, + new Artist { Name = "Spyro Gyra" }, + new Artist { Name = "Stevie Ray Vaughan & Double Trouble" }, + new Artist { Name = "Stevie Ray Vaughan" }, + new Artist { Name = "Sting" }, + new Artist { Name = "Stone Temple Pilots" }, + new Artist { Name = "Styx" }, + new Artist { Name = "Sufjan Stevens" }, + new Artist { Name = "Supreme Beings of Leisure" }, + new Artist { Name = "System Of A Down" }, + new Artist { Name = "T&N" }, + new Artist { Name = "Talking Heads" }, + new Artist { Name = "Tears For Fears" }, + new Artist { Name = "Ted Nugent" }, + new Artist { Name = "Temple of the Dog" }, + new Artist { Name = "Terry Bozzio, Tony Levin & Steve Stevens" }, + new Artist { Name = "The 12 Cellists of The Berlin Philharmonic" }, + new Artist { Name = "The Axis of Awesome" }, + new Artist { Name = "The Beatles" }, + new Artist { Name = "The Black Crowes" }, + new Artist { Name = "The Black Keys" }, + new Artist { Name = "The Carpenters" }, + new Artist { Name = "The Cat Empire" }, + new Artist { Name = "The Cult" }, + new Artist { Name = "The Cure" }, + new Artist { Name = "The Decemberists" }, + new Artist { Name = "The Doors" }, + new Artist { Name = "The Eagles of Death Metal" }, + new Artist { Name = "The Go! Team" }, + new Artist { Name = "The Head and the Heart" }, + new Artist { Name = "The Jezabels" }, + new Artist { Name = "The King's Singers" }, + new Artist { Name = "The Lumineers" }, + new Artist { Name = "The Offspring" }, + new Artist { Name = "The Police" }, + new Artist { Name = "The Posies" }, + new Artist { Name = "The Prodigy" }, + new Artist { Name = "The Rolling Stones" }, + new Artist { Name = "The Rubberbandits" }, + new Artist { Name = "The Smashing Pumpkins" }, + new Artist { Name = "The Stone Roses" }, + new Artist { Name = "The Who" }, + new Artist { Name = "Them Crooked Vultures" }, + new Artist { Name = "TheStart" }, + new Artist { Name = "Thievery Corporation" }, + new Artist { Name = "Tiësto" }, + new Artist { Name = "Tim Maia" }, + new Artist { Name = "Ton Koopman" }, + new Artist { Name = "Tool" }, + new Artist { Name = "Tori Amos" }, + new Artist { Name = "Trampled By Turtles" }, + new Artist { Name = "Trans-Siberian Orchestra" }, + new Artist { Name = "Tygers of Pan Tang" }, + new Artist { Name = "U2" }, + new Artist { Name = "UB40" }, + new Artist { Name = "Uh Huh Her " }, + new Artist { Name = "Van Halen" }, + new Artist { Name = "Various Artists" }, + new Artist { Name = "Velvet Revolver" }, + new Artist { Name = "Venus Hum" }, + new Artist { Name = "Vicente Fernandez" }, + new Artist { Name = "Vinícius De Moraes" }, + new Artist { Name = "Weezer" }, + new Artist { Name = "Weird Al" }, + new Artist { Name = "Wendy Carlos" }, + new Artist { Name = "Wilhelm Kempff" }, + new Artist { Name = "Yano" }, + new Artist { Name = "Yehudi Menuhin" }, + new Artist { Name = "Yes" }, + new Artist { Name = "Yo-Yo Ma" }, + new Artist { Name = "Zeca Pagodinho" }, + new Artist { Name = "אריק אינשטיין"} + }; + + // TODO [EF] Swap to store generated keys when available + int artistId = 1; + artists = new Dictionary(); + foreach (Artist artist in artistsList) + { + artist.ArtistId = artistId++; + artists.Add(artist.Name, artist); + } + } + + return artists; + } + } + + private static Dictionary genres; + public static Dictionary Genres + { + get + { + if (genres == null) + { + var genresList = new Genre[] + { + new Genre { Name = "Pop" }, + new Genre { Name = "Rock" }, + new Genre { Name = "Jazz" }, + new Genre { Name = "Metal" }, + new Genre { Name = "Electronic" }, + new Genre { Name = "Blues" }, + new Genre { Name = "Latin" }, + new Genre { Name = "Rap" }, + new Genre { Name = "Classical" }, + new Genre { Name = "Alternative" }, + new Genre { Name = "Country" }, + new Genre { Name = "R&B" }, + new Genre { Name = "Indie" }, + new Genre { Name = "Punk" }, + new Genre { Name = "World" } + }; + + genres = new Dictionary(); + // TODO [EF] Swap to store generated keys when available + int genreId = 1; + foreach (Genre genre in genresList) + { + genre.GenreId = genreId++; + + // TODO [EF] Remove when null values are supported by update pipeline + genre.Description = genre.Name + " is great music (if you like it)."; + + genres.Add(genre.Name, genre); + } + } + + return genres; + } + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Models/ShoppingCart.cs b/src/MusicStore.Spa/Models/ShoppingCart.cs new file mode 100644 index 0000000000..69223596a3 --- /dev/null +++ b/src/MusicStore.Spa/Models/ShoppingCart.cs @@ -0,0 +1,208 @@ +using Microsoft.AspNet.Http; +using Microsoft.Data.Entity; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MusicStore.Models +{ + public partial class ShoppingCart + { + MusicStoreContext _db; + string ShoppingCartId { get; set; } + + public ShoppingCart(MusicStoreContext db) + { + _db = db; + } + + public static ShoppingCart GetCart(MusicStoreContext db, HttpContext context) + { + var cart = new ShoppingCart(db); + cart.ShoppingCartId = cart.GetCartId(context); + return cart; + } + + public void AddToCart(Album album) + { + // Get the matching cart and album instances + var cartItem = _db.CartItems.SingleOrDefault( + c => c.CartId == ShoppingCartId + && c.AlbumId == album.AlbumId); + + if (cartItem == null) + { + // TODO [EF] Swap to store generated key once we support identity pattern + var nextCartItemId = _db.CartItems.Any() + ? _db.CartItems.Max(c => c.CartItemId) + 1 + : 1; + + // Create a new cart item if no cart item exists + cartItem = new CartItem + { + CartItemId = nextCartItemId, + AlbumId = album.AlbumId, + CartId = ShoppingCartId, + Count = 1, + DateCreated = DateTime.Now + }; + + _db.CartItems.Add(cartItem); + } + else + { + // If the item does exist in the cart, then add one to the quantity + cartItem.Count++; + + // TODO [EF] Remove this line once change detection is available + _db.ChangeTracker.Entry(cartItem).State = EntityState.Modified; + } + } + + public int RemoveFromCart(int id) + { + // Get the cart + var cartItem = _db.CartItems.Single( + cart => cart.CartId == ShoppingCartId + && cart.CartItemId == id); + + int itemCount = 0; + + if (cartItem != null) + { + if (cartItem.Count > 1) + { + cartItem.Count--; + + // TODO [EF] Remove this line once change detection is available + _db.ChangeTracker.Entry(cartItem).State = EntityState.Modified; + + itemCount = cartItem.Count; + } + else + { + _db.CartItems.Remove(cartItem); + } + } + + return itemCount; + } + + public void EmptyCart() + { + var cartItems = _db.CartItems.Where(cart => cart.CartId == ShoppingCartId); + + foreach (var cartItem in cartItems) + { + // TODO [EF] Change to DbSet.Remove once querying attaches instances + _db.ChangeTracker.Entry(cartItem).State = EntityState.Deleted; + } + } + + public List GetCartItems() + { + var cartItems = _db.CartItems.Where(cart => cart.CartId == ShoppingCartId).ToList(); + //TODO: Auto population of the related album data not available until EF feature is lighted up. + foreach (var cartItem in cartItems) + { + cartItem.Album = _db.Albums.Single(a => a.AlbumId == cartItem.AlbumId); + } + + return cartItems; + } + + public int GetCount() + { + // Get the count of each item in the cart and sum them up + int? count = (from cartItems in _db.CartItems + where cartItems.CartId == ShoppingCartId + select (int?)cartItems.Count).Sum(); + + // Return 0 if all entries are null + return count ?? 0; + } + + public decimal GetTotal() + { + // Multiply album price by count of that album to get + // the current price for each of those albums in the cart + // sum all album price totals to get the cart total + + // TODO Collapse to a single query once EF supports querying related data + decimal total = 0; + foreach (var item in _db.CartItems.Where(c => c.CartId == ShoppingCartId)) + { + var album = _db.Albums.Single(a => a.AlbumId == item.AlbumId); + total += item.Count * album.Price; + } + + return total; + } + + public int CreateOrder(Order order) + { + decimal orderTotal = 0; + + var cartItems = GetCartItems(); + + // TODO [EF] Swap to store generated identity key when supported + var nextId = _db.OrderDetails.Any() + ? _db.OrderDetails.Max(o => o.OrderDetailId) + 1 + : 1; + + // Iterate over the items in the cart, adding the order details for each + foreach (var item in cartItems) + { + //var album = _db.Albums.Find(item.AlbumId); + var album = _db.Albums.Single(a => a.AlbumId == item.AlbumId); + + var orderDetail = new OrderDetail + { + OrderDetailId = nextId, + AlbumId = item.AlbumId, + OrderId = order.OrderId, + UnitPrice = album.Price, + Quantity = item.Count, + }; + + // Set the order total of the shopping cart + orderTotal += (item.Count * album.Price); + + _db.OrderDetails.Add(orderDetail); + + nextId++; + } + + // Set the order's total to the orderTotal count + order.Total = orderTotal; + + // Empty the shopping cart + EmptyCart(); + + // Return the OrderId as the confirmation number + return order.OrderId; + } + + // We're using HttpContextBase to allow access to cookies. + public string GetCartId(HttpContext context) + { + var sessionCookie = context.Request.Cookies.Get("Session"); + string cartId = null; + + if (string.IsNullOrWhiteSpace(sessionCookie)) + { + //A GUID to hold the cartId. + cartId = Guid.NewGuid().ToString(); + + // Send cart Id as a cookie to the client. + context.Response.Cookies.Append("Session", cartId); + } + else + { + cartId = sessionCookie; + } + + return cartId; + } + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/MusicStore.Spa.kproj b/src/MusicStore.Spa/MusicStore.Spa.kproj new file mode 100644 index 0000000000..0f7a09080c --- /dev/null +++ b/src/MusicStore.Spa/MusicStore.Spa.kproj @@ -0,0 +1,3567 @@ + + + + + 12.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + true + 1.0 + false + + + Debug + AnyCPU + + + + 9bceb29b-7e09-4b4c-a466-498efc602331 + Web + + + + + MusicStore + + + 2.0 + + + 2452 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/MusicStore.Spa/Project.json b/src/MusicStore.Spa/Project.json new file mode 100644 index 0000000000..216896dc0e --- /dev/null +++ b/src/MusicStore.Spa/Project.json @@ -0,0 +1,26 @@ +{ + "compilationOptions" : { "define" : ["DEBUG"] }, + "dependencies": { + "Helios" : "0.1-alpha-*", + "Microsoft.AspNet.Mvc" : "0.1-alpha-*", + "Microsoft.AspNet.StaticFiles" : "0.1-alpha-*", + "Microsoft.Data.Entity.Relational": "0.1-alpha-*", + "Microsoft.Data.Entity.InMemory": "0.1-alpha-*", + "Microsoft.Data.Entity.SqlServer": "0.1-alpha-*", + "Microsoft.AspNet.Security.Cookies": "0.1-alpha-*", + "Microsoft.AspNet.Identity.Security": "0.1-alpha-*", + "Microsoft.AspNet.Identity.Entity": "0.1-alpha-*", + "Microsoft.Framework.ConfigurationModel": "0.1-alpha-*", + "Microsoft.Framework.ConfigurationModel.Json": "0.1-alpha-build-*" + }, + "configurations" : { + "net45" : { + "dependencies": { + "System.Runtime" : "", + "System.Data": "", + "System.ComponentModel.DataAnnotations": "" + } + }, + "k10" : { } + } +} diff --git a/src/MusicStore.Spa/Startup.cs b/src/MusicStore.Spa/Startup.cs new file mode 100644 index 0000000000..ed6d073e72 --- /dev/null +++ b/src/MusicStore.Spa/Startup.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.FileSystems; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Security; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.Security.Cookies; +using Microsoft.AspNet.StaticFiles; +using Microsoft.Data.Entity; +using Microsoft.Framework.ConfigurationModel; +using Microsoft.Framework.DependencyInjection; +using MusicStore.Models; + +namespace MusicStore.Spa +{ + public class Startup + { + public void Configure(IBuilder app) + { + var configuration = new Configuration() + .AddJsonFile("Config.json") + .AddEnvironmentVariables(); + + app.UseServices(services => + { + // Add options accessors to the service container + services.SetupOptions(options => + { + options.DefaultAdminUserName = configuration.Get("DefaultAdminUsername"); + options.DefaultAdminPassword = configuration.Get("DefaultAdminPassword"); + options.UseSqlServer(configuration.Get("Data:IdentityConnection:ConnectionString")); + }); + + services.SetupOptions(options => + options.UseSqlServer(configuration.Get("Data:DefaultConnection:ConnectionString"))); + + // Add MVC services to the service container + services.AddMvc(); + + // Add EF services to the service container + services.AddEntityFramework() + .AddSqlServer(); + + // Add Identity services to the service container + services.AddIdentity() + .AddEntityFramework() + .AddHttpSignIn(); + + // Add application services to the service container + services.AddScoped(); + services.AddTransient(typeof(IHtmlHelper<>), typeof(AngularHtmlHelper<>)); + }); + + // Initialize the sample data + SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait(); + SampleData.InitializeIdentityDatabaseAsync(app.ApplicationServices).Wait(); + + // Configure the HTTP request pipeline + + // Add cookie auth + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, + LoginPath = new PathString("/Account/Login") + }); + + // Add static files + app.UseStaticFiles(new StaticFileOptions { FileSystem = new PhysicalFileSystem("wwwroot") }); + + // Add MVC + app.UseMvc(routes => + { + // TODO: Move these back to attribute routes when they're available + routes.MapRoute(null, "api/genres/menu", new { controller = "GenresApi", action = "GenreMenuList" }); + routes.MapRoute(null, "api/genres", new { controller = "GenresApi", action = "GenreList" }); + routes.MapRoute(null, "api/genres/{genreId}/albums", new { controller = "GenresApi", action = "GenreAlbums" }); + routes.MapRoute(null, "api/albums/mostPopular", new { controller = "AlbumsApi", action = "MostPopular" }); + routes.MapRoute(null, "api/albums/all", new { controller = "AlbumsApi", action = "All" }); + routes.MapRoute(null, "api/albums/{albumId}", new { controller = "AlbumsApi", action = "Details" }); + routes.MapRoute(null, "{controller}/{action}", new { controller = "Home", action = "Index" }); + }); + } + } +} diff --git a/src/MusicStore.Spa/Views/Account/Login.cshtml b/src/MusicStore.Spa/Views/Account/Login.cshtml new file mode 100644 index 0000000000..f5a68c0df2 --- /dev/null +++ b/src/MusicStore.Spa/Views/Account/Login.cshtml @@ -0,0 +1,104 @@ +@model MusicStore.Models.LoginViewModel + +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Log in"; + ViewBag.ngApp = "MusicStore.Store"; +} + +@section NavBarItems { + +
  • + @*@Html.InlineData("GenreMenuList", "GenresApi")*@ + +} + +

    @ViewBag.Title.

    + +
    +
    +
    + @using (Html.BeginForm("Login", "Account", new { ReturnUrl = ViewBag.ReturnUrl }, FormMethod.Post, + new { + @class = "form-horizontal", + role = "form", + novalidate = "", + name = "login", + app_prevent_submit = "login.$invalid", + ng_submit = "login.submitAttempted=true" + })) + { + @Html.AntiForgeryToken() +

    Use a local account to log in.

    +
    + @Html.ValidationSummary(true) + +
    + @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngTextBoxFor(m => m.UserName, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.UserName, "login", new { @class = "help-block field-validation-error" }) +
    +
    + + @* What this might look like using Tag Helpers: + <@div class="form-group" validation-for="UserName" validation-form-name="login" validation-class="has-error"> + <@label for="UserName" class="col-md-2 control-label"> +
    + <@input for="UserName" class="form-control" /> + <@span validation-for="UserName" validation-form-name="login" class="field-validation-error"> +
    + + *@ + +
    + @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.Password, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.Password, "login", new { @class = "help-block field-validation-error" }) +
    +
    + +
    +
    +
    + @Html.CheckBoxFor(m => m.RememberMe) + @Html.LabelFor(m => m.RememberMe) +
    +
    +
    + +
    +
    + +
    +
    + +

    + @Html.ActionLink("Register", "Register") if you don't have a local account. +

    + } +
    +
    +
    + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + @* + *@ + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Views/Account/Manage.cshtml b/src/MusicStore.Spa/Views/Account/Manage.cshtml new file mode 100644 index 0000000000..fef49f927d --- /dev/null +++ b/src/MusicStore.Spa/Views/Account/Manage.cshtml @@ -0,0 +1,21 @@ +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Manage Account"; +} + +

    @ViewBag.Title.

    +

    @ViewBag.StatusMessage

    + +
    +
    + @await Html.PartialAsync("_ChangePasswordPartial") +
    +
    + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Views/Account/Register.cshtml b/src/MusicStore.Spa/Views/Account/Register.cshtml new file mode 100644 index 0000000000..b6a0891300 --- /dev/null +++ b/src/MusicStore.Spa/Views/Account/Register.cshtml @@ -0,0 +1,46 @@ +@model MusicStore.Models.RegisterViewModel +@{ + //TODO: Until we have a way to specify the layout page at application level. + Layout = "/Views/Shared/_Layout.cshtml"; + ViewBag.Title = "Register"; +} + +

    @ViewBag.Title.

    + +@using (Html.BeginForm("Register", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" })) +{ + @Html.AntiForgeryToken() +

    Create a new account.

    +
    + @Html.ValidationSummary() +
    + @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label" }) +
    + @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" }) +
    +
    +
    + @Html.LabelFor(m => m.Password, new { @class = "col-md-2 control-label" }) +
    + @Html.PasswordFor(m => m.Password, new { @class = "form-control" }) +
    +
    +
    + @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
    + @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
    +
    +
    +
    + +
    +
    +} + +@section Scripts { + @*TODO : Until script helpers are available, adding script references manually*@ + @*@Scripts.Render("~/bundles/jqueryval")*@ + + +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml b/src/MusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml new file mode 100644 index 0000000000..c3e2573be9 --- /dev/null +++ b/src/MusicStore.Spa/Views/Account/_ChangePasswordPartial.cshtml @@ -0,0 +1,65 @@ +@using System.Security.Principal +@model MusicStore.Models.ManageUserViewModel + +

    You're logged in as @Context.HttpContext.User.Identity.GetUserName().

    + +@using (Html.BeginForm("Manage", "Account", FormMethod.Post, + new +{ + @class = "form-horizontal", + role = "form", + novalidate = "", + name = "changePassword", + app_prevent_submit = "changePassword.$invalid", + ng_submit = "changePassword.submitAttempted=true" +})) +{ + @Html.AntiForgeryToken() +

    Change Password

    +
    + @Html.ValidationSummary() + +
    + @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.OldPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.OldPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.NewPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.NewPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    + @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" }) +
    +
    +
    + @Html.ngPasswordFor(m => m.ConfirmPassword, new { @class = "form-control" }) +
    +
    + @Html.ngValidationMessageFor(m => m.ConfirmPassword, "changePassword", new { @class = "help-block field-validation-error" }) +
    +
    + +
    +
    + +
    +
    +} + +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Views/Home/Index.cshtml b/src/MusicStore.Spa/Views/Home/Index.cshtml new file mode 100644 index 0000000000..ba62b7f966 --- /dev/null +++ b/src/MusicStore.Spa/Views/Home/Index.cshtml @@ -0,0 +1,26 @@ +@{ + ViewBag.Title = "Home Page"; + ViewBag.ngApp = "MusicStore.Store"; + Layout = "/Views/Shared/_Layout.cshtml"; +} + +@section NavBarItems { + +
  • +@*@Html.InlineData("GenreMenuList", "GenresApi")*@ + +} + +
    + +@*@Html.InlineData("MostPopular", "AlbumsApi")*@ + +@section Scripts { + + + +@* TODO: This is currently all the compiled TypeScript, non-minified. Need to explore options + for alternate loading schemes, e.g. AMD loader of individual modules, min vs. non-min, etc. *@ + + +} \ No newline at end of file diff --git a/src/MusicStore.Spa/Views/Shared/_Layout.cshtml b/src/MusicStore.Spa/Views/Shared/_Layout.cshtml new file mode 100644 index 0000000000..0e460fc4fe --- /dev/null +++ b/src/MusicStore.Spa/Views/Shared/_Layout.cshtml @@ -0,0 +1,51 @@ + + + + + + @ViewBag.Title – MVC Music Store + + + + + + +
    + @RenderBody() +
    + +
    + + @* TODO: Need to figure out best way to switch these to min links for release, e.g. new helper, + Grunt task to replace, CDN support, etc. *@ + + @**@ + + + @RenderSection("scripts", required: false) + + diff --git a/src/MusicStore.Spa/Views/Shared/_LoginPartial.cshtml b/src/MusicStore.Spa/Views/Shared/_LoginPartial.cshtml new file mode 100644 index 0000000000..8c70f2d26b --- /dev/null +++ b/src/MusicStore.Spa/Views/Shared/_LoginPartial.cshtml @@ -0,0 +1,46 @@ +@using System.Security.Principal +@using Microsoft.AspNet.Identity + +@{ + //Func js = input => Html.Raw(HttpUtility.JavaScriptStringEncode(input, false)); +} + +@if (User.Identity.IsAuthenticated) +{ + using (Html.BeginForm("LogOff", "Account", FormMethod.Post, new { id = "logoutForm", @class = "navbar-right" })) + { + @Html.AntiForgeryToken() + + + + @*@Html.Json(new { + isAuthenticated = true, + userName = User.Identity.GetUserName(), + userId = User.Identity.GetUserId(), + roles = ((System.Security.Claims.ClaimsPrincipal)User).Claims + .Where(c => c.Type == System.Security.Claims.ClaimTypes.Role) + .Select(role => role.Value) + }, + new { id = "userDetails" })*@ + } +} +else +{ + + + @*@Html.Json(new { + isAuthenticated = false, + userName = (string)null, + userId = (string)null, + roles = Enumerable.Empty() + }, + new { id = "userDetails" })*@ +} diff --git a/src/MusicStore.Spa/bower.json b/src/MusicStore.Spa/bower.json new file mode 100644 index 0000000000..224510d8ae --- /dev/null +++ b/src/MusicStore.Spa/bower.json @@ -0,0 +1,17 @@ +{ + "name": "MvcMusicStore", + "version": "0.0.0", + "private": true, + "dependencies": { + "bootstrap": "~3.1.0", + "jquery.validation": "~1.11.1", + "jquery": "~2.1.0", + "modernizr": "~2.7.1", + "respond": "~1.4.2", + "dt-angular": "~1.2.15", + "angular": "~1.2.15", + "angular-route": "~1.2.15", + "angular-bootstrap": "~0.10.0", + "dt-angular-ui-bootstrap": "*" + } +} diff --git a/src/MusicStore.Spa/package.json b/src/MusicStore.Spa/package.json new file mode 100644 index 0000000000..87481b1d2d --- /dev/null +++ b/src/MusicStore.Spa/package.json @@ -0,0 +1,16 @@ +{ + "name": "MusicStore", + "version": "0.0.0", + "devDependencies": { + "grunt": "~0.4.2", + "grunt-contrib-jshint": "~0.10.0", + "grunt-contrib-uglify": "~0.4.0", + "grunt-contrib-watch": "~0.6.1", + "grunt-contrib-copy": "~0.5.0", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-less": "~0.11.0", + "grunt-typescript": "~0.3.6", + "grunt-tslint": "~0.4.1", + "grunt-tsng": "~0.1.3" + } +} \ No newline at end of file diff --git a/src/MusicStore.Spa/tslint.json b/src/MusicStore.Spa/tslint.json new file mode 100644 index 0000000000..975f4ff5c5 --- /dev/null +++ b/src/MusicStore.Spa/tslint.json @@ -0,0 +1,48 @@ +{ + "rules": { + "class-name": true, + "curly": true, + "eofline": false, + "forin": true, + "indent": [true, 4], + "label-position": true, + "label-undefined": true, + "max-line-length": [true, 140], + "no-arg": true, + "no-bitwise": true, + "no-console": [true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-key": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-string-literal": true, + "no-trailing-whitespace": true, + "no-unreachable": true, + "one-line": [true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [true, "double"], + "radix": true, + "semicolon": true, + "triple-equals": [true, "allow-null-check"], + "variable-name": false, + "whitespace": [true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file diff --git a/src/MusicStore/Models/Genre.cs b/src/MusicStore/Models/Genre.cs index 53b1e9c6f0..29c9107187 100644 --- a/src/MusicStore/Models/Genre.cs +++ b/src/MusicStore/Models/Genre.cs @@ -9,7 +9,9 @@ namespace MusicStore.Models [Required] public string Name { get; set; } + public string Description { get; set; } + public List Albums { get; set; } } } \ No newline at end of file diff --git a/src/MusicStore/Models/OrderDetail.cs b/src/MusicStore/Models/OrderDetail.cs index 29f87988df..7e92c4681c 100644 --- a/src/MusicStore/Models/OrderDetail.cs +++ b/src/MusicStore/Models/OrderDetail.cs @@ -3,12 +3,17 @@ public class OrderDetail { public int OrderDetailId { get; set; } + public int OrderId { get; set; } + public int AlbumId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } public virtual Album Album { get; set; } + public virtual Order Order { get; set; } } } \ No newline at end of file diff --git a/src/MusicStore/MusicStore.kproj b/src/MusicStore/MusicStore.kproj index 1a0ef98df2..1330dda5ac 100644 --- a/src/MusicStore/MusicStore.kproj +++ b/src/MusicStore/MusicStore.kproj @@ -16,6 +16,9 @@ 2.0 + + 41532 + diff --git a/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj b/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj index 03a8234ad0..4e2ea5fb39 100644 --- a/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj +++ b/src/MvcMusicStore.Spa/MvcMusicStore.Spa.csproj @@ -247,46 +247,45 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 10.0