Merge branch 'master' of github.com:aspnet/AspNetCore into rybrande/MondoMaster

This commit is contained in:
Ryan Brandenburg 2018-11-19 16:01:22 -08:00
commit 78eb188014
183 changed files with 38944 additions and 7 deletions

4
.gitmodules vendored
View File

@ -50,10 +50,6 @@
path = modules/MetaPackages
url = https://github.com/aspnet/MetaPackages.git
branch = master
[submodule "modules/MusicStore"]
path = modules/MusicStore
url = https://github.com/aspnet/MusicStore.git
branch = master
[submodule "modules/Mvc"]
path = modules/Mvc
url = https://github.com/aspnet/Mvc.git

View File

@ -30,7 +30,7 @@
<RepositoryBuildOrder Include="Identity" Order="15" />
<RepositoryBuildOrder Include="JavaScriptServices" Order="15" />
<RepositoryBuildOrder Include="AzureIntegration" Order="15" RootPath="$(RepositoryRoot)src\AzureIntegration\" />
<RepositoryBuildOrder Include="MusicStore" Order="16" />
<RepositoryBuildOrder Include="MusicStore" Order="16" RootPath="$(RepositoryRoot)src\MusicStore\" />
<RepositoryBuildOrder Include="SignalR" Order="16" />
<RepositoryBuildOrder Include="AuthSamples" Order="16" RootPath="$(RepositoryRoot)src\AuthSamples\" />
<RepositoryBuildOrder Include="Templating" Order="17" RootPath="$(RepositoryRoot)src\Templating\" />

View File

@ -63,7 +63,7 @@
<!-- Test-only repos -->
<Repository Include="AuthSamples" RootPath="$(RepositoryRoot)src\AuthSamples\" PatchPolicy="AlwaysUpdateAndCascadeVersions" />
<Repository Include="MusicStore" PatchPolicy="AlwaysUpdateAndCascadeVersions" />
<Repository Include="MusicStore" RootPath="$(RepositoryRoot)src\MusicStore\" PatchPolicy="AlwaysUpdateAndCascadeVersions" />
<Repository Include="ServerTests" PatchPolicy="AlwaysUpdateAndCascadeVersions" />
</ItemGroup>
</Project>

@ -1 +0,0 @@
Subproject commit aca4b432e157c5e9063f551af8bff3de7641b744

43
src/MusicStore/.gitignore vendored Normal file
View File

@ -0,0 +1,43 @@
[Oo]bj/
[Bb]in/
.vs/
*.xap
*.user
/TestResults
*.vspscc
*.vssscc
*.suo
*.cache
*.docstates
*.log
_ReSharper.*
*.csproj.user
*[Rr]e[Ss]harper.user
_ReSharper.*/
packages/*
artifacts/*
PublishProfiles/
*.psess
*.vsp
*.pidb
*.userprefs
*DS_Store
*.ncrunchsolution
*.log
*.vspx
/.symbols
nuget.exe
*net45.csproj
*k10.csproj
App_Data/
bower_components
node_modules
*.sln.ide
*.ng.ts
*.sln.ide
.build/
.testpublish/
launchSettings.json
.vscode/
TestResults/
global.json

View File

@ -0,0 +1,12 @@
<Project>
<Import
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
<Import Project="build\dependencies.props" />
<Import Project="build\sources.props" />
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<!-- This is the one repo where we plan to continue testing all the TFMs for regression coverage. -->
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp3.0' ">$(MicrosoftNETCoreApp30PackageVersion)</RuntimeFrameworkVersion>
<NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
<!-- aspnet/BuildTools#662 Don't police what version of NetCoreApp we use -->
<NETCoreAppMaximumVersion>99.9</NETCoreAppMaximumVersion>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,69 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26730.12
MinimumVisualStudioVersion = 15.0.26730.03
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7D749BDA-4638-4517-B66A-D40DEDEEB141}"
ProjectSection(SolutionItems) = preProject
.appveyor.yml = .appveyor.yml
.travis.yml = .travis.yml
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
NuGet.config = NuGet.config
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B7B176B6-8D4D-4EF1-BBD2-DDA650C78FFF}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{363D2681-31A6-48C9-90BB-9ACFF4A41F06}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicStore", "samples\MusicStore\MusicStore.csproj", "{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicStore.Test", "test\MusicStore.Test\MusicStore.Test.csproj", "{CA663205-77DE-4E55-B300-85594181B5A9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MusicStore.E2ETests", "test\MusicStore.E2ETests\MusicStore.E2ETests.csproj", "{72A5F455-121F-4954-BF28-D712C6BE88EA}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{88A30728-49E5-46C5-9CEC-9D8FD346A043}"
ProjectSection(SolutionItems) = preProject
build\dependencies.props = build\dependencies.props
build\repo.props = build\repo.props
build\repo.targets = build\repo.targets
build\sources.props = build\sources.props
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
RuntimeStore|Any CPU = RuntimeStore|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.Release|Any CPU.Build.0 = Release|Any CPU
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.RuntimeStore|Any CPU.ActiveCfg = RuntimeStore|Any CPU
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0}.RuntimeStore|Any CPU.Build.0 = RuntimeStore|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.Release|Any CPU.Build.0 = Release|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.RuntimeStore|Any CPU.ActiveCfg = Release|Any CPU
{CA663205-77DE-4E55-B300-85594181B5A9}.RuntimeStore|Any CPU.Build.0 = Release|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.Release|Any CPU.Build.0 = Release|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.RuntimeStore|Any CPU.ActiveCfg = Release|Any CPU
{72A5F455-121F-4954-BF28-D712C6BE88EA}.RuntimeStore|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3CFBED5D-2ED8-49DB-96FB-BDAA748DC5A0} = {B7B176B6-8D4D-4EF1-BBD2-DDA650C78FFF}
{CA663205-77DE-4E55-B300-85594181B5A9} = {363D2681-31A6-48C9-90BB-9ACFF4A41F06}
{72A5F455-121F-4954-BF28-D712C6BE88EA} = {363D2681-31A6-48C9-90BB-9ACFF4A41F06}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4AC7A44E-260A-4E70-88A7-77A0027E12A5}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,7 @@
{
"Default": {
"rules": [
"DefaultCompositeRule"
]
}
}

20
src/MusicStore/README.md Normal file
View File

@ -0,0 +1,20 @@
MusicStore (test application)
=============================
AppVeyor: [![AppVeyor][appveyor-badge]][appveyor-build]
Travis: [![Travis][travis-badge]][travis-build]
[appveyor-badge]: https://ci.appveyor.com/api/projects/status/ja8a7j6jscj7k3xa/branch/dev?svg=true
[appveyor-build]: https://ci.appveyor.com/project/aspnetci/MusicStore/branch/dev
[travis-badge]: https://travis-ci.org/aspnet/MusicStore.svg?branch=dev
[travis-build]: https://travis-ci.org/aspnet/MusicStore
This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
## About this repo
This repository is a test application used for ASP.NET Core internal test processes.
It is not intended to be a representative sample of how to use ASP.NET Core.
Samples and docs for ASP.NET Core can be found here: <https://docs.asp.net>.

View File

@ -0,0 +1,49 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<MicrosoftAspNetCoreAppPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAppPackageVersion>
<MicrosoftAspNetCoreAspNetCoreModulePackageVersion>2.2.0-rtm-10670</MicrosoftAspNetCoreAspNetCoreModulePackageVersion>
<MicrosoftAspNetCoreAspNetCoreModuleV2PackageVersion>2.2.0-rtm-10670</MicrosoftAspNetCoreAspNetCoreModuleV2PackageVersion>
<MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationCookiesPackageVersion>
<MicrosoftAspNetCoreAuthenticationCorePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationCorePackageVersion>
<MicrosoftAspNetCoreAuthenticationFacebookPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationFacebookPackageVersion>
<MicrosoftAspNetCoreAuthenticationGooglePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationGooglePackageVersion>
<MicrosoftAspNetCoreAuthenticationMicrosoftAccountPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationMicrosoftAccountPackageVersion>
<MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>
<MicrosoftAspNetCoreAuthenticationTwitterPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreAuthenticationTwitterPackageVersion>
<MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion>
<MicrosoftAspNetCoreIdentityPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreIdentityPackageVersion>
<MicrosoftAspNetCoreMvcPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreMvcPackageVersion>
<MicrosoftAspNetCorePackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCorePackageVersion>
<MicrosoftAspNetCoreServerHttpSysPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreServerHttpSysPackageVersion>
<MicrosoftAspNetCoreServerIISPackageVersion>2.2.0-rtm-10670</MicrosoftAspNetCoreServerIISPackageVersion>
<MicrosoftAspNetCoreServerIntegrationTestingIISPackageVersion>2.2.0-rtm-10670</MicrosoftAspNetCoreServerIntegrationTestingIISPackageVersion>
<MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>0.7.0-a-alpha1-3-0-tfm-17161</MicrosoftAspNetCoreServerIntegrationTestingPackageVersion>
<MicrosoftAspNetCoreSessionPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreSessionPackageVersion>
<MicrosoftAspNetCoreStaticFilesPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreStaticFilesPackageVersion>
<MicrosoftAspNetCoreWebUtilitiesPackageVersion>3.0.0-alpha1-10670</MicrosoftAspNetCoreWebUtilitiesPackageVersion>
<MicrosoftEntityFrameworkCoreInMemoryPackageVersion>3.0.0-alpha1-10670</MicrosoftEntityFrameworkCoreInMemoryPackageVersion>
<MicrosoftEntityFrameworkCoreSqlServerPackageVersion>3.0.0-alpha1-10670</MicrosoftEntityFrameworkCoreSqlServerPackageVersion>
<MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>3.0.0-alpha1-10657</MicrosoftExtensionsCommandLineUtilsSourcesPackageVersion>
<MicrosoftExtensionsConfigurationBinderPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsConfigurationBinderPackageVersion>
<MicrosoftExtensionsConfigurationCommandLinePackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsConfigurationCommandLinePackageVersion>
<MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsConfigurationEnvironmentVariablesPackageVersion>
<MicrosoftExtensionsConfigurationJsonPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsConfigurationJsonPackageVersion>
<MicrosoftExtensionsConfigurationUserSecretsPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsConfigurationUserSecretsPackageVersion>
<MicrosoftExtensionsLoggingConsolePackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsLoggingConsolePackageVersion>
<MicrosoftExtensionsLoggingPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsLoggingPackageVersion>
<MicrosoftExtensionsLoggingTestingPackageVersion>3.0.0-alpha1-10670</MicrosoftExtensionsLoggingTestingPackageVersion>
<MicrosoftNETCoreApp30PackageVersion>3.0.0-preview1-26907-05</MicrosoftNETCoreApp30PackageVersion>
<MicrosoftNETSdkRazorPackageVersion>3.0.0-alpha1-10670</MicrosoftNETSdkRazorPackageVersion>
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
<SystemDataSqlClientPackageVersion>4.6.0-preview1-26907-04</SystemDataSqlClientPackageVersion>
<XunitPackageVersion>2.3.1</XunitPackageVersion>
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
</PropertyGroup>
<PropertyGroup Label="Package Versions: Pinned" />
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
</Project>

View File

@ -0,0 +1,14 @@
<Project>
<Import Project="dependencies.props" />
<PropertyGroup>
<!-- These properties are use by the automation that updates dependencies.props -->
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
</PropertyGroup>
<ItemGroup>
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp30PackageVersion)" />
<DotNetCoreRuntime Condition="'$(OS)' == 'Windows_NT'" Include="$(MicrosoftNETCoreApp30PackageVersion)" Arch="x86" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,39 @@
<Project>
<ItemGroup>
<Solutions Update="$(RepositoryRoot)MusicStore.sln" Condition="'$(RUN_RUNTIME_STORE_TESTS)' == 'true'">
<AdditionalProperties>Configuration=RuntimeStore</AdditionalProperties>
</Solutions>
</ItemGroup>
<PropertyGroup>
<MusicStoreE2ETestProject>$(RepositoryRoot)test\MusicStore.E2ETests\MusicStore.E2ETests.csproj</MusicStoreE2ETestProject>
</PropertyGroup>
<Target Name="_FilterTestProjects" BeforeTargets="TestProjects"
Condition="'$(RUN_TESTS_ON_NANO)'=='true' Or '$(RUN_RUNTIME_STORE_TESTS)'=='true'">
<Warning Text="Updated test projects to run only the project '$(MusicStoreE2ETestProject)' as the variable 'RUN_TESTS_ON_NANO' is set to '$(RUN_TESTS_ON_NANO)'" Condition="'$(RUN_TESTS_ON_NANO)'=='true'" />
<Warning Text="Updated test projects to run only the project '$(MusicStoreE2ETestProject)' as the variable 'RUN_RUNTIME_STORE_TESTS' is set to '$(RUN_RUNTIME_STORE_TESTS)'" Condition="'$(RUN_RUNTIME_STORE_TESTS)'=='true'" />
<ItemGroup>
<ProjectsToTest Remove="@(ProjectsToTest)" />
<ProjectsToTest Include="$(MusicStoreE2ETestProject)" />
</ItemGroup>
<Error Text="Could not find test projects to run" Condition="@(ProjectsToTest->Count()) == 0" />
<ItemGroup Condition="'$(RUN_TESTS_ON_NANO)'=='true'">
<ProjectsToTest Update="$(MusicStoreE2ETestProject)">
<AdditionalProperties>VSTestTestCaseFilter=E2ETests=NanoServer</AdditionalProperties>
</ProjectsToTest>
</ItemGroup>
<ItemGroup Condition="'$(RUN_RUNTIME_STORE_TESTS)'=='true'">
<ProjectsToTest Update="$(MusicStoreE2ETestProject)">
<AdditionalProperties>VSTestTestCaseFilter=smoketests=usestore</AdditionalProperties>
</ProjectsToTest>
</ItemGroup>
</Target>
</Project>

View File

@ -0,0 +1,17 @@
<Project>
<Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
<PropertyGroup Label="RestoreSources">
<RestoreSources>$(DotNetRestoreSources)</RestoreSources>
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
$(RestoreSources);
https://dotnet.myget.org/F/dotnet-core/api/v3/index.json;
https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json;
https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json;
</RestoreSources>
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
$(RestoreSources);
https://api.nuget.org/v3/index.json;
</RestoreSources>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,219 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using MusicStore.Models;
using MusicStore.ViewModels;
namespace MusicStore.Areas.Admin.Controllers
{
[Area("Admin")]
[Authorize("ManageStore")]
public class StoreManagerController : Controller
{
private readonly AppSettings _appSettings;
public StoreManagerController(MusicStoreContext dbContext, IOptions<AppSettings> options)
{
DbContext = dbContext;
_appSettings = options.Value;
}
public MusicStoreContext DbContext { get; }
//
// GET: /StoreManager/
public async Task<IActionResult> Index()
{
var albums = await DbContext.Albums
.Include(a => a.Genre)
.Include(a => a.Artist)
.ToListAsync();
return View(albums);
}
//
// GET: /StoreManager/Details/5
public async Task<IActionResult> Details(
[FromServices] IMemoryCache cache,
int id)
{
var cacheKey = GetCacheKey(id);
Album album;
if (!cache.TryGetValue(cacheKey, out album))
{
album = await DbContext.Albums
.Where(a => a.AlbumId == id)
.Include(a => a.Artist)
.Include(a => a.Genre)
.FirstOrDefaultAsync();
if (album != null)
{
if (_appSettings.CacheDbResults)
{
//Remove it from cache if not retrieved in last 10 minutes.
cache.Set(
cacheKey,
album,
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10)));
}
}
}
if (album == null)
{
cache.Remove(cacheKey);
return NotFound();
}
return View(album);
}
//
// GET: /StoreManager/Create
public IActionResult Create()
{
ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name");
ViewBag.ArtistId = new SelectList(DbContext.Artists, "ArtistId", "Name");
return View();
}
// POST: /StoreManager/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(
Album album,
[FromServices] IMemoryCache cache,
CancellationToken requestAborted)
{
if (ModelState.IsValid)
{
DbContext.Albums.Add(album);
await DbContext.SaveChangesAsync(requestAborted);
var albumData = new AlbumData
{
Title = album.Title,
Url = Url.Action("Details", "Store", new { id = album.AlbumId })
};
cache.Remove("latestAlbum");
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(DbContext.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
//
// GET: /StoreManager/Edit/5
public async Task<IActionResult> Edit(int id)
{
var album = await DbContext.Albums.
Where(a => a.AlbumId == id).
FirstOrDefaultAsync();
if (album == null)
{
return NotFound();
}
ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(DbContext.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
//
// POST: /StoreManager/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(
[FromServices] IMemoryCache cache,
Album album,
CancellationToken requestAborted)
{
if (ModelState.IsValid)
{
DbContext.Update(album);
await DbContext.SaveChangesAsync(requestAborted);
//Invalidate the cache entry as it is modified
cache.Remove(GetCacheKey(album.AlbumId));
return RedirectToAction("Index");
}
ViewBag.GenreId = new SelectList(DbContext.Genres, "GenreId", "Name", album.GenreId);
ViewBag.ArtistId = new SelectList(DbContext.Artists, "ArtistId", "Name", album.ArtistId);
return View(album);
}
//
// GET: /StoreManager/RemoveAlbum/5
public async Task<IActionResult> RemoveAlbum(int id)
{
var album = await DbContext.Albums.Where(a => a.AlbumId == id).FirstOrDefaultAsync();
if (album == null)
{
return NotFound();
}
return View(album);
}
//
// POST: /StoreManager/RemoveAlbum/5
[HttpPost, ActionName("RemoveAlbum")]
public async Task<IActionResult> RemoveAlbumConfirmed(
[FromServices] IMemoryCache cache,
int id,
CancellationToken requestAborted)
{
var album = await DbContext.Albums.Where(a => a.AlbumId == id).FirstOrDefaultAsync();
if (album == null)
{
return NotFound();
}
DbContext.Albums.Remove(album);
await DbContext.SaveChangesAsync(requestAborted);
//Remove the cache entry as it is removed
cache.Remove(GetCacheKey(id));
return RedirectToAction("Index");
}
private static string GetCacheKey(int id)
{
return string.Format("album_{0}", id);
}
// NOTE: this is used for end to end testing only
//
// GET: /StoreManager/GetAlbumIdFromName
// Note: Added for automated testing purpose. Application does not use this.
[HttpGet]
[SkipStatusCodePages]
[EnableCors("CorsPolicy")]
public async Task<IActionResult> GetAlbumIdFromName(string albumName)
{
var album = await DbContext.Albums.Where(a => a.Title == albumName).FirstOrDefaultAsync();
if (album == null)
{
return NotFound();
}
return Content(album.AlbumId.ToString());
}
}
}

View File

@ -0,0 +1,75 @@
@model MusicStore.Models.Album
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Album</h4>
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("GenreId", String.Empty)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ArtistId, "ArtistId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("ArtistId", String.Empty)
@Html.ValidationMessageFor(model => model.ArtistId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Price)
@Html.ValidationMessageFor(model => model.Price)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.AlbumArtUrl, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.AlbumArtUrl)
@Html.ValidationMessageFor(model => model.AlbumArtUrl)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@*TODO : Until script helpers are available, adding script references manually*@
@*@Scripts.Render("~/bundles/jqueryval")*@
<script src="@Url.Content("~/Scripts/jquery.validate.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")"></script>
}

View File

@ -0,0 +1,58 @@
@model MusicStore.Models.Album
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<div>
<h4>Album</h4>
<hr />
<dl class="dl-horizontal">
<dt>
@Html.DisplayNameFor(model => model.Artist.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Artist.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Genre.Name)
</dt>
<dd>
@Html.DisplayFor(model => model.Genre.Name)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Title)
</dt>
<dd>
@Html.DisplayFor(model => model.Title)
</dd>
<dt>
@Html.DisplayNameFor(model => model.Price)
</dt>
<dd>
@Html.DisplayFor(model => model.Price)
</dd>
<dt>
@Html.DisplayNameFor(model => model.AlbumArtUrl)
</dt>
<dd>
@Html.DisplayFor(model => model.AlbumArtUrl)
</dd>
</dl>
</div>
<p>
@Html.ActionLink("Edit", "Edit", new { id = Model.AlbumId }) |
@Html.ActionLink("Back to List", "Index")
</p>

View File

@ -0,0 +1,76 @@
@model MusicStore.Models.Album
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Album</h4>
<hr />
@Html.ValidationSummary(true)
@Html.HiddenFor(model => model.AlbumId)
<div class="form-group">
@Html.LabelFor(model => model.GenreId, "GenreId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("GenreId", String.Empty)
@Html.ValidationMessageFor(model => model.GenreId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ArtistId, "ArtistId", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.DropDownList("ArtistId", String.Empty)
@Html.ValidationMessageFor(model => model.ArtistId)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Price, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Price)
@Html.ValidationMessageFor(model => model.Price)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.AlbumArtUrl, new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.AlbumArtUrl)
@Html.ValidationMessageFor(model => model.AlbumArtUrl)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
@Html.ActionLink("Back to List", "Index")
</div>
@section Scripts {
@*TODO : Until script helpers are available, adding script references manually*@
@*@Scripts.Render("~/bundles/jqueryval")*@
<script src="@Url.Content("~/Scripts/jquery.validate.js")"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")"></script>
}

View File

@ -0,0 +1,66 @@
@model IEnumerable<MusicStore.Models.Album>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.Genre.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstOrDefault().Artist.Name)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstOrDefault().Title)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstOrDefault().Price)
</th>
<th></th>
</tr>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Genre.Name)
</td>
<td>
@if (item.Artist.Name.Length <= 25)
{
@item.Artist.Name
}
else
{
@item.Artist.Name.Substring(0, 25)<text>...</text>
}
</td>
<td>
@if (item.Title.Length <= 25)
{
@item.Title
}
else
{
@item.Title.Substring(0, 25)<text>...</text>
}
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id = item.AlbumId }) |
@Html.ActionLink("Details", "Details", new { id = item.AlbumId }) |
@Html.ActionLink("Delete", "RemoveAlbum", new { id = item.AlbumId })
</td>
</tr>
}
</table>

View File

@ -0,0 +1,29 @@
@model MusicStore.Models.Album
@{
ViewBag.Title = "Delete";
}
@if (Model != null)
{
<h2>Delete Confirmation</h2>
<p>
Are you sure you want to delete the album titled
<strong>@Model.Title</strong>?
</p>
@using (Html.BeginForm())
{
<p>
<input type="submit" value="Delete" />
</p>
<p>
@Html.ActionLink("Back to List", "Index")
</p>
}
}
else
{
@Html.Label(null, "Unable to locate the album")
}

View File

@ -0,0 +1,3 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using MusicStore.Models;
namespace MusicStore.Components
{
[ViewComponent(Name = "CartSummary")]
public class CartSummaryComponent : ViewComponent
{
public CartSummaryComponent(MusicStoreContext dbContext)
{
DbContext = dbContext;
}
private MusicStoreContext DbContext { get; }
public async Task<IViewComponentResult> InvokeAsync()
{
var cart = ShoppingCart.GetCart(DbContext, HttpContext);
var cartItems = await cart.GetCartItems();
ViewBag.CartCount = cartItems.Sum(c => c.Count);
ViewBag.CartSummary = string.Join("\n", cartItems.Select(c => c.Album.Title).Distinct());
return View();
}
}
}

View File

@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MusicStore.Models;
namespace MusicStore.Components
{
[ViewComponent(Name = "GenreMenu")]
public class GenreMenuComponent : ViewComponent
{
public GenreMenuComponent(MusicStoreContext dbContext)
{
DbContext = dbContext;
}
private MusicStoreContext DbContext { get; }
public async Task<IViewComponentResult> InvokeAsync()
{
// TODO use nested sum https://github.com/aspnet/EntityFramework/issues/3792
//.OrderByDescending(
// g => g.Albums.Sum(a => a.OrderDetails.Sum(od => od.Quantity)))
var genres = await DbContext.Genres.Select(g => g.Name).Take(9).ToListAsync();
return View(genres);
}
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace MusicStore.Components
{
/// <summary>
/// Abstracts the system clock to facilitate testing.
/// </summary>
public interface ISystemClock
{
/// <summary>
/// Gets a DateTime object that is set to the current date and time on this computer,
/// expressed as the Coordinated Universal Time(UTC)
/// </summary>
DateTime UtcNow { get; }
}
}

View File

@ -0,0 +1,22 @@
using System;
namespace MusicStore.Components
{
/// <summary>
/// Provides access to the normal system clock.
/// </summary>
public class SystemClock : ISystemClock
{
/// <inheritdoc />
public DateTime UtcNow
{
get
{
// The clock measures whole seconds only, and truncates the milliseconds,
// because millisecond resolution is inconsistent among various underlying systems.
DateTime utcNow = DateTime.UtcNow;
return utcNow.AddMilliseconds(-utcNow.Millisecond);
}
}
}
}

View File

@ -0,0 +1,512 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MusicStore.Models;
namespace MusicStore.Controllers
{
[Authorize]
public class AccountController : Controller
{
private readonly ILogger<AccountController> _logger;
public AccountController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
ILogger<AccountController> logger)
{
UserManager = userManager;
SignInManager = signInManager;
_logger = logger;
}
public UserManager<ApplicationUser> UserManager { get; }
public SignInManager<ApplicationUser> SignInManager { get; }
//
// GET: /Account/Login
[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (!ModelState.IsValid)
{
return View(model);
}
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, change to lockoutOnFailure: true
var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("Logged in {userName}.", model.Email);
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
_logger.LogWarning("Failed to log in {userName}.", model.Email);
ModelState.AddModelError("", "Invalid login attempt.");
return View(model);
}
}
//
// GET: /Account/VerifyCode
[AllowAnonymous]
public async Task<ActionResult> VerifyCode(string provider, bool rememberMe, string returnUrl = null)
{
var user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error");
}
// Remove before production
#if DEMO
if (user != null)
{
ViewBag.Code = await UserManager.GenerateTwoFactorTokenAsync(user, provider);
}
#endif
return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
//
// POST: /Account/VerifyCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> VerifyCode(VerifyCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// The following code protects for brute force attacks against the two factor codes.
// If a user enters incorrect codes for a specified amount of time then the user account
// will be locked out for a specified amount of time.
// You can configure the account lockout settings in IdentityConfig
var result = await SignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser);
if (result.Succeeded)
{
return RedirectToLocal(model.ReturnUrl);
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
ModelState.AddModelError("", "Invalid code.");
return View(model);
}
}
//
// GET: /Account/Register
[AllowAnonymous]
public IActionResult Register()
{
return View();
}
//
// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
_logger.LogInformation("User {userName} was created.", model.Email);
//Bug: Remember browser option missing?
//Uncomment this and comment the later part if account verification is not needed.
//await SignInManager.SignInAsync(user, isPersistent: false);
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
string code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
await MessageServices.SendEmailAsync(model.Email, "Confirm your account",
"Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");
#if !DEMO
return RedirectToAction("Index", "Home");
#else
//To display the email link in a friendly page instead of sending email
ViewBag.Link = callbackUrl;
return View("DemoLinkDisplay");
#endif
}
AddErrors(result);
}
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/ConfirmEmail
[AllowAnonymous]
public async Task<ActionResult> ConfirmEmail(string userId, string code)
{
if (userId == null || code == null)
{
return View("Error");
}
var user = await UserManager.FindByIdAsync(userId);
if (user == null)
{
return View("Error");
}
var result = await UserManager.ConfirmEmailAsync(user, code);
return View(result.Succeeded ? "ConfirmEmail" : "Error");
}
//
// GET: /Account/ForgotPassword
[AllowAnonymous]
public ActionResult ForgotPassword()
{
return View();
}
//
// POST: /Account/ForgotPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null || !(await UserManager.IsEmailConfirmedAsync(user)))
{
// Don't reveal that the user does not exist or is not confirmed
return View("ForgotPasswordConfirmation");
}
// For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=320771
// Send an email with this link
string code = await UserManager.GeneratePasswordResetTokenAsync(user);
var callbackUrl = Url.Action("ResetPassword", "Account", new { code = code }, protocol: HttpContext.Request.Scheme);
await MessageServices.SendEmailAsync(model.Email, "Reset Password",
"Please reset your password by clicking here: <a href=\"" + callbackUrl + "\">link</a>");
#if !DEMO
return RedirectToAction("ForgotPasswordConfirmation");
#else
//To display the email link in a friendly page instead of sending email
ViewBag.Link = callbackUrl;
return View("DemoLinkDisplay");
#endif
}
ModelState.AddModelError("", string.Format("We could not locate an account with email : {0}", model.Email));
// If we got this far, something failed, redisplay form
return View(model);
}
//
// GET: /Account/ForgotPasswordConfirmation
[AllowAnonymous]
public ActionResult ForgotPasswordConfirmation()
{
return View();
}
//
// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
//TODO: Fix this?
var resetPasswordViewModel = new ResetPasswordViewModel() { Code = code };
return code == null ? View("Error") : View(resetPasswordViewModel);
}
//
// POST: /Account/ResetPassword
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await UserManager.FindByNameAsync(model.Email);
if (user == null)
{
// Don't reveal that the user does not exist
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
var result = await UserManager.ResetPasswordAsync(user, model.Code, model.Password);
if (result.Succeeded)
{
return RedirectToAction("ResetPasswordConfirmation", "Account");
}
AddErrors(result);
return View();
}
//
// GET: /Account/ResetPasswordConfirmation
[AllowAnonymous]
public ActionResult ResetPasswordConfirmation()
{
return View();
}
//
// POST: /Account/ExternalLogin
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult ExternalLogin(string provider, string returnUrl = null)
{
// Request a redirect to the external login provider
var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
return new ChallengeResult(provider, properties);
}
//
// GET: /Account/SendCode
[AllowAnonymous]
public async Task<ActionResult> SendCode(bool rememberMe, string returnUrl = null)
{
//TODO : Default rememberMe as well?
var user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error");
}
var userFactors = await UserManager.GetValidTwoFactorProvidersAsync(user);
var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList();
return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe });
}
//
// POST: /Account/SendCode
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> SendCode(SendCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View();
}
var user = await SignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
return View("Error");
}
// Generate the token and send it
var code = await UserManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider);
if (string.IsNullOrWhiteSpace(code))
{
return View("Error");
}
var message = "Your security code is: " + code;
if (model.SelectedProvider == "Email")
{
await MessageServices.SendEmailAsync(await UserManager.GetEmailAsync(user), "Security Code", message);
}
else if (model.SelectedProvider == "Phone")
{
await MessageServices.SendSmsAsync(await UserManager.GetPhoneNumberAsync(user), message);
}
return RedirectToAction("VerifyCode", new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe });
}
//
// GET: /Account/ExternalLoginCallback
[AllowAnonymous]
public async Task<ActionResult> ExternalLoginCallback(string returnUrl = null)
{
var loginInfo = await SignInManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
var result = await SignInManager.ExternalLoginSignInAsync(loginInfo.LoginProvider, loginInfo.ProviderKey, isPersistent: false);
if (result.Succeeded)
{
return RedirectToLocal(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
}
if (result.IsLockedOut)
{
return View("Lockout");
}
else
{
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.LoginProvider;
// REVIEW: handle case where email not in claims?
var email = loginInfo.Principal.FindFirstValue(ClaimTypes.Email);
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email });
}
}
//
// POST: /Account/ExternalLoginConfirmation
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null)
{
if (SignInManager.IsSignedIn(User))
{
return RedirectToAction("Index", "Manage");
}
if (ModelState.IsValid)
{
// Get the information about the user from the external login provider
var info = await SignInManager.GetExternalLoginInfoAsync();
if (info == null)
{
return View("ExternalLoginFailure");
}
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await UserManager.CreateAsync(user);
// NOTE: Used for end to end testing only
//Just for automated testing adding a claim named 'ManageStore' - Not required for production
var manageClaim = info.Principal.Claims.Where(c => c.Type == "ManageStore").FirstOrDefault();
if (manageClaim != null)
{
await UserManager.AddClaimAsync(user, manageClaim);
}
if (result.Succeeded)
{
result = await UserManager.AddLoginAsync(user, info);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToLocal(returnUrl);
}
}
AddErrors(result);
}
ViewBag.ReturnUrl = returnUrl;
return View(model);
}
//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> LogOff()
{
var userName = HttpContext.User.Identity.Name;
// clear all items from the cart
HttpContext.Session.Clear();
await SignInManager.SignOutAsync();
// TODO: Currently SignInManager.SignOut does not sign out OpenIdc and does not have a way to pass in a specific
// AuthType to sign out.
var appEnv = HttpContext.RequestServices.GetService<IHostingEnvironment>();
if (appEnv.EnvironmentName.StartsWith("OpenIdConnect"))
{
return new SignOutResult("OpenIdConnect", new AuthenticationProperties
{
RedirectUri = Url.Action("Index", "Home")
});
}
_logger.LogInformation("{userName} logged out.", userName);
return RedirectToAction("Index", "Home");
}
//
// GET: /Account/ExternalLoginFailure
[AllowAnonymous]
public ActionResult ExternalLoginFailure()
{
return View();
}
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error.Description);
_logger.LogWarning("Error in creating user: {error}", error.Description);
}
}
private Task<ApplicationUser> GetCurrentUserAsync()
{
return UserManager.GetUserAsync(HttpContext.User);
}
private ActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
#endregion
}
}

View File

@ -0,0 +1,113 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MusicStore.Models;
namespace MusicStore.Controllers
{
[Authorize]
public class CheckoutController : Controller
{
private const string PromoCode = "FREE";
private readonly ILogger<CheckoutController> _logger;
public CheckoutController(ILogger<CheckoutController> logger)
{
_logger = logger;
}
//
// GET: /Checkout/
public IActionResult AddressAndPayment()
{
return View();
}
//
// POST: /Checkout/AddressAndPayment
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddressAndPayment(
[FromServices] MusicStoreContext dbContext,
[FromForm] Order order,
CancellationToken requestAborted)
{
if (!ModelState.IsValid)
{
return View(order);
}
var formCollection = await HttpContext.Request.ReadFormAsync();
try
{
if (string.Equals(formCollection["PromoCode"].FirstOrDefault(), PromoCode,
StringComparison.OrdinalIgnoreCase) == false)
{
return View(order);
}
else
{
order.Username = HttpContext.User.Identity.Name;
order.OrderDate = DateTime.Now;
//Add the Order
dbContext.Orders.Add(order);
//Process the order
var cart = ShoppingCart.GetCart(dbContext, HttpContext);
await cart.CreateOrder(order);
_logger.LogInformation("User {userName} started checkout of {orderId}.", order.Username, order.OrderId);
// Save all changes
await dbContext.SaveChangesAsync(requestAborted);
return RedirectToAction("Complete", new { id = order.OrderId });
}
}
catch
{
//Invalid - redisplay with errors
return View(order);
}
}
//
// GET: /Checkout/Complete
public async Task<IActionResult> Complete(
[FromServices] MusicStoreContext dbContext,
int id)
{
var userName = HttpContext.User.Identity.Name;
// Validate customer owns this order
bool isValid = await dbContext.Orders.AnyAsync(
o => o.OrderId == id &&
o.Username == userName);
if (isValid)
{
_logger.LogInformation("User {userName} completed checkout on order {orderId}.", userName, id);
return View(id);
}
else
{
_logger.LogError(
"User {userName} tried to checkout with an order ({orderId}) that doesn't belong to them.",
userName,
id);
return View("Error");
}
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using MusicStore.Models;
namespace MusicStore.Controllers
{
public class HomeController : Controller
{
private readonly AppSettings _appSettings;
public HomeController(IOptions<AppSettings> options)
{
_appSettings = options.Value;
}
//
// GET: /Home/
public async Task<IActionResult> Index(
[FromServices] MusicStoreContext dbContext,
[FromServices] IMemoryCache cache)
{
// Get most popular albums
var cacheKey = "topselling";
List<Album> albums;
if (!cache.TryGetValue(cacheKey, out albums))
{
albums = await GetTopSellingAlbumsAsync(dbContext, 6);
if (albums != null && albums.Count > 0)
{
if (_appSettings.CacheDbResults)
{
// Refresh it every 10 minutes.
// Let this be the last item to be removed by cache if cache GC kicks in.
cache.Set(
cacheKey,
albums,
new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromMinutes(10))
.SetPriority(CacheItemPriority.High));
}
}
}
return View(albums);
}
public IActionResult Error()
{
return View("~/Views/Shared/Error.cshtml");
}
public IActionResult StatusCodePage()
{
return View("~/Views/Shared/StatusCodePage.cshtml");
}
public IActionResult AccessDenied()
{
return View("~/Views/Shared/AccessDenied.cshtml");
}
private Task<List<Album>> GetTopSellingAlbumsAsync(MusicStoreContext dbContext, int count)
{
// Group the order details by album and return
// the albums with the highest count
return dbContext.Albums
.OrderByDescending(a => a.OrderDetails.Count)
.Take(count)
.ToListAsync();
}
}
}

View File

@ -0,0 +1,365 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using MusicStore.Models;
namespace MusicStore.Controllers
{
[Authorize]
public class ManageController : Controller
{
public ManageController(
UserManager<ApplicationUser> userManager,
SignInManager<ApplicationUser> signInManager,
IAuthenticationSchemeProvider schemes)
{
UserManager = userManager;
SignInManager = signInManager;
SchemeProvider = schemes;
}
public UserManager<ApplicationUser> UserManager { get; }
public SignInManager<ApplicationUser> SignInManager { get; }
public IAuthenticationSchemeProvider SchemeProvider { get; }
//
// GET: /Manage/Index
public async Task<ActionResult> Index(ManageMessageId? message = null)
{
ViewBag.StatusMessage =
message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed."
: message == ManageMessageId.SetPasswordSuccess ? "Your password has been set."
: message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set."
: message == ManageMessageId.Error ? "An error has occurred."
: message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added."
: message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed."
: "";
var user = await GetCurrentUserAsync();
var model = new IndexViewModel
{
HasPassword = await UserManager.HasPasswordAsync(user),
PhoneNumber = await UserManager.GetPhoneNumberAsync(user),
TwoFactor = await UserManager.GetTwoFactorEnabledAsync(user),
Logins = await UserManager.GetLoginsAsync(user),
BrowserRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user)
};
return View(model);
}
//
// POST: /Manage/RemoveLogin
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveLogin(string loginProvider, string providerKey)
{
ManageMessageId? message = ManageMessageId.Error;
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await UserManager.RemoveLoginAsync(user, loginProvider, providerKey);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
message = ManageMessageId.RemoveLoginSuccess;
}
}
return RedirectToAction("ManageLogins", new { Message = message });
}
//
// GET: /Account/AddPhoneNumber
public IActionResult AddPhoneNumber()
{
return View();
}
//
// POST: /Account/AddPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> AddPhoneNumber(AddPhoneNumberViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
// Generate the token and send it
var code = await UserManager.GenerateChangePhoneNumberTokenAsync(user, model.Number);
await MessageServices.SendSmsAsync(model.Number, "Your security code is: " + code);
return RedirectToAction("VerifyPhoneNumber", new { PhoneNumber = model.Number });
}
//
// POST: /Manage/EnableTwoFactorAuthentication
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableTwoFactorAuthentication()
{
var user = await GetCurrentUserAsync();
if (user != null)
{
await UserManager.SetTwoFactorEnabledAsync(user, true);
// TODO: flow remember me somehow?
await SignInManager.SignInAsync(user, isPersistent: false);
}
return RedirectToAction("Index", "Manage");
}
//
// POST: /Manage/DisableTwoFactorAuthentication
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DisableTwoFactorAuthentication()
{
var user = await GetCurrentUserAsync();
if (user != null)
{
await UserManager.SetTwoFactorEnabledAsync(user, false);
await SignInManager.SignInAsync(user, isPersistent: false);
}
return RedirectToAction("Index", "Manage");
}
//
// GET: /Account/VerifyPhoneNumber
public async Task<IActionResult> VerifyPhoneNumber(string phoneNumber)
{
// This code allows you exercise the flow without actually sending codes
// For production use please register a SMS provider in IdentityConfig and generate a code here.
#if DEMO
ViewBag.Code = await UserManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), phoneNumber);
#endif
return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber });
}
//
// POST: /Account/VerifyPhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> VerifyPhoneNumber(VerifyPhoneNumberViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await UserManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", new { Message = ManageMessageId.AddPhoneSuccess });
}
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "Failed to verify phone");
return View(model);
}
//
// GET: /Account/RemovePhoneNumber
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemovePhoneNumber()
{
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await UserManager.SetPhoneNumberAsync(user, null);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess });
}
}
return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error });
}
//
// GET: /Manage/ChangePassword
public IActionResult ChangePassword()
{
return View();
}
//
// POST: /Account/Manage
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await UserManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", new { Message = ManageMessageId.ChangePasswordSuccess });
}
AddErrors(result);
return View(model);
}
return RedirectToAction("Index", new { Message = ManageMessageId.Error });
}
//
// GET: /Manage/SetPassword
public IActionResult SetPassword()
{
return View();
}
//
// POST: /Manage/SetPassword
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SetPassword(SetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var user = await GetCurrentUserAsync();
if (user != null)
{
var result = await UserManager.AddPasswordAsync(user, model.NewPassword);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", new { Message = ManageMessageId.SetPasswordSuccess });
}
AddErrors(result);
return View(model);
}
return RedirectToAction("Index", new { Message = ManageMessageId.Error });
}
//
// POST: /Manage/RememberBrowser
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RememberBrowser()
{
var user = await GetCurrentUserAsync();
if (user != null)
{
await SignInManager.RememberTwoFactorClientAsync(user);
await SignInManager.SignInAsync(user, isPersistent: false);
}
return RedirectToAction("Index", "Manage");
}
//
// POST: /Manage/ForgetBrowser
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgetBrowser()
{
await SignInManager.ForgetTwoFactorClientAsync();
return RedirectToAction("Index", "Manage");
}
//
// GET: /Account/Manage
public async Task<IActionResult> ManageLogins(ManageMessageId? message = null)
{
ViewBag.StatusMessage =
message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed."
: message == ManageMessageId.AddLoginSuccess ? "The external login was added."
: message == ManageMessageId.Error ? "An error has occurred."
: "";
var user = await GetCurrentUserAsync();
if (user == null)
{
return View("Error");
}
var userLogins = await UserManager.GetLoginsAsync(user);
var schemes = await SchemeProvider.GetAllSchemesAsync();
var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();
ViewBag.ShowRemoveButton = user.PasswordHash != null || userLogins.Count > 1;
return View(new ManageLoginsViewModel
{
CurrentLogins = userLogins,
OtherLogins = otherLogins
});
}
//
// POST: /Manage/LinkLogin
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LinkLogin(string provider)
{
// Request a redirect to the external login provider to link a login for the current user
var redirectUrl = Url.Action("LinkLoginCallback", "Manage");
var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, UserManager.GetUserId(User));
return new ChallengeResult(provider, properties);
}
//
// GET: /Manage/LinkLoginCallback
public async Task<ActionResult> LinkLoginCallback()
{
var user = await GetCurrentUserAsync();
if (user == null)
{
return View("Error");
}
var loginInfo = await SignInManager.GetExternalLoginInfoAsync(await UserManager.GetUserIdAsync(user));
if (loginInfo == null)
{
return RedirectToAction("ManageLogins", new { Message = ManageMessageId.Error });
}
var result = await UserManager.AddLoginAsync(user, loginInfo);
var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error;
return RedirectToAction("ManageLogins", new { Message = message });
}
#region Helpers
private void AddErrors(IdentityResult result)
{
foreach (var error in result.Errors)
{
ModelState.AddModelError("", error.Description);
}
}
public enum ManageMessageId
{
AddPhoneSuccess,
AddLoginSuccess,
ChangePasswordSuccess,
SetTwoFactorSuccess,
SetPasswordSuccess,
RemoveLoginSuccess,
RemovePhoneSuccess,
Error
}
private Task<ApplicationUser> GetCurrentUserAsync()
{
return UserManager.GetUserAsync(HttpContext.User);
}
#endregion
}
}

View File

@ -0,0 +1,113 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using MusicStore.Models;
using MusicStore.ViewModels;
namespace MusicStore.Controllers
{
public class ShoppingCartController : Controller
{
private readonly ILogger<ShoppingCartController> _logger;
public ShoppingCartController(MusicStoreContext dbContext, ILogger<ShoppingCartController> logger)
{
DbContext = dbContext;
_logger = logger;
}
public MusicStoreContext DbContext { get; }
//
// GET: /ShoppingCart/
public async Task<IActionResult> Index()
{
var cart = ShoppingCart.GetCart(DbContext, HttpContext);
// Set up our ViewModel
var viewModel = new ShoppingCartViewModel
{
CartItems = await cart.GetCartItems(),
CartTotal = await cart.GetTotal()
};
// Return the view
return View(viewModel);
}
//
// GET: /ShoppingCart/AddToCart/5
public async Task<IActionResult> AddToCart(int id, CancellationToken requestAborted)
{
// Retrieve the album from the database
var addedAlbum = await DbContext.Albums
.SingleAsync(album => album.AlbumId == id);
// Add it to the shopping cart
var cart = ShoppingCart.GetCart(DbContext, HttpContext);
await cart.AddToCart(addedAlbum);
await DbContext.SaveChangesAsync(requestAborted);
_logger.LogInformation("Album {albumId} was added to the cart.", addedAlbum.AlbumId);
// Go back to the main store page for more shopping
return RedirectToAction("Index");
}
//
// AJAX: /ShoppingCart/RemoveFromCart/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RemoveFromCart(
int id,
CancellationToken requestAborted)
{
// Retrieve the current user's shopping cart
var cart = ShoppingCart.GetCart(DbContext, HttpContext);
// Get the name of the album to display confirmation
var cartItem = await DbContext.CartItems
.Where(item => item.CartItemId == id)
.Include(c => c.Album)
.SingleOrDefaultAsync();
string message;
int itemCount;
if (cartItem != null)
{
// Remove from cart
itemCount = cart.RemoveFromCart(id);
await DbContext.SaveChangesAsync(requestAborted);
string removed = (itemCount > 0) ? " 1 copy of " : string.Empty;
message = removed + cartItem.Album.Title + " has been removed from your shopping cart.";
}
else
{
itemCount = 0;
message = "Could not find this item, nothing has been removed from your shopping cart.";
}
// Display the confirmation message
var results = new ShoppingCartRemoveViewModel
{
Message = message,
CartTotal = await cart.GetTotal(),
CartCount = await cart.GetCount(),
ItemCount = itemCount,
DeleteId = id
};
_logger.LogInformation("Album {id} was removed from a cart.", id);
return Json(results);
}
}
}

View File

@ -0,0 +1,86 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using MusicStore.Models;
namespace MusicStore.Controllers
{
public class StoreController : Controller
{
private readonly AppSettings _appSettings;
public StoreController(MusicStoreContext dbContext, IOptions<AppSettings> options)
{
DbContext = dbContext;
_appSettings = options.Value;
}
public MusicStoreContext DbContext { get; }
//
// GET: /Store/
public async Task<IActionResult> Index()
{
var genres = await DbContext.Genres.ToListAsync();
return View(genres);
}
//
// GET: /Store/Browse?genre=Disco
public async Task<IActionResult> Browse(string genre)
{
// Retrieve Genre genre and its Associated associated Albums albums from database
var genreModel = await DbContext.Genres
.Include(g => g.Albums)
.Where(g => g.Name == genre)
.FirstOrDefaultAsync();
if (genreModel == null)
{
return NotFound();
}
return View(genreModel);
}
public async Task<IActionResult> Details(
[FromServices] IMemoryCache cache,
int id)
{
var cacheKey = string.Format("album_{0}", id);
Album album;
if (!cache.TryGetValue(cacheKey, out album))
{
album = await DbContext.Albums
.Where(a => a.AlbumId == id)
.Include(a => a.Artist)
.Include(a => a.Genre)
.FirstOrDefaultAsync();
if (album != null)
{
if (_appSettings.CacheDbResults)
{
//Remove it from cache if not retrieved in last 10 minutes
cache.Set(
cacheKey,
album,
new MemoryCacheEntryOptions().SetSlidingExpiration(TimeSpan.FromMinutes(10)));
}
}
}
if (album == null)
{
return NotFound();
}
return View(album);
}
}
}

View File

@ -0,0 +1,41 @@
using Microsoft.AspNetCore.Authentication;
using Newtonsoft.Json;
namespace MusicStore.Mocks.Common
{
public class CustomStateDataFormat : ISecureDataFormat<AuthenticationProperties>
{
private static string _lastSavedAuthenticationProperties;
public string Protect(AuthenticationProperties data, string purose)
{
return Protect(data);
}
public string Protect(AuthenticationProperties data)
{
_lastSavedAuthenticationProperties = Serialize(data);
return "ValidStateData";
}
public AuthenticationProperties Unprotect(string state, string purpose)
{
return Unprotect(state);
}
public AuthenticationProperties Unprotect(string state)
{
return state == "ValidStateData" ? DeSerialize(_lastSavedAuthenticationProperties) : null;
}
private string Serialize(AuthenticationProperties data)
{
return JsonConvert.SerializeObject(data, Formatting.Indented);
}
private AuthenticationProperties DeSerialize(string state)
{
return JsonConvert.DeserializeObject<AuthenticationProperties>(state);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace MusicStore.Mocks.Common
{
internal class Helpers
{
internal static void ThrowIfConditionFailed(Func<bool> condition, string errorMessage)
{
if (!condition())
{
throw new Exception(errorMessage);
}
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.AspNetCore.WebUtilities;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.Facebook
{
/// <summary>
/// Summary description for FacebookMockBackChannelHttpHandler
/// </summary>
public class FacebookMockBackChannelHttpHandler : HttpMessageHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage();
if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.facebook.com/v2.6/oauth/access_token"))
{
var formData = new FormCollection(await new FormReader(await request.Content.ReadAsStreamAsync()).ReadFormAsync());
if (formData["grant_type"] == "authorization_code")
{
if (formData["code"] == "ValidCode")
{
Helpers.ThrowIfConditionFailed(() => ((string)formData["redirect_uri"]).EndsWith("signin-facebook"), "Redirect URI is not ending with /signin-facebook");
Helpers.ThrowIfConditionFailed(() => formData["client_id"] == "[AppId]", "Invalid client Id received");
Helpers.ThrowIfConditionFailed(() => formData["client_secret"] == "[AppSecret]", "Invalid client secret received");
response.Content = new StringContent("{ \"access_token\": \"ValidAccessToken\", \"expires_in\": \"100\" }");
return response;
}
response.StatusCode = (HttpStatusCode)400;
return response;
}
}
else if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.facebook.com/v2.6/me"))
{
var queryParameters = new QueryCollection(QueryHelpers.ParseQuery(request.RequestUri.Query));
Helpers.ThrowIfConditionFailed(() => queryParameters["appsecret_proof"].Count > 0, "appsecret_proof is empty");
if (queryParameters["access_token"] == "ValidAccessToken")
{
response.Content = new StringContent("{\"id\":\"Id\",\"name\":\"AspnetvnextTest AspnetvnextTest\",\"first_name\":\"AspnetvnextTest\",\"last_name\":\"AspnetvnextTest\",\"link\":\"https:\\/\\/www.facebook.com\\/myLink\",\"username\":\"AspnetvnextTest.AspnetvnextTest.7\",\"gender\":\"male\",\"email\":\"AspnetvnextTest\\u0040test.com\",\"timezone\":-7,\"locale\":\"en_US\",\"verified\":true,\"updated_time\":\"2013-08-06T20:38:48+0000\",\"CertValidatorInvoked\":\"ValidAccessToken\"}");
}
else
{
response.Content = new StringContent("{\"error\":{\"message\":\"Invalid OAuth access token.\",\"type\":\"OAuthException\",\"code\":190}}");
}
return response;
}
throw new NotImplementedException(request.RequestUri.AbsoluteUri);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Facebook;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.Facebook
{
internal class TestFacebookEvents
{
internal static Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
if (context.Principal != null)
{
Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", "");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Email)?.Value == "AspnetvnextTest@test.com", "");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value == "Id", "");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst("urn:facebook:link")?.Value == "https://www.facebook.com/myLink", "");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Name)?.Value == "AspnetvnextTest AspnetvnextTest", "");
Helpers.ThrowIfConditionFailed(() => context.User.SelectToken("id").ToString() == context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value, "");
Helpers.ThrowIfConditionFailed(() => context.ExpiresIn.Value == TimeSpan.FromSeconds(100), "");
Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", "");
context.Principal.Identities.First().AddClaim(new Claim("ManageStore", "false"));
}
return Task.FromResult(0);
}
internal static Task OnTicketReceived(TicketReceivedContext context)
{
if (context.Principal != null && context.Options.SignInScheme == IdentityConstants.ExternalScheme)
{
//This way we will know all events were fired.
var identity = context.Principal.Identities.First();
var manageStoreClaim = identity?.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault();
if (manageStoreClaim != null)
{
identity.RemoveClaim(manageStoreClaim);
identity.AddClaim(new Claim("ManageStore", "Allowed"));
}
}
return Task.FromResult(0);
}
internal static Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context)
{
context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom");
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,55 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
namespace MusicStore.Mocks.Google
{
/// <summary>
/// Summary description for GoogleMockBackChannelHttpHandler
/// </summary>
public class GoogleMockBackChannelHttpHandler : HttpMessageHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage();
if (request.RequestUri.AbsoluteUri.StartsWith("https://www.googleapis.com/oauth2/v4/token"))
{
var formData = new FormCollection(await new FormReader(await request.Content.ReadAsStreamAsync()).ReadFormAsync());
if (formData["grant_type"] == "authorization_code")
{
if (formData["code"] == "ValidCode")
{
if (formData["redirect_uri"].Count > 0 && ((string)formData["redirect_uri"]).EndsWith("signin-google") &&
formData["client_id"] == "[ClientId]" && formData["client_secret"] == "[ClientSecret]")
{
response.Content = new StringContent("{\"access_token\":\"ValidAccessToken\",\"refresh_token\":\"ValidRefreshToken\",\"token_type\":\"Bearer\",\"expires_in\":\"1200\",\"id_token\":\"Token\"}", Encoding.UTF8, "application/json");
return response;
}
}
}
response.StatusCode = (HttpStatusCode)400;
return response;
}
else if (request.RequestUri.AbsoluteUri.StartsWith("https://www.googleapis.com/plus/v1/people/me"))
{
if (request.Headers.Authorization.Parameter == "ValidAccessToken")
{
response.Content = new StringContent("{ \"kind\": \"plus#person\",\n \"etag\": \"\\\"YFr-hUROXQN7IOa3dUHg9dQ8eq0/2hY18HdHEP8NLykSTVEiAhkKsBE\\\"\",\n \"gender\": \"male\",\n \"emails\": [\n {\n \"value\": \"AspnetvnextTest@gmail.com\",\n \"type\": \"account\"\n }\n ],\n \"objectType\": \"person\",\n \"id\": \"106790274378320830963\",\n \"displayName\": \"AspnetvnextTest AspnetvnextTest\",\n \"name\": {\n \"familyName\": \"AspnetvnextTest\",\n \"givenName\": \"FirstName\"\n },\n \"url\": \"https://plus.google.com/106790274378320830963\",\n \"image\": {\n \"url\": \"https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg?sz=50\"\n },\n \"isPlusUser\": true,\n \"language\": \"en\",\n \"circledByCount\": 0,\n \"verified\": false\n}\n", Encoding.UTF8, "application/json");
}
else
{
response.Content = new StringContent("{\"error\":{\"message\":\"Invalid OAuth access token.\",\"type\":\"OAuthException\",\"code\":190}}");
}
return response;
}
throw new NotImplementedException(request.RequestUri.AbsoluteUri);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.Google
{
internal class TestGoogleEvents
{
internal static Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
if (context.Principal != null)
{
Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", "Access token is not valid");
Helpers.ThrowIfConditionFailed(() => context.RefreshToken == "ValidRefreshToken", "Refresh token is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Email)?.Value == "AspnetvnextTest@gmail.com", "Email is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value == "106790274378320830963", "Id is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Surname)?.Value == "AspnetvnextTest", "FamilyName is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Name)?.Value == "AspnetvnextTest AspnetvnextTest", "Name is not valid");
Helpers.ThrowIfConditionFailed(() => context.ExpiresIn.Value == TimeSpan.FromSeconds(1200), "ExpiresIn is not valid");
Helpers.ThrowIfConditionFailed(() => context.User != null, "User object is not valid");
context.Principal.Identities.First().AddClaim(new Claim("ManageStore", "false"));
}
return Task.FromResult(0);
}
internal static Task OnTicketReceived(TicketReceivedContext context)
{
if (context.Principal != null && context.Options.SignInScheme == IdentityConstants.ExternalScheme)
{
//This way we will know all events were fired.
var identity = context.Principal.Identities.First();
var manageStoreClaim = identity?.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault();
if (manageStoreClaim != null)
{
identity.RemoveClaim(manageStoreClaim);
identity.AddClaim(new Claim("ManageStore", "Allowed"));
}
}
return Task.FromResult(0);
}
internal static Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context)
{
context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom");
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
namespace MusicStore.Mocks.MicrosoftAccount
{
/// <summary>
/// Summary description for MicrosoftAccountMockBackChannelHandler
/// </summary>
public class MicrosoftAccountMockBackChannelHandler : HttpMessageHandler
{
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage();
if (request.RequestUri.AbsoluteUri.StartsWith("https://login.microsoftonline.com/common/oauth2/v2.0/token"))
{
var formData = new FormCollection(await new FormReader(await request.Content.ReadAsStreamAsync()).ReadFormAsync());
if (formData["grant_type"] == "authorization_code")
{
if (formData["code"] == "ValidCode")
{
if (formData["redirect_uri"].Count > 0 && ((string)formData["redirect_uri"]).EndsWith("signin-microsoft") &&
formData["client_id"] == "[ClientId]" && formData["client_secret"] == "[ClientSecret]")
{
response.Content = new StringContent("{\"token_type\":\"bearer\",\"expires_in\":3600,\"scope\":\"https://graph.microsoft.com/user.read\",\"access_token\":\"ValidAccessToken\",\"refresh_token\":\"ValidRefreshToken\",\"authentication_token\":\"ValidAuthenticationToken\"}");
return response;
}
}
}
response.StatusCode = (HttpStatusCode)400;
return response;
}
else if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.microsoft.com/v1.0/me"))
{
if (request.Headers.Authorization.Parameter == "ValidAccessToken")
{
response.Content = new StringContent("{\r \"id\": \"fccf9a24999f4f4f\", \r \"displayName\": \"AspnetvnextTest AspnetvnextTest\", \r \"givenName\": \"AspnetvnextTest\", \r \"surname\": \"AspnetvnextTest\", \r \"link\": \"https://profile.live.com/\", \r \"gender\": null, \r \"locale\": \"en_US\", \r \"updated_time\": \"2013-08-27T22:18:14+0000\"\r}");
}
else
{
response.Content = new StringContent("{\r \"error\": {\r \"code\": \"request_token_invalid\", \r \"message\": \"The access token isn't valid.\"\r }\r}", Encoding.UTF8, "text/javascript");
}
return response;
}
throw new NotImplementedException(request.RequestUri.AbsoluteUri);
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Identity;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.MicrosoftAccount
{
internal class TestMicrosoftAccountEvents
{
internal static Task OnCreatingTicket(OAuthCreatingTicketContext context)
{
if (context.Principal != null)
{
Helpers.ThrowIfConditionFailed(() => context.AccessToken == "ValidAccessToken", "Access token is not valid");
Helpers.ThrowIfConditionFailed(() => context.RefreshToken == "ValidRefreshToken", "Refresh token is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.GivenName)?.Value == "AspnetvnextTest", "Given name is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Surname)?.Value == "AspnetvnextTest", "Surname is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value == "fccf9a24999f4f4f", "Id is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.Name)?.Value == "AspnetvnextTest AspnetvnextTest", "Name is not valid");
Helpers.ThrowIfConditionFailed(() => context.ExpiresIn.Value == TimeSpan.FromSeconds(3600), "ExpiresIn is not valid");
Helpers.ThrowIfConditionFailed(() => context.User != null, "User object is not valid");
Helpers.ThrowIfConditionFailed(() => context.Identity.FindFirst(ClaimTypes.NameIdentifier)?.Value == context.User.SelectToken("id").ToString(), "User id is not valid");
context.Principal.Identities.First().AddClaim(new Claim("ManageStore", "false"));
}
return Task.FromResult(0);
}
internal static Task OnTicketReceived(TicketReceivedContext context)
{
if (context.Principal != null && context.Options.SignInScheme == IdentityConstants.ExternalScheme)
{
//This way we will know all events were fired.
var identity = context.Principal.Identities.First();
var manageStoreClaim = identity?.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault();
if (manageStoreClaim != null)
{
identity.RemoveClaim(manageStoreClaim);
identity.AddClaim(new Claim("ManageStore", "Allowed"));
}
}
return Task.FromResult(0);
}
internal static Task RedirectToAuthorizationEndpoint(RedirectContext<OAuthOptions> context)
{
context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom");
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Authentication;
namespace MusicStore.Mocks.OpenIdConnect
{
internal class CustomStringDataFormat : ISecureDataFormat<string>
{
private const string _capturedNonce = "635579928639517715.OTRjOTVkM2EtMDRmYS00ZDE3LThhZGUtZWZmZGM4ODkzZGZkMDRlNDhkN2MtOWIwMC00ZmVkLWI5MTItMTUwYmQ4MzdmOWI0";
public string Protect(string data)
{
return "protectedString";
}
public string Protect(string data, string purpose)
{
return purpose + "protectedString";
}
public string Unprotect(string protectedText)
{
return protectedText == "protectedString" ? _capturedNonce : null;
}
public string Unprotect(string protectedText, string purpose)
{
return protectedText == (purpose + "protectedString") ? _capturedNonce : null;
}
}
}

View File

@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
namespace MusicStore.Mocks.OpenIdConnect
{
internal class OpenIdConnectBackChannelHttpHandler : HttpMessageHandler
{
private IHostingEnvironment _env;
public OpenIdConnectBackChannelHttpHandler(IHostingEnvironment env)
{
_env = env;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage();
var basePath = Path.GetFullPath(Path.Combine(_env.ContentRootPath, "ForTesting", "Mocks", "OpenIdConnect"));
if (request.RequestUri.AbsoluteUri == "https://login.windows.net/[tenantName].onmicrosoft.com/.well-known/openid-configuration")
{
response.Content = new StringContent(File.ReadAllText(Path.Combine(basePath, "openid-configuration.json")));
}
else if (request.RequestUri.AbsoluteUri == "https://login.windows.net/common/discovery/keys")
{
response.Content = new StringContent(File.ReadAllText(Path.Combine(basePath, "keys.json")));
}
else if (request.RequestUri.AbsoluteUri == "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/oauth2/token")
{
response.Content = new StringContent("{\"id_token\": \"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImtyaU1QZG1Cdng2OHNrVDgtbVBBQjNCc2VlQSJ9.eyJhdWQiOiJjOTk0OTdhYS0zZWUyLTQ3MDctYjhhOC1jMzNmNTEzMjNmZWYiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC80YWZiYzY4OS04MDViLTQ4Y2YtYTI0Yy1kNGFhMzI0OGEyNDgvIiwiaWF0IjoxNDIyMzk1NzYzLCJuYmYiOjE0MjIzOTU3NjMsImV4cCI6MTQyMjM5OTY2MywidmVyIjoiMS4wIiwidGlkIjoiNGFmYmM2ODktODA1Yi00OGNmLWEyNGMtZDRhYTMyNDhhMjQ4IiwiYW1yIjpbInB3ZCJdLCJvaWQiOiJmODc2YWJlYi1kNmI1LTQ0ZTQtOTcxNi02MjY2YWMwMTgxYTgiLCJ1cG4iOiJ1c2VyM0BwcmFidXJhamdtYWlsLm9ubWljcm9zb2Z0LmNvbSIsInN1YiI6IlBVZGhjbFA1UGdJalNVOVAxUy1IZWxEYVNGU2YtbVhWMVk2MC1LMnZXcXciLCJnaXZlbl9uYW1lIjoiVXNlcjMiLCJmYW1pbHlfbmFtZSI6IlVzZXIzIiwibmFtZSI6IlVzZXIzIiwidW5pcXVlX25hbWUiOiJ1c2VyM0BwcmFidXJhamdtYWlsLm9ubWljcm9zb2Z0LmNvbSIsIm5vbmNlIjoiNjM1NTc5OTI4NjM5NTE3NzE1Lk9UUmpPVFZrTTJFdE1EUm1ZUzAwWkRFM0xUaGhaR1V0WldabVpHTTRPRGt6Wkdaa01EUmxORGhrTjJNdE9XSXdNQzAwWm1Wa0xXSTVNVEl0TVRVd1ltUTRNemRtT1dJMCIsImNfaGFzaCI6IkZHdDN3Y1FBRGUwUFkxUXg3TzFyNmciLCJwd2RfZXhwIjoiNjY5MzI4MCIsInB3ZF91cmwiOiJodHRwczovL3BvcnRhbC5taWNyb3NvZnRvbmxpbmUuY29tL0NoYW5nZVBhc3N3b3JkLmFzcHgifQ.coAdCkdMgnslMHagdU8IBgH7Z0dilRdMfKytyqPJuTr6sbmbhrAoAj-KeGwbKgzrd-BeDk_rW47dntWuuAqGrAOGzxXvS2dcSWgoEKoXuDccIL5b4rIomRpfJpaeE-YwiU3usyRvoQCpHmtOa0g7xVilIj3_1-9ylMgRDY5qcrtQ_hEZlGuYyiCPR0dw8WmNU7r6PKObG-o3Yk_RbEBHjnaWxKoJwrVUEZUQOJDAvlr6ZYEmGTlD_BM0Rc_0fJZPU7A3uN9PHLw1atm-chN06IDXf23R33JI_xFuEZnj9HZQ_eIzNCl7GFmUryK3FFgYJpIbsI0BIFuksSikXz33IA\", \"access_token\": \"access\"}");
}
return Task.FromResult(response);
}
}
}

View File

@ -0,0 +1,62 @@
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.OpenIdConnect
{
internal class TestOpenIdConnectEvents
{
private static List<string> eventsFired = new List<string>();
internal static Task MessageReceived(MessageReceivedContext context)
{
Helpers.ThrowIfConditionFailed(() => context.ProtocolMessage != null, "ProtocolMessage is null.");
eventsFired.Add(nameof(MessageReceived));
return Task.FromResult(0);
}
internal static Task TokenValidated(TokenValidatedContext context)
{
Helpers.ThrowIfConditionFailed(() => context.Principal != null, "context.Principal is null.");
Helpers.ThrowIfConditionFailed(() => context.Principal.Identity != null, "context.Principal.Identity is null.");
Helpers.ThrowIfConditionFailed(() => !string.IsNullOrWhiteSpace(context.Principal.Identity.Name), "context.Principal.Identity.Name is null.");
eventsFired.Add(nameof(TokenValidated));
return Task.FromResult(0);
}
internal static Task AuthorizationCodeReceived(AuthorizationCodeReceivedContext context)
{
Helpers.ThrowIfConditionFailed(() => context.TokenEndpointRequest.Code == "AAABAAAAvPM1KaPlrEqdFSBzjqfTGGBtrTYVn589oKw4lLgJ6Svz0AhPVOJr0J2-Uu_KffGlqIbYlRAyxmt-vZ7VlSVdrWvOkNhK9OaAMaSD7LDoPbBTVMEkB0MdAgBTV34l2el-s8ZI02_9PvgQaORZs7n8eGaGbcoKAoxiDn2OcKuJVplXYgrGUwU4VpRaqe6RaNzuseM7qBFbLIv4Wps8CndE6W8ccmuu6EvGC6-H4uF9EZL7gU4nEcTcvkE4Qyt8do6VhTVfM1ygRNQgmV1BCig5t_5xfhL6-xWQdy15Uzn_Df8VSsyDXe8s9cxyKlqc_AIyLFy_NEiMQFUqjZWKd_rR3A8ugug15SEEGuo1kF3jMc7dVMdE6OF9UBd-Ax5ILWT7V4clnRQb6-CXB538DlolREfE-PowXYruFBA-ARD6rwAVtuVfCSbS0Zr4ZqfNjt6x8yQdK-OkdQRZ1thiZcZlm1lyb2EquGZ8Deh2iWBoY1uNcyjzhG-L43EivxtHAp6Y8cErhbo41iacgqOycgyJWxiB5J0HHkxD0nQ2RVVuY8Ybc9sdgyfKkkK2wZ3idGaRCdZN8Q9VBhWRXPDMqHWG8t3aZRtvJ_Xd3WhjNPJC0GpepUGNNQtXiEoIECC363o1z6PZC5-E7U3l9xK06BZkcfTOnggUiSWNCrxUKS44dNqaozdYlO5E028UgAEhJ4eDtcP3PZty-0j4j5Mw0F2FmyAA",
"context.TokenEndpointRequest.Code is invalid.");
eventsFired.Add(nameof(AuthorizationCodeReceived));
// Verify all events are fired.
if (eventsFired.Contains(nameof(RedirectToIdentityProvider)) &&
eventsFired.Contains(nameof(MessageReceived)) &&
eventsFired.Contains(nameof(TokenValidated)) &&
eventsFired.Contains(nameof(AuthorizationCodeReceived)))
{
((ClaimsIdentity)context.Principal.Identity).AddClaim(new Claim("ManageStore", "Allowed"));
}
return Task.FromResult(0);
}
internal static Task RedirectToIdentityProvider(RedirectContext context)
{
eventsFired.Add(nameof(RedirectToIdentityProvider));
if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
context.ProtocolMessage.PostLogoutRedirectUri =
context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase + new PathString("/Account/Login");
}
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,26 @@
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "kriMPdmBvx68skT8-mPAB3BseeA",
"x5t": "kriMPdmBvx68skT8-mPAB3BseeA",
"n": "kSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuw==",
"e": "AQAB",
"x5c": [
"MIIDPjCCAiqgAwIBAgIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTQwMTAxMDcwMDAwWhcNMTYwMTAxMDcwMDAwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkSCWg6q9iYxvJE2NIhSyOiKvqoWCO2GFipgH0sTSAs5FalHQosk9ZNTztX0ywS/AHsBeQPqYygfYVJL6/EgzVuwRk5txr9e3n1uml94fLyq/AXbwo9yAduf4dCHTP8CWR1dnDR+Qnz/4PYlWVEuuHHONOw/blbfdMjhY+C/BYM2E3pRxbohBb3x//CfueV7ddz2LYiH3wjz0QS/7kjPiNCsXcNyKQEOTkbHFi3mu0u13SQwNddhcynd/GTgWN8A+6SN1r4hzpjFKFLbZnBt77ACSiYx+IHK4Mp+NaVEi5wQtSsjQtI++XsokxRDqYLwus1I1SihgbV/STTg5enufuwIDAQABo2IwYDBeBgNVHQEEVzBVgBDLebM6bK3BjWGqIBrBNFeNoS8wLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldIIQsRiM0jheFZhKk49YD0SK1TAJBgUrDgMCHQUAA4IBAQCJ4JApryF77EKC4zF5bUaBLQHQ1PNtA1uMDbdNVGKCmSf8M65b8h0NwlIjGGGy/unK8P6jWFdm5IlZ0YPTOgzcRZguXDPj7ajyvlVEQ2K2ICvTYiRQqrOhEhZMSSZsTKXFVwNfW6ADDkN3bvVOVbtpty+nBY5UqnI7xbcoHLZ4wYD251uj5+lo13YLnsVrmQ16NCBYq2nQFNPuNJw6t3XUbwBHXpF46aLT1/eGf/7Xx6iy8yPJX4DyrpFTutDz882RWofGEO5t4Cw+zZg70dJ/hH/ODYRMorfXEW+8uKmXMKmX2wyxMKvfiPbTy5LmAU8Jvjs2tLg4rOBcXWLAIarZ"
]
},
{
"kty": "RSA",
"use": "sig",
"kid": "MnC_VZcATfM5pOYiJHMba9goEKY",
"x5t": "MnC_VZcATfM5pOYiJHMba9goEKY",
"n": "vIqz+4+ER/vNWLON9yv8hIYV737JQ6rCl6XfzOC628seYUPf0TaGk91CFxefhzh23V9Tkq+RtwN1Vs/z57hO82kkzL+cQHZX3bMJD+GEGOKXCEXURN7VMyZWMAuzQoW9vFb1k3cR1RW/EW/P+C8bb2dCGXhBYqPfHyimvz2WarXhntPSbM5XyS5v5yCw5T/Vuwqqsio3V8wooWGMpp61y12NhN8bNVDQAkDPNu2DT9DXB1g0CeFINp/KAS/qQ2Kq6TSvRHJqxRR68RezYtje9KAqwqx4jxlmVAQy0T3+T+IAbsk1wRtWDndhO6s1Os+dck5TzyZ/dNOhfXgelixLUQ==",
"e": "AQAB",
"x5c": [
"MIIC4jCCAcqgAwIBAgIQQNXrmzhLN4VGlUXDYCRT3zANBgkqhkiG9w0BAQsFADAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MB4XDTE0MTAyODAwMDAwMFoXDTE2MTAyNzAwMDAwMFowLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyKs/uPhEf7zVizjfcr/ISGFe9+yUOqwpel38zgutvLHmFD39E2hpPdQhcXn4c4dt1fU5KvkbcDdVbP8+e4TvNpJMy/nEB2V92zCQ/hhBjilwhF1ETe1TMmVjALs0KFvbxW9ZN3EdUVvxFvz/gvG29nQhl4QWKj3x8opr89lmq14Z7T0mzOV8kub+cgsOU/1bsKqrIqN1fMKKFhjKaetctdjYTfGzVQ0AJAzzbtg0/Q1wdYNAnhSDafygEv6kNiquk0r0RyasUUevEXs2LY3vSgKsKseI8ZZlQEMtE9/k/iAG7JNcEbVg53YTurNTrPnXJOU88mf3TToX14HpYsS1ECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAfolx45w0i8CdAUjjeAaYdhG9+NDHxop0UvNOqlGqYJexqPLuvX8iyUaYxNGzZxFgGI3GpKfmQP2JQWQ1E5JtY/n8iNLOKRMwqkuxSCKJxZJq4Sl/m/Yv7TS1P5LNgAj8QLCypxsWrTAmq2HSpkeSk4JBtsYxX6uhbGM/K1sEktKybVTHu22/7TmRqWTmOUy9wQvMjJb2IXdMGLG3hVntN/WWcs5w8vbt1i8Kk6o19W2MjZ95JaECKjBDYRlhG1KmSBtrsKsCBQoBzwH/rXfksTO9JoUYLXiW0IppB7DhNH4PJ5hZI91R8rR0H3/bKkLSuDaKLWSqMhozdhXsIIKvJQ=="
]
}
]
}

View File

@ -0,0 +1,34 @@
{
"issuer": "https://sts.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/",
"authorization_endpoint": "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/oauth2/authorize",
"token_endpoint": "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/oauth2/token",
"token_endpoint_auth_methods_supported": [
"client_secret_post",
"private_key_jwt"
],
"jwks_uri": "https://login.windows.net/common/discovery/keys",
"response_types_supported": [
"code",
"id_token",
"code id_token",
"token"
],
"response_modes_supported": [
"query",
"fragment",
"form_post"
],
"subject_types_supported": [
"pairwise"
],
"scopes_supported": [
"openid"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"microsoft_multi_refresh_token": true,
"check_session_iframe": "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/oauth2/checksession",
"end_session_endpoint": "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/oauth2/logout",
"userinfo_endpoint": "https://login.windows.net/4afbc689-805b-48cf-a24c-d4aa3248a248/openid/userinfo"
}

View File

@ -0,0 +1,169 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MusicStore.Components;
using MusicStore.Mocks.Common;
using MusicStore.Mocks.OpenIdConnect;
using MusicStore.Models;
namespace MusicStore
{
public class StartupOpenIdConnectTesting
{
private readonly Platform _platform;
public StartupOpenIdConnectTesting(IHostingEnvironment env)
{
//Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1' is found in both the registered sources,
//then the later source will win. By this way a Local config can be overridden by a different setting while deployed remotely.
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("config.json")
.AddEnvironmentVariables(); //All environment variables in the process's context flow in as configuration values.
Configuration = builder.Build();
_platform = new Platform();
Env = env;
}
public IConfiguration Configuration { get; private set; }
public IHostingEnvironment Env { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add EF services to the services container
if (_platform.UseInMemoryStore)
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseInMemoryDatabase("Scratch"));
}
else
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
}
// Add Identity services to the services container
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MusicStoreContext>()
.AddDefaultTokenProviders();
// Create an Azure Active directory application and copy paste the following
services.AddAuthentication().AddOpenIdConnect(options =>
{
options.Authority = "https://login.windows.net/[tenantName].onmicrosoft.com";
options.ClientId = "c99497aa-3ee2-4707-b8a8-c33f51323fef";
options.BackchannelHttpHandler = new OpenIdConnectBackChannelHttpHandler(Env);
options.StringDataFormat = new CustomStringDataFormat();
options.StateDataFormat = new CustomStateDataFormat();
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
options.UseTokenLifetime = false;
options.TokenValidationParameters.ValidateLifetime = false;
options.ProtocolValidator.RequireNonce = true;
options.ProtocolValidator.NonceLifetime = TimeSpan.FromDays(36500);
options.Events = new OpenIdConnectEvents
{
OnMessageReceived = TestOpenIdConnectEvents.MessageReceived,
OnAuthorizationCodeReceived = TestOpenIdConnectEvents.AuthorizationCodeReceived,
OnRedirectToIdentityProvider = TestOpenIdConnectEvents.RedirectToIdentityProvider,
OnTokenValidated = TestOpenIdConnectEvents.TokenValidated,
};
});
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder.WithOrigins("http://example.com");
});
});
// Add MVC services to the services container
services.AddMvc();
//Add InMemoryCache
services.AddSingleton<IMemoryCache, MemoryCache>();
// Add session related services.
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
// Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>();
// Configure Auth
services.Configure<AuthorizationOptions>(options =>
{
options.AddPolicy("ManageStore", new AuthorizationPolicyBuilder().RequireClaim("ManageStore", "Allowed").Build());
});
}
public void Configure(IApplicationBuilder app)
{
// force the en-US culture, so that the app behaves the same even on machines with different default culture
var supportedCultures = new[] { new CultureInfo("en-US") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
// Display custom error page in production when error occurs
// During development use the ErrorPage middleware to display error information in the browser
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
// Configure Session.
app.UseSession();
// Add static files to the request pipeline
app.UseStaticFiles();
// Add authentication to the request pipeline
app.UseAuthentication();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "api",
template: "{controller}/{id?}");
});
//Populates the MusicStore sample data
SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait();
}
}
}

View File

@ -0,0 +1,209 @@
using System;
using System.Globalization;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Authentication.Twitter;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MusicStore.Components;
using MusicStore.Mocks.Common;
using MusicStore.Mocks.Facebook;
using MusicStore.Mocks.Google;
using MusicStore.Mocks.MicrosoftAccount;
using MusicStore.Mocks.Twitter;
using MusicStore.Models;
namespace MusicStore
{
public class StartupSocialTesting
{
private readonly Platform _platform;
public StartupSocialTesting(IHostingEnvironment hostingEnvironment)
{
//Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1' is found in both the registered sources,
//then the later source will win. By this way a Local config can be overridden by a different setting while deployed remotely.
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("config.json")
.AddEnvironmentVariables() //All environment variables in the process's context flow in as configuration values.
.AddJsonFile("configoverride.json", optional: true); // Used to override some configuration parameters that cannot be overridden by environment.
Configuration = builder.Build();
_platform = new Platform();
}
public IConfiguration Configuration { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add EF services to the services container
if (_platform.UseInMemoryStore)
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseInMemoryDatabase("Scratch"));
}
else
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
}
// Add Identity services to the services container
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MusicStoreContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => options.AccessDeniedPath = "/Home/AccessDenied");
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder.WithOrigins("http://example.com");
});
});
// Add MVC services to the services container
services.AddMvc();
//Add InMemoryCache
services.AddSingleton<IMemoryCache, MemoryCache>();
// Add session related services.
services.AddMemoryCache();
services.AddDistributedMemoryCache();
services.AddSession();
// Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>();
// Configure Auth
services.AddAuthorization(options =>
{
options.AddPolicy("ManageStore", new AuthorizationPolicyBuilder().RequireClaim("ManageStore", "Allowed").Build());
});
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = "[AppId]";
options.AppSecret = "[AppSecret]";
options.Events = new OAuthEvents()
{
OnCreatingTicket = TestFacebookEvents.OnCreatingTicket,
OnTicketReceived = TestFacebookEvents.OnTicketReceived,
OnRedirectToAuthorizationEndpoint = TestFacebookEvents.RedirectToAuthorizationEndpoint
};
options.BackchannelHttpHandler = new FacebookMockBackChannelHttpHandler();
options.StateDataFormat = new CustomStateDataFormat();
options.Scope.Add("email");
options.Scope.Add("read_friendlists");
options.Scope.Add("user_checkins");
}).AddGoogle(options =>
{
options.ClientId = "[ClientId]";
options.ClientSecret = "[ClientSecret]";
options.AccessType = "offline";
options.Events = new OAuthEvents()
{
OnCreatingTicket = TestGoogleEvents.OnCreatingTicket,
OnTicketReceived = TestGoogleEvents.OnTicketReceived,
OnRedirectToAuthorizationEndpoint = TestGoogleEvents.RedirectToAuthorizationEndpoint
};
options.StateDataFormat = new CustomStateDataFormat();
options.BackchannelHttpHandler = new GoogleMockBackChannelHttpHandler();
}).AddTwitter(options =>
{
options.ConsumerKey = "[ConsumerKey]";
options.ConsumerSecret = "[ConsumerSecret]";
options.Events = new TwitterEvents()
{
OnCreatingTicket = TestTwitterEvents.OnCreatingTicket,
OnTicketReceived = TestTwitterEvents.OnTicketReceived,
OnRedirectToAuthorizationEndpoint = TestTwitterEvents.RedirectToAuthorizationEndpoint
};
options.StateDataFormat = new CustomTwitterStateDataFormat();
options.BackchannelHttpHandler = new TwitterMockBackChannelHttpHandler();
}).AddMicrosoftAccount(options =>
{
options.ClientId = "[ClientId]";
options.ClientSecret = "[ClientSecret]";
options.Events = new OAuthEvents()
{
OnCreatingTicket = TestMicrosoftAccountEvents.OnCreatingTicket,
OnTicketReceived = TestMicrosoftAccountEvents.OnTicketReceived,
OnRedirectToAuthorizationEndpoint = TestMicrosoftAccountEvents.RedirectToAuthorizationEndpoint
};
options.BackchannelHttpHandler = new MicrosoftAccountMockBackChannelHandler();
options.StateDataFormat = new CustomStateDataFormat();
options.Scope.Add("wl.basic");
options.Scope.Add("wl.signin");
});
}
public void Configure(IApplicationBuilder app)
{
// force the en-US culture, so that the app behaves the same even on machines with different default culture
var supportedCultures = new[] { new CultureInfo("en-US") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
// Error page middleware displays a nice formatted HTML page for any unhandled exceptions in the request pipeline.
// Note: Not recommended for production.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
// Configure Session.
app.UseSession();
// Add static files to the request pipeline
app.UseStaticFiles();
// Add cookie-based authentication to the request pipeline
app.UseAuthentication();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "api",
template: "{controller}/{id?}");
});
//Populates the MusicStore sample data
SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait();
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Twitter;
using Newtonsoft.Json;
namespace MusicStore.Mocks.Twitter
{
/// <summary>
/// Summary description for CustomTwitterStateDataFormat
/// </summary>
public class CustomTwitterStateDataFormat : ISecureDataFormat<RequestToken>
{
private static string _lastSavedRequestToken;
public string Protect(RequestToken data)
{
data.Token = "valid_oauth_token";
_lastSavedRequestToken = Serialize(data);
return "valid_oauth_token";
}
public string Protect(RequestToken data, string purpose)
{
return Protect(data);
}
public RequestToken Unprotect(string state)
{
return state == "valid_oauth_token" ? DeSerialize(_lastSavedRequestToken) : null;
}
public RequestToken Unprotect(string state, string purpose)
{
return Unprotect(state);
}
private string Serialize(RequestToken data)
{
return JsonConvert.SerializeObject(data, Formatting.Indented);
}
private RequestToken DeSerialize(string state)
{
return JsonConvert.DeserializeObject<RequestToken>(state);
}
}
}

View File

@ -0,0 +1,50 @@
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Twitter;
using Microsoft.AspNetCore.Identity;
using MusicStore.Mocks.Common;
namespace MusicStore.Mocks.Twitter
{
internal class TestTwitterEvents
{
internal static Task OnCreatingTicket(TwitterCreatingTicketContext context)
{
if (context.Principal != null)
{
Helpers.ThrowIfConditionFailed(() => context.UserId == "valid_user_id", "UserId is not valid");
Helpers.ThrowIfConditionFailed(() => context.ScreenName == "valid_screen_name", "ScreenName is not valid");
Helpers.ThrowIfConditionFailed(() => context.AccessToken == "valid_oauth_token", "AccessToken is not valid");
Helpers.ThrowIfConditionFailed(() => context.AccessTokenSecret == "valid_oauth_token_secret", "AccessTokenSecret is not valid");
context.Principal.Identities.First().AddClaim(new Claim("ManageStore", "false"));
}
return Task.FromResult(0);
}
internal static Task OnTicketReceived(TicketReceivedContext context)
{
if (context.Principal != null && context.Options.SignInScheme == IdentityConstants.ExternalScheme)
{
//This way we will know all Events were fired.
var identity = context.Principal.Identities.First();
var manageStoreClaim = identity?.Claims.Where(c => c.Type == "ManageStore" && c.Value == "false").FirstOrDefault();
if (manageStoreClaim != null)
{
identity.RemoveClaim(manageStoreClaim);
identity.AddClaim(new Claim("ManageStore", "Allowed"));
}
}
return Task.FromResult(0);
}
internal static Task RedirectToAuthorizationEndpoint(RedirectContext<TwitterOptions> context)
{
context.Response.Redirect(context.RedirectUri + "&custom_redirect_uri=custom");
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
namespace MusicStore.Mocks.Twitter
{
/// <summary>
/// Summary description for TwitterMockBackChannelHttpHandler
/// </summary>
public class TwitterMockBackChannelHttpHandler : HttpMessageHandler
{
private static bool _requestTokenEndpointInvoked = false;
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage();
if (request.RequestUri.AbsoluteUri.StartsWith("https://api.twitter.com/oauth/access_token"))
{
var formData = new FormCollection(await new FormReader(await request.Content.ReadAsStreamAsync()).ReadFormAsync());
if (formData["oauth_verifier"] == "valid_oauth_verifier")
{
if (_requestTokenEndpointInvoked)
{
var response_Form_data = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("oauth_token", "valid_oauth_token"),
new KeyValuePair<string, string>("oauth_token_secret", "valid_oauth_token_secret"),
new KeyValuePair<string, string>("user_id", "valid_user_id"),
new KeyValuePair<string, string>("screen_name", "valid_screen_name"),
};
response.Content = new FormUrlEncodedContent(response_Form_data);
}
else
{
response.StatusCode = HttpStatusCode.InternalServerError;
response.Content = new StringContent("RequestTokenEndpoint is not invoked");
}
return response;
}
response.StatusCode = (HttpStatusCode)400;
return response;
}
else if (request.RequestUri.AbsoluteUri.StartsWith("https://api.twitter.com/oauth/request_token"))
{
var response_Form_data = new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("oauth_callback_confirmed", "true"),
new KeyValuePair<string, string>("oauth_token", "valid_oauth_token"),
new KeyValuePair<string, string>("oauth_token_secret", "valid_oauth_token_secret")
};
_requestTokenEndpointInvoked = true;
response.Content = new FormUrlEncodedContent(response_Form_data);
return response;
}
throw new NotImplementedException(request.RequestUri.AbsoluteUri);
}
}
}

View File

@ -0,0 +1,7 @@
namespace MusicStore
{
public class StoreConfig
{
public const string ConnectionStringKey = "Data__DefaultConnection__ConnectionString";
}
}

View File

@ -0,0 +1 @@
The contents of this folder are used for end to end testing.

View File

@ -0,0 +1,19 @@
using System.Threading.Tasks;
namespace MusicStore
{
public static class MessageServices
{
public static Task SendEmailAsync(string email, string subject, string message)
{
// Plug in your email service
return Task.FromResult(0);
}
public static Task SendSmsAsync(string number, string message)
{
// Plug in your sms service
return Task.FromResult(0);
}
}
}

View File

@ -0,0 +1,113 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MusicStore.Models
{
public class ExternalLoginConfirmationViewModel
{
[Required]
[Display(Name = "Email")]
public string Email { get; set; }
}
public class ExternalLoginListViewModel
{
public string ReturnUrl { get; set; }
}
public class SendCodeViewModel
{
public string SelectedProvider { get; set; }
public ICollection<SelectListItem> Providers { get; set; }
public string ReturnUrl { get; set; }
public bool RememberMe { get; set; }
}
public class VerifyCodeViewModel
{
[Required]
public string Provider { get; set; }
[Required]
[Display(Name = "Code")]
public string Code { get; set; }
public string ReturnUrl { get; set; }
[Display(Name = "Remember this browser?")]
public bool RememberBrowser { get; set; }
public bool RememberMe { get; set; }
}
public class ForgotViewModel
{
[Required]
[Display(Name = "Email")]
public string Email { get; set; }
}
public class LoginViewModel
{
[Required]
[Display(Name = "Email")]
[EmailAddress]
public string Email { 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]
[EmailAddress]
[Display(Name = "Email")]
public string Email { 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; }
}
public class ResetPasswordViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { 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; }
public string Code { get; set; }
}
public class ForgotPasswordViewModel
{
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace MusicStore.Models
{
public class Album
{
[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)]
[Column(TypeName = "decimal(18,2)")]
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 List<OrderDetail> OrderDetails { get; set; }
[ScaffoldColumn(false)]
[BindNever]
[Required]
public DateTime Created { get; set; }
/// <summary>
/// TODO: Temporary hack to populate the orderdetails until EF does this automatically.
/// </summary>
public Album()
{
OrderDetails = new List<OrderDetail>();
Created = DateTime.UtcNow;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace MusicStore.Models
{
public class Genre
{
public int GenreId { get; set; }
[Required]
public string Name { get; set; }
public string Description { get; set; }
public List<Album> Albums { get; set; }
}
}

View File

@ -0,0 +1,87 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MusicStore.Models
{
public class IndexViewModel
{
public bool HasPassword { get; set; }
public IList<UserLoginInfo> Logins { get; set; }
public string PhoneNumber { get; set; }
public bool TwoFactor { get; set; }
public bool BrowserRemembered { get; set; }
}
public class ManageLoginsViewModel
{
public IList<UserLoginInfo> CurrentLogins { get; set; }
public IList<AuthenticationScheme> OtherLogins { get; set; }
}
public class FactorViewModel
{
public string Purpose { get; set; }
}
public class SetPasswordViewModel
{
[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 ChangePasswordViewModel
{
[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 AddPhoneNumberViewModel
{
[Required]
[Phone]
[Display(Name = "Phone Number")]
public string Number { get; set; }
}
public class VerifyPhoneNumberViewModel
{
[Required]
[Display(Name = "Code")]
public string Code { get; set; }
[Required]
[Phone]
[Display(Name = "Phone Number")]
public string PhoneNumber { get; set; }
}
public class ConfigureTwoFactorViewModel
{
public string SelectedProvider { get; set; }
public ICollection<SelectListItem> Providers { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
namespace MusicStore.Models
{
public class ApplicationUser : IdentityUser { }
public class MusicStoreContext : IdentityDbContext<ApplicationUser>
{
public MusicStoreContext(DbContextOptions<MusicStoreContext> options)
: base(options)
{
// TODO: #639
//ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
public DbSet<Album> Albums { get; set; }
public DbSet<Artist> Artists { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<Genre> Genres { get; set; }
public DbSet<CartItem> CartItems { get; set; }
public DbSet<OrderDetail> OrderDetails { get; set; }
}
}

View File

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace MusicStore.Models
{
//[Bind(Include = "FirstName,LastName,Address,City,State,PostalCode,Country,Phone,Email")]
public class Order
{
[BindNever]
[ScaffoldColumn(false)]
public int OrderId { get; set; }
[BindNever]
[ScaffoldColumn(false)]
public System.DateTime OrderDate { get; set; }
[BindNever]
[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 not valid.")]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[BindNever]
[ScaffoldColumn(false)]
[Column(TypeName = "decimal(18,2)")]
public decimal Total { get; set; }
[BindNever]
public List<OrderDetail> OrderDetails { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations.Schema;
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; }
[Column(TypeName = "decimal(18,2)")]
public decimal UnitPrice { get; set; }
public virtual Album Album { get; set; }
public virtual Order Order { get; set; }
}
}

View File

@ -0,0 +1,965 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace MusicStore.Models
{
public static class SampleData
{
const string imgUrl = "~/Images/placeholder.png";
const string defaultAdminUserName = "DefaultAdminUserName";
const string defaultAdminPassword = "DefaultAdminPassword";
public static async Task InitializeMusicStoreDatabaseAsync(IServiceProvider serviceProvider, bool createUsers = true)
{
using (var serviceScope = serviceProvider.CreateScope())
{
var scopeServiceProvider = serviceScope.ServiceProvider;
var db = scopeServiceProvider.GetService<MusicStoreContext>();
if (await db.Database.EnsureCreatedAsync())
{
await InsertTestData(scopeServiceProvider);
if (createUsers)
{
await CreateAdminUser(scopeServiceProvider);
}
}
}
}
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<TEntity>(
IServiceProvider serviceProvider,
Func<TEntity, object> propertyToMatch, IEnumerable<TEntity> entities)
where TEntity : class
{
// Query in a separate context so that we can attach existing entities as modified
List<TEntity> existingData;
using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var db = serviceScope.ServiceProvider.GetService<MusicStoreContext>();
existingData = db.Set<TEntity>().ToList();
}
using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope())
{
var db = serviceScope.ServiceProvider.GetService<MusicStoreContext>();
foreach (var item in entities)
{
db.Entry(item).State = existingData.Any(g => propertyToMatch(g).Equals(propertyToMatch(item)))
? EntityState.Modified
: EntityState.Added;
}
await db.SaveChangesAsync();
}
}
/// <summary>
/// Creates a store manager user who can manage the inventory.
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
private static async Task CreateAdminUser(IServiceProvider serviceProvider)
{
var env = serviceProvider.GetService<IHostingEnvironment>();
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("config.json")
.AddEnvironmentVariables();
var configuration = builder.Build();
//const string adminRole = "Administrator";
var userManager = serviceProvider.GetService<UserManager<ApplicationUser>>();
// TODO: Identity SQL does not support roles yet
//var roleManager = serviceProvider.GetService<ApplicationRoleManager>();
//if (!await roleManager.RoleExistsAsync(adminRole))
//{
// await roleManager.CreateAsync(new IdentityRole(adminRole));
//}
var user = await userManager.FindByNameAsync(configuration[defaultAdminUserName]);
if (user == null)
{
user = new ApplicationUser { UserName = configuration[defaultAdminUserName] };
await userManager.CreateAsync(user, configuration[defaultAdminPassword]);
//await userManager.AddToRoleAsync(user, adminRole);
await userManager.AddClaimAsync(user, new Claim("ManageStore", "Allowed"));
}
// NOTE: For end to end testing only
var envPerfLab = configuration["PERF_LAB"];
if (envPerfLab == "true")
{
for (int i = 0; i < 100; ++i)
{
var email = string.Format("User{0:D3}@example.com", i);
var normalUser = await userManager.FindByEmailAsync(email);
if (normalUser == null)
{
await userManager.CreateAsync(new ApplicationUser { UserName = email, Email = email }, "Password~!1");
}
}
}
}
private static Album[] GetAlbums(string imgUrl, Dictionary<string, Genre> genres, Dictionary<string, Artist> 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 19902000", 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 },
};
foreach (var album in albums)
{
album.ArtistId = album.Artist.ArtistId;
album.GenreId = album.Genre.GenreId;
}
return albums;
}
private static Dictionary<string, Artist> artists;
public static Dictionary<string, Artist> 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 = "אריק אינשטיין"}
};
artists = new Dictionary<string, Artist>();
foreach (Artist artist in artistsList)
{
artists.Add(artist.Name, artist);
}
}
return artists;
}
}
private static Dictionary<string, Genre> genres;
public static Dictionary<string, Genre> 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<string, Genre>();
foreach (Genre genre in genresList)
{
genres.Add(genre.Name, genre);
}
}
return genres;
}
}
}
}

View File

@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
namespace MusicStore.Models
{
public class ShoppingCart
{
private readonly MusicStoreContext _dbContext;
private readonly string _shoppingCartId;
private ShoppingCart(MusicStoreContext dbContext, string id)
{
_dbContext = dbContext;
_shoppingCartId = id;
}
public static ShoppingCart GetCart(MusicStoreContext db, HttpContext context)
=> GetCart(db, GetCartId(context));
public static ShoppingCart GetCart(MusicStoreContext db, string cartId)
=> new ShoppingCart(db, cartId);
public async Task AddToCart(Album album)
{
// Get the matching cart and album instances
var cartItem = await _dbContext.CartItems.SingleOrDefaultAsync(
c => c.CartId == _shoppingCartId
&& c.AlbumId == album.AlbumId);
if (cartItem == null)
{
// Create a new cart item if no cart item exists
cartItem = new CartItem
{
AlbumId = album.AlbumId,
CartId = _shoppingCartId,
Count = 1,
DateCreated = DateTime.Now
};
_dbContext.CartItems.Add(cartItem);
}
else
{
// If the item does exist in the cart, then add one to the quantity
cartItem.Count++;
}
}
public int RemoveFromCart(int id)
{
// Get the cart
var cartItem = _dbContext.CartItems.SingleOrDefault(
cart => cart.CartId == _shoppingCartId
&& cart.CartItemId == id);
int itemCount = 0;
if (cartItem != null)
{
if (cartItem.Count > 1)
{
cartItem.Count--;
itemCount = cartItem.Count;
}
else
{
_dbContext.CartItems.Remove(cartItem);
}
}
return itemCount;
}
public async Task EmptyCart()
{
var cartItems = await _dbContext
.CartItems
.Where(cart => cart.CartId == _shoppingCartId)
.ToArrayAsync();
_dbContext.CartItems.RemoveRange(cartItems);
}
public Task<List<CartItem>> GetCartItems()
{
return _dbContext
.CartItems
.Where(cart => cart.CartId == _shoppingCartId)
.Include(c => c.Album)
.ToListAsync();
}
public Task<List<string>> GetCartAlbumTitles()
{
return _dbContext
.CartItems
.Where(cart => cart.CartId == _shoppingCartId)
.Select(c => c.Album.Title)
.OrderBy(n => n)
.ToListAsync();
}
public Task<int> GetCount()
{
// Get the count of each item in the cart and sum them up
return _dbContext
.CartItems
.Where(c => c.CartId == _shoppingCartId)
.Select(c => c.Count)
.SumAsync();
}
public Task<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
return _dbContext
.CartItems
.Where(c => c.CartId == _shoppingCartId)
.Select(c => c.Album.Price * c.Count)
.SumAsync();
}
public async Task<int> CreateOrder(Order order)
{
decimal orderTotal = 0;
var cartItems = await GetCartItems();
// 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 = await _dbContext.Albums.SingleAsync(a => a.AlbumId == item.AlbumId);
var orderDetail = new OrderDetail
{
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);
_dbContext.OrderDetails.Add(orderDetail);
}
// Set the order's total to the orderTotal count
order.Total = orderTotal;
// Empty the shopping cart
await EmptyCart();
// Return the OrderId as the confirmation number
return order.OrderId;
}
// We're using HttpContextBase to allow access to sessions.
private static string GetCartId(HttpContext context)
{
var cartId = context.Session.GetString("Session");
if (cartId == null)
{
//A GUID to hold the cartId.
cartId = Guid.NewGuid().ToString();
// Send cart Id as a cookie to the client.
context.Session.SetString("Session", cartId);
}
return cartId;
}
}
}

View File

@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<Description>Music store application on ASP.NET Core</Description>
<TargetFramework>netcoreapp3.0</TargetFramework>
<DefineConstants>$(DefineConstants);DEMO</DefineConstants>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<RuntimeIdentifiers Condition="'$(Configuration)' != 'RuntimeStore'">win7-x86;win7-x64;linux-x64;osx-x64</RuntimeIdentifiers>
<Configurations>Debug;Release;RuntimeStore</Configurations>
</PropertyGroup>
<ItemGroup>
<Content Update="ForTesting\**\*" CopyToPublishDirectory="Never" Condition=" '$(PublishForTesting)' != 'true' " />
</ItemGroup>
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<PackageReference Include="Microsoft.AspNetCore.AspNetCoreModule" Version="$(MicrosoftAspNetCoreAspNetCoreModulePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.AspNetCoreModuleV2" Version="$(MicrosoftAspNetCoreAspNetCoreModuleV2PackageVersion)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETCoreApp' AND '$(Configuration)' == 'RuntimeStore' ">
<PackageReference Include="Microsoft.AspNetCore.App" Version="$(MicrosoftAspNetCoreAppPackageVersion)" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFrameworkIdentifier)'=='.NETFramework' OR '$(Configuration)' != 'RuntimeStore' ">
<PackageReference Include="Microsoft.AspNetCore" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Cookies" Version="$(MicrosoftAspNetCoreAuthenticationCookiesPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="$(MicrosoftAspNetCoreAuthenticationFacebookPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="$(MicrosoftAspNetCoreAuthenticationGooglePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.MicrosoftAccount" Version="$(MicrosoftAspNetCoreAuthenticationMicrosoftAccountPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Twitter" Version="$(MicrosoftAspNetCoreAuthenticationTwitterPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="$(MicrosoftAspNetCoreDiagnosticsEntityFrameworkCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="$(MicrosoftAspNetCoreIdentityEntityFrameworkCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="$(MicrosoftAspNetCoreMvcPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Server.HttpSys" Version="$(MicrosoftAspNetCoreServerHttpSysPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Server.IIS" Version="$(MicrosoftAspNetCoreServerIISPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Session" Version="$(MicrosoftAspNetCoreSessionPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="$(MicrosoftAspNetCoreStaticFilesPackageVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="$(MicrosoftEntityFrameworkCoreInMemoryPackageVersion)" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="$(MicrosoftExtensionsConfigurationCommandLinePackageVersion)" />
<PackageReference Include="Microsoft.NET.Sdk.Razor" Version="$(MicrosoftNETSdkRazorPackageVersion)" PrivateAssets="All" />
</ItemGroup>
<Target Name="VerifyPrecompiledViews" AfterTargets="Publish">
<Error Text="Did not use Razor Sdk to compile views. Actual $(ResolvedRazorCompileToolset)" Condition="'$(ResolvedRazorCompileToolset)' != 'RazorSdk'" />
<PropertyGroup>
<ExpectedViewOutput>$(PublishDir)$(RazorTargetName).dll</ExpectedViewOutput>
</PropertyGroup>
<Error Text="Did not precompiled view binary '$(ExpectedViewOutput)'" Condition="!Exists('$(ExpectedViewOutput)')" />
</Target>
</Project>

View File

@ -0,0 +1,2 @@
@page
@{ throw new InvalidOperationException(); }

View File

@ -0,0 +1,110 @@
using System;
using System.Runtime.InteropServices;
namespace MusicStore
{
internal class Platform
{
// Defined in winnt.h
private const int PRODUCT_NANO_SERVER = 0x0000006D;
private const int PRODUCT_DATACENTER_NANO_SERVER = 0x0000008F;
private const int PRODUCT_STANDARD_NANO_SERVER = 0x00000090;
[DllImport("api-ms-win-core-sysinfo-l1-2-1.dll", SetLastError = false)]
private static extern bool GetProductInfo(
int dwOSMajorVersion,
int dwOSMinorVersion,
int dwSpMajorVersion,
int dwSpMinorVersion,
out int pdwReturnedProductType);
private bool? _isNano;
private bool? _isWindows;
public bool IsRunningOnWindows
{
get
{
if (_isWindows == null)
{
_isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}
return _isWindows.Value;
}
}
public bool IsRunningOnNanoServer
{
get
{
if (_isNano == null)
{
var osVersion = new Version(RtlGetVersion() ?? string.Empty);
try
{
int productType;
if (GetProductInfo(osVersion.Major, osVersion.Minor, 0, 0, out productType))
{
_isNano = productType == PRODUCT_NANO_SERVER ||
productType == PRODUCT_DATACENTER_NANO_SERVER ||
productType == PRODUCT_STANDARD_NANO_SERVER;
}
else
{
_isNano = false;
}
}
catch
{
// If the API call fails, the API set is not there which means
// that we are definetely not running on Nano
_isNano = false;
}
}
return _isNano.Value;
}
}
// Sql client not available on mono, non-windows, or nano
public bool UseInMemoryStore
{
get
{
return !IsRunningOnWindows || IsRunningOnNanoServer;
}
}
[StructLayout(LayoutKind.Sequential)]
internal struct RTL_OSVERSIONINFOEX
{
internal uint dwOSVersionInfoSize;
internal uint dwMajorVersion;
internal uint dwMinorVersion;
internal uint dwBuildNumber;
internal uint dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
internal string szCSDVersion;
}
// This call avoids the shimming Windows does to report old versions
[DllImport("ntdll")]
private static extern int RtlGetVersion(out RTL_OSVERSIONINFOEX lpVersionInformation);
internal static string RtlGetVersion()
{
RTL_OSVERSIONINFOEX osvi = new RTL_OSVERSIONINFOEX();
osvi.dwOSVersionInfoSize = (uint)Marshal.SizeOf(osvi);
if (RtlGetVersion(out osvi) == 0)
{
return $"{osvi.dwMajorVersion}.{osvi.dwMinorVersion}.{osvi.dwBuildNumber}";
}
else
{
return null;
}
}
}
}

View File

@ -0,0 +1,72 @@
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace MusicStore
{
public static class Program
{
public static void Main(string[] args)
{
var config = new ConfigurationBuilder()
.AddCommandLine(args)
.AddEnvironmentVariables(prefix: "ASPNETCORE_")
.Build();
var builder = new WebHostBuilder()
.UseConfiguration(config)
.UseIISIntegration()
.UseStartup("MusicStore")
.UseDefaultServiceProvider((context, options) => {
options.ValidateScopes = true;
});
var environment = builder.GetSetting("environment") ??
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
if (string.Equals(builder.GetSetting("server"), "Microsoft.AspNetCore.Server.HttpSys", System.StringComparison.Ordinal))
{
if (string.Equals(environment, "NtlmAuthentication", System.StringComparison.Ordinal))
{
// Set up NTLM authentication for WebListener like below.
// For IIS and IISExpress: Use inetmgr to setup NTLM authentication on the application vDir or
// modify the applicationHost.config to enable NTLM.
builder.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.NTLM;
options.Authentication.AllowAnonymous = false;
});
}
else
{
builder.UseHttpSys();
}
}
else
{
builder.UseKestrel();
}
// In Proc
builder.UseIIS();
builder.ConfigureLogging(factory =>
{
factory.AddConsole();
var logLevel = string.Equals(environment, "Development", StringComparison.Ordinal) ? LogLevel.Information : LogLevel.Warning;
factory.SetMinimumLevel(logLevel);
// Turn off Info logging for EF commands
factory.AddFilter("Microsoft.EntityFrameworkCore.Database.Command", LogLevel.Warning);
});
var host = builder.Build();
host.Run();
}
}
}

View File

@ -0,0 +1,9 @@
namespace MusicStore
{
public class AppSettings
{
public string SiteTitle { get; set; }
public bool CacheDbResults { get; set; } = true;
}
}

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:4088/",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"MusicStore": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "http://localhost:5000/",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
/// <autosync enabled="true" />
/// <reference path="../wwwroot/Scripts/bootstrap.js" />
/// <reference path="../wwwroot/Scripts/jquery.signalR-2.0.1.js" />
/// <reference path="../wwwroot/Scripts/jquery.validate.js" />
/// <reference path="../wwwroot/Scripts/jquery.validate.unobtrusive.js" />
/// <reference path="../wwwroot/Scripts/jquery-2.0.3.js" />
/// <reference path="../wwwroot/Scripts/modernizr-2.6.2.js" />
/// <reference path="../wwwroot/Scripts/respond.js" />

View File

@ -0,0 +1,223 @@
using System.Globalization;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MusicStore.Components;
using MusicStore.Models;
namespace MusicStore
{
public class Startup
{
private readonly Platform _platform;
public Startup(IHostingEnvironment hostingEnvironment)
{
// Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1'
// is found in both the registered sources, then the later source will win. By this way a Local config
// can be overridden by a different setting while deployed remotely.
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("config.json")
//All environment variables in the process's context flow in as configuration values.
.AddEnvironmentVariables();
Configuration = builder.Build();
_platform = new Platform();
}
public IConfiguration Configuration { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add EF services to the services container
if (_platform.UseInMemoryStore)
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseInMemoryDatabase("Scratch"));
}
else
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseSqlServer(Configuration[StoreConfig.ConnectionStringKey.Replace("__", ":")]));
}
// Add Identity services to the services container
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MusicStoreContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options => options.AccessDeniedPath = "/Home/AccessDenied");
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder.WithOrigins("http://example.com");
});
});
services.AddLogging();
// Add MVC services to the services container
services.AddMvc();
// Add memory cache services
services.AddMemoryCache();
services.AddDistributedMemoryCache();
// Add session related services.
services.AddSession();
// Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>();
// Configure Auth
services.AddAuthorization(options =>
{
options.AddPolicy(
"ManageStore",
authBuilder =>
{
authBuilder.RequireClaim("ManageStore", "Allowed");
});
});
services.AddAuthentication()
.AddFacebook(options =>
{
options.AppId = "550624398330273";
options.AppSecret = "10e56a291d6b618da61b1e0dae3a8954";
})
.AddGoogle(options =>
{
options.ClientId = "995291875932-0rt7417v5baevqrno24kv332b7d6d30a.apps.googleusercontent.com";
options.ClientSecret = "J_AT57H5KH_ItmMdu0r6PfXm";
})
.AddTwitter(options =>
{
options.ConsumerKey = "lDSPIu480ocnXYZ9DumGCDw37";
options.ConsumerSecret = "fpo0oWRNc3vsZKlZSq1PyOSoeXlJd7NnG4Rfc94xbFXsdcc3nH";
})
// The MicrosoftAccount service has restrictions that prevent the use of
// http://localhost:5001/ for test applications.
// As such, here is how to change this sample to uses http://ktesting.com:5001/ instead.
// From an admin command console first enter:
// notepad C:\Windows\System32\drivers\etc\hosts
// and add this to the file, save, and exit (and reboot?):
// 127.0.0.1 ktesting.com
// Then you can choose to run the app as admin (see below) or add the following ACL as admin:
// netsh http add urlacl url=http://ktesting:5001/ user=[domain\user]
// The sample app can then be run via:
// dnx . web
.AddMicrosoftAccount(options =>
{
// MicrosoftAccount requires project changes
options.ClientId = "000000004012C08A";
options.ClientSecret = "GaMQ2hCnqAC6EcDLnXsAeBVIJOLmeutL";
});
}
//This method is invoked when ASPNETCORE_ENVIRONMENT is 'Development' or is not defined
//The allowed values are Development,Staging and Production
public void ConfigureDevelopment(IApplicationBuilder app)
{
// StatusCode pages to gracefully handle status codes 400-599.
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
// Display custom error page in production when error occurs
// During development use the ErrorPage middleware to display error information in the browser
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
Configure(app);
}
//This method is invoked when ASPNETCORE_ENVIRONMENT is 'Staging'
//The allowed values are Development,Staging and Production
public void ConfigureStaging(IApplicationBuilder app)
{
// StatusCode pages to gracefully handle status codes 400-599.
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
app.UseExceptionHandler("/Home/Error");
Configure(app);
}
//This method is invoked when ASPNETCORE_ENVIRONMENT is 'Production'
//The allowed values are Development,Staging and Production
public void ConfigureProduction(IApplicationBuilder app)
{
// StatusCode pages to gracefully handle status codes 400-599.
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
app.UseExceptionHandler("/Home/Error");
Configure(app);
}
public void Configure(IApplicationBuilder app)
{
// force the en-US culture, so that the app behaves the same even on machines with different default culture
var supportedCultures = new[] { new CultureInfo("en-US") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.Use((context, next) =>
{
context.Response.Headers["Arch"] = RuntimeInformation.ProcessArchitecture.ToString();
return next();
});
// Configure Session.
app.UseSession();
// Add static files to the request pipeline
app.UseStaticFiles();
// Add cookie-based authentication to the request pipeline
app.UseAuthentication();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "api",
template: "{controller}/{id?}");
});
//Populates the MusicStore sample data
SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait();
}
}
}

View File

@ -0,0 +1,167 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Principal;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MusicStore.Components;
using MusicStore.Models;
namespace MusicStore
{
/// <summary>
/// To make runtime to load an environment based startup class, specify the environment by the following ways:
/// 1. Drop a Microsoft.AspNetCore.Hosting.ini file in the wwwroot folder
/// 2. Add a setting in the ini file named 'ASPNETCORE_ENVIRONMENT' with value of the format 'Startup[EnvironmentName]'.
/// For example: To load a Startup class named 'StartupNtlmAuthentication' the value of the env should be
/// 'NtlmAuthentication' (eg. ASPNETCORE_ENVIRONMENT=NtlmAuthentication). Runtime adds a 'Startup' prefix to this and
/// loads 'StartupNtlmAuthentication'.
/// If no environment name is specified the default startup class loaded is 'Startup'.
///
/// Alternative ways to specify environment are:
/// 1. Set the environment variable named SET ASPNETCORE_ENVIRONMENT=NtlmAuthentication
/// 2. For selfhost based servers pass in a command line variable named --env with this value. Eg:
/// "commands": {
/// "web": "Microsoft.AspNetCore.Hosting --server Microsoft.AspNetCore.Server.WebListener
/// --server.urls http://localhost:5002 --ASPNETCORE_ENVIRONMENT NtlmAuthentication",
/// },
/// </summary>
public class StartupNtlmAuthentication
{
public StartupNtlmAuthentication(IHostingEnvironment hostingEnvironment)
{
// Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1'
// is found in both the registered sources, then the later source will win. By this way a Local config
// can be overridden by a different setting while deployed remotely.
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("config.json")
//All environment variables in the process's context flow in as configuration values.
.AddEnvironmentVariables();
Configuration = builder.Build();
}
public IConfiguration Configuration { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add EF services to the services container
services.AddDbContext<MusicStoreContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
// Add Identity services to the services container
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MusicStoreContext>()
.AddDefaultTokenProviders();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder.WithOrigins("http://example.com");
});
});
// Add MVC services to the services container
services.AddMvc();
// Add memory cache services
services.AddMemoryCache();
services.AddDistributedMemoryCache();
// Add session related services.
services.AddSession();
// Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>();
// Configure Auth
services.AddAuthorization(options =>
{
options.AddPolicy(
"ManageStore",
authBuilder => {
authBuilder.RequireClaim("ManageStore", "Allowed");
});
});
}
public void Configure(IApplicationBuilder app)
{
// force the en-US culture, so that the app behaves the same even on machines with different default culture
var supportedCultures = new[] { new CultureInfo("en-US") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
// Error page middleware displays a nice formatted HTML page for any unhandled exceptions in the
// request pipeline.
// Note: Not recommended for production.
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.Use((context, next) =>
{
context.Response.Headers["Arch"] = RuntimeInformation.ProcessArchitecture.ToString();
return next();
});
app.Use(async (context, next) =>
{
// Who will get admin access? For demo sake I'm listing the currently logged on user as the application
// administrator. But this can be changed to suit the needs.
var identity = (ClaimsIdentity)context.User.Identity;
if (context.User.Identity.Name == WindowsIdentity.GetCurrent().Name)
{
identity.AddClaim(new Claim("ManageStore", "Allowed"));
}
await next.Invoke();
});
// Configure Session.
app.UseSession();
// Add static files to the request pipeline
app.UseStaticFiles();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "api",
template: "{controller}/{id?}");
});
//Populates the MusicStore sample data
SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices, false).Wait();
}
}
}

View File

@ -0,0 +1,165 @@
using System.Globalization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.AspNetCore.Localization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using MusicStore.Components;
using MusicStore.Models;
namespace MusicStore
{
/// <summary>
/// To make runtime to load an environment based startup class, specify the environment by the following ways:
/// 1. Drop a Microsoft.AspNetCore.Hosting.ini file in the wwwroot folder
/// 2. Add a setting in the ini file named 'ASPNETCORE_ENVIRONMENT' with value of the format 'Startup[EnvironmentName]'.
/// For example: To load a Startup class named 'StartupOpenIdConnect' the value of the env should be
/// 'OpenIdConnect' (eg. ASPNETCORE_ENVIRONMENT=OpenIdConnect). Runtime adds a 'Startup' prefix to this
/// and loads 'StartupOpenIdConnect'.
///
/// If no environment name is specified the default startup class loaded is 'Startup'.
/// Alternative ways to specify environment are:
/// 1. Set the environment variable named SET ASPNETCORE_ENVIRONMENT=OpenIdConnect
/// 2. For selfhost based servers pass in a command line variable named --env with this value. Eg:
/// "commands": {
/// "web": "Microsoft.AspNetCore.Hosting --server Microsoft.AspNetCore.Server.WebListener
/// --server.urls http://localhost:5002 --ASPNET_ENV OpenIdConnect",
/// },
/// </summary>
public class StartupOpenIdConnect
{
private readonly Platform _platform;
public StartupOpenIdConnect(IHostingEnvironment hostingEnvironment)
{
// Below code demonstrates usage of multiple configuration sources. For instance a setting say 'setting1'
// is found in both the registered sources, then the later source will win. By this way a Local config can
// be overridden by a different setting while deployed remotely.
var builder = new ConfigurationBuilder()
.SetBasePath(hostingEnvironment.ContentRootPath)
.AddJsonFile("config.json")
//All environment variables in the process's context flow in as configuration values.
.AddEnvironmentVariables();
Configuration = builder.Build();
_platform = new Platform();
}
public IConfiguration Configuration { get; private set; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSection("AppSettings"));
// Add EF services to the services container
if (_platform.UseInMemoryStore)
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseInMemoryDatabase("Scratch"));
}
else
{
services.AddDbContext<MusicStoreContext>(options =>
options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));
}
// Add Identity services to the services container
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<MusicStoreContext>()
.AddDefaultTokenProviders();
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", builder =>
{
builder.WithOrigins("http://example.com");
});
});
// Add MVC services to the services container
services.AddMvc();
// Add memory cache services
services.AddMemoryCache();
services.AddDistributedMemoryCache();
// Add session related services.
services.AddSession();
// Add the system clock service
services.AddSingleton<ISystemClock, SystemClock>();
// Configure Auth
services.AddAuthorization(options =>
{
options.AddPolicy(
"ManageStore",
authBuilder =>
{
authBuilder.RequireClaim("ManageStore", "Allowed");
});
});
// Create an Azure Active directory application and copy paste the following
services.AddAuthentication().AddOpenIdConnect(options =>
{
options.Authority = "https://login.windows.net/[tenantName].onmicrosoft.com";
options.ClientId = "[ClientId]";
options.ResponseType = OpenIdConnectResponseType.CodeIdToken;
});
}
public void Configure(IApplicationBuilder app)
{
// force the en-US culture, so that the app behaves the same even on machines with different default culture
var supportedCultures = new[] { new CultureInfo("en-US") };
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture("en-US"),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseStatusCodePagesWithRedirects("~/Home/StatusCodePage");
// Display custom error page in production when error occurs
// During development use the ErrorPage middleware to display error information in the browser
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
// Configure Session.
app.UseSession();
// Add static files to the request pipeline
app.UseStaticFiles();
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute(
name: "areaRoute",
template: "{area:exists}/{controller}/{action}",
defaults: new { action = "Index" });
routes.MapRoute(
name: "default",
template: "{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
routes.MapRoute(
name: "api",
template: "{controller}/{id?}");
});
//Populates the MusicStore sample data
SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait();
}
}
}

View File

@ -0,0 +1,9 @@
namespace MusicStore.ViewModels
{
public class AlbumData
{
public string Title { get; set; }
public string Url { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace MusicStore.ViewModels
{
public class ShoppingCartRemoveViewModel
{
public string Message { get; set; }
public decimal CartTotal { get; set; }
public int CartCount { get; set; }
public int ItemCount { get; set; }
public int DeleteId { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using MusicStore.Models;
namespace MusicStore.ViewModels
{
public class ShoppingCartViewModel
{
public List<CartItem> CartItems { get; set; }
public decimal CartTotal { get; set; }
}
}

View File

@ -0,0 +1,10 @@
@{
ViewBag.Title = "Confirm Email";
}
<h2>@ViewBag.Title.</h2>
<div>
<p>
Thank you for confirming your email. Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>.
</p>
</div>

View File

@ -0,0 +1,34 @@
@model ExternalLoginConfirmationViewModel
@{
ViewBag.Title = "Register";
}
<h2>@ViewBag.Title.</h2>
<h3>Associate your @ViewBag.LoginProvider account.</h3>
<form asp-controller="Account" asp-action="ExternalLoginConfirmation" asp-route-returnurl="@ViewBag.ReturnUrl" method="post" class="form-horizontal" role="form">
<h4>Association Form</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<p class="text-info">
You've successfully authenticated with <strong>@ViewBag.LoginProvider</strong>.
Please enter a user name for this site below and click the Register button to finish
logging in.
</p>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,8 @@
@{
ViewBag.Title = "Login Failure";
}
<hgroup>
<h2>@ViewBag.Title.</h2>
<h3 class="text-danger">Unsuccessful login with service.</h3>
</hgroup>

View File

@ -0,0 +1,28 @@
@model ForgotPasswordViewModel
@{
ViewBag.Title = "Forgot your password?";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Account" asp-action="ForgotPassword" method="post" class="form-horizontal" role="form">
<h4>Enter your email.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Email Link" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,15 @@
@{
ViewBag.Title = "Forgot Password Confirmation";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
</hgroup>
<div>
<p>
Please check your email to reset your password.
</p>
<p>
For demo purpose only: <a asp-action="ResetPassword" asp-route-code="@ViewBag.Code">Click here to reset the password</a>
</p>
</div>

View File

@ -0,0 +1,60 @@
@model LoginViewModel
@{
ViewBag.Title = "Log in";
}
<h2>@ViewBag.Title.</h2>
<div class="row">
<div class="col-md-8">
<section id="loginForm">
<form asp-controller="Account" asp-action="Login" asp-route-returnurl="@ViewBag.ReturnUrl" method="post" class="form-horizontal" role="form">
<h4>Use a local account to log in.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Password" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
<input asp-for="RememberMe" />
<label asp-for="RememberMe"></label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Log in" class="btn btn-default" />
</div>
</div>
<p>
<a asp-action="Register">Register as a new user?</a>
</p>
<p>
<a asp-action="ForgotPassword">Forgot your password?</a>
</p>
</form>
</section>
</div>
<div class="col-md-4">
<section id="socialLoginForm">
@await Html.PartialAsync("_ExternalLoginsListPartial", new ExternalLoginListViewModel { ReturnUrl = ViewBag.ReturnUrl })
</section>
</div>
</div>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,42 @@
@model RegisterViewModel
@{
ViewBag.Title = "Register";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Account" asp-action="Register" method="post" class="form-horizontal" role="form">
<h4>Create a new account.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Password" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Register" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,15 @@
@{
ViewBag.Title = "Register Confirmation";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
</hgroup>
<div>
<p>
Please check your email to activate your account.
</p>
<p>
Demo/testing purposes only: The sample displays the code and user id in the page: <a asp-action="ConfirmEmail" asp-route-code="@ViewBag.Code" asp-route-userId="@ViewBag.UserId">Click here to confirm your email: </a>
</p>
</div>

View File

@ -0,0 +1,43 @@
@model ResetPasswordViewModel
@{
ViewBag.Title = "Reset password";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Account" asp-action="ResetPassword" method="post" class="form-horizontal" role="form">
<h4>Reset your password.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Code" type="hidden" />
<div class="form-group">
<label asp-for="Email" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Password" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Reset" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,12 @@
@{
ViewBag.Title = "Reset password confirmation";
}
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
</hgroup>
<div>
<p>
Your password has been reset. Please <a asp-controller="Account" asp-action="Login">Click here to log in</a>.
</p>
</div>

View File

@ -0,0 +1,21 @@
@model SendCodeViewModel
@{
ViewBag.Title = "Send Verification Code";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Account" asp-action="SendCode" asp-route-returnurl="@Model.ReturnUrl" method="post" class="form-horizontal" role="form">
<input asp-for="RememberMe" type="hidden" />
<div class="row">
<div class="col-md-8">
Select Two-Factor Authentication Provider:
<select asp-for="SelectedProvider" asp-items="Model.Providers"></select>
<input type="submit" value="Submit" class="btn btn-default" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,43 @@
@model VerifyCodeViewModel
@{
ViewBag.Title = "Verify";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Account" asp-action="VerifyCode" asp-route-returnurl="@Model.ReturnUrl" method="post" class="form-horizontal" role="form">
<div asp-validation-summary="All" class="text-danger"></div>
<input asp-for="Provider" type="hidden" />
<input asp-for="RememberMe" type="hidden" />
<h4>Enter verification code</h4>
<p class="text-danger">
For DEMO only: You can type in this code in the below text box to proceed: [ @ViewBag.Code ]
<br />
Please change this code to register an SMS/Email service in IdentityConfig to send a message.
</p>
<hr />
<div class="form-group">
<label asp-for="Code" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Code" class="form-control" />
<span asp-validation-for="Code" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<div class="checkbox">
<input asp-for="RememberBrowser" />
<label asp-for="RememberBrowser"></label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Submit" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,31 @@
@using Microsoft.AspNetCore.Authentication
@model ExternalLoginListViewModel
@inject IAuthenticationSchemeProvider SchemeProvider
<h4>Use another service to log in.</h4>
<hr />
@{
var schemes = await SchemeProvider.GetAllSchemesAsync();
var loginProviders = schemes.ToList();
if (!loginProviders.Any())
{
<div>
<p>
There are no external authentication services configured. See <a href="http://go.microsoft.com/fwlink/?LinkId=313242">this article</a>
for details on setting up this ASP.NET application to support logging in via external services.
</p>
</div>
}
else
{
<form asp-controller="Account" asp-action="ExternalLogin" asp-route-returnurl="@ViewBag.ReturnUrl" method="post" class="form-horizontal" role="form">
<div id="socialLoginList">
<p>
@foreach (var p in loginProviders)
{
<button type="submit" class="btn btn-default" id="@p.Name" name="provider" value="@p.Name" title="Log in using your @p.Name account">@p.Name</button>
}
</p>
</div>
</form>
}
}

View File

@ -0,0 +1,33 @@
@model Order
@{
ViewBag.Title = "Address And Payment";
}
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}
<form asp-antiforgery="true">
<h2>Address And Payment</h2>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<fieldset>
<legend>Shipping Information</legend>
@Html.EditorForModel()
</fieldset>
<fieldset>
<legend>Payment</legend>
<p>We're running a promotion: all music is free with the promo code: "FREE"</p>
<div class="editor-label">
<label title="Promo Code">Promo Code</label>
</div>
<div class="editor-field">
@Html.TextBox("PromoCode")
</div>
</fieldset>
<input type="submit" value="Submit Order" />
</form>

View File

@ -0,0 +1,14 @@
@model int
@{
ViewBag.Title = "Checkout Complete";
}
<h2>Checkout Complete</h2>
<p>Thanks for your order! Your order number is: @Model</p>
<p>
How about shopping for some more music in our
<a asp-controller="Home" asp-action="Index">Store</a>
</p>

View File

@ -0,0 +1,21 @@
@inject IOptions<AppSettings> AppSettings
@{
ViewBag.Title = "Home Page";
}
<div class="jumbotron">
<h1>@AppSettings.Value.SiteTitle</h1>
<img src="~/Images/home-showcase.png" />
</div>
<ul class="row list-unstyled" id="album-list">
@foreach (var album in Model)
{
<li class="col-lg-2 col-md-2 col-sm-2 col-xs-4 container">
<a asp-controller="Store" asp-action="Details" asp-route-id="@album.AlbumId">
<img alt="@album.Title" src="@Url.Content(@album.AlbumArtUrl)" />
<h4>@album.Title</h4>
</a>
</li>
}
</ul>

View File

@ -0,0 +1,27 @@
@model AddPhoneNumberViewModel
@{
ViewBag.Title = "Add Phone Number";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Manage" asp-action="AddPhoneNumber" method="post" class="form-horizontal" role="form">
<h4>Add a phone number.</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Number" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Number" class="form-control" />
<span asp-validation-for="Number" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Send verification code" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,42 @@
@model ChangePasswordViewModel
@{
ViewBag.Title = "Change Password";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Manage" asp-action="ChangePassword" method="post" class="form-horizontal" role="form">
<h4>Change Password Form</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="OldPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="OldPassword" class="form-control" />
<span asp-validation-for="OldPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="NewPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="NewPassword" class="form-control" />
<span asp-validation-for="NewPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Change password" class="btn btn-default" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,70 @@
@model IndexViewModel
@{
ViewData["Title"] = "Manage your account";
}
<h2>@ViewData["Title"].</h2>
<p class="text-success">@ViewData["StatusMessage"]</p>
<div>
<h4>Change your account settings</h4>
<hr />
<dl class="dl-horizontal">
<dt>Password:</dt>
<dd>
@if (Model.HasPassword)
{
<text>[&nbsp;&nbsp;<a asp-controller="Manage" asp-action="ChangePassword">Change</a>&nbsp;&nbsp;]</text>
}
else
{
<text>[&nbsp;&nbsp;<a asp-controller="Manage" asp-action="SetPassword">Create</a>&nbsp;&nbsp;]</text>
}
</dd>
<dt>External Logins:</dt>
<dd>
@Model.Logins.Count [&nbsp;&nbsp;<a asp-controller="Manage" asp-action="ManageLogins">Manage</a>&nbsp;&nbsp;]
</dd>
<dt>Phone Number:</dt>
<dd>
<p>
Phone Numbers can used as a second factor of verification in two-factor authentication.
See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
for details on setting up this ASP.NET application to support two-factor authentication using SMS.
</p>
@*@(Model.PhoneNumber ?? "None")
@if (Model.PhoneNumber != null)
{
<br />
<text>[&nbsp;&nbsp;<a asp-controller="Manage" asp-action="AddPhoneNumber">Change</a>&nbsp;&nbsp;]</text>
<form asp-controller="Manage" asp-action="RemovePhoneNumber" method="post" role="form">
[<button type="submit" class="btn-link">Remove</button>]
</form>
}
else
{
<text>[&nbsp;&nbsp;<a asp-controller="Manage" asp-action="AddPhoneNumber">Add</a>&nbsp;&nbsp;]</text>
}*@
</dd>
<dt>Two-Factor Authentication:</dt>
<dd>
<p>
There are no two-factor authentication providers configured. See <a href="http://go.microsoft.com/fwlink/?LinkID=532713">this article</a>
for setting up this application to support two-factor authentication.
</p>
@*@if (Model.TwoFactor)
{
<form asp-controller="Manage" asp-action="DisableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
Enabled [<button type="submit" class="btn-link">Disable</button>]
</form>
}
else
{
<form asp-controller="Manage" asp-action="EnableTwoFactorAuthentication" method="post" class="form-horizontal" role="form">
[<button type="submit" class="btn-link">Enable</button>] Disabled
</form>
}*@
</dd>
</dl>
</div>

View File

@ -0,0 +1,53 @@
@model ManageLoginsViewModel
@{
ViewBag.Title = "Manage your external logins";
}
<h2>@ViewBag.Title.</h2>
<p class="text-success">@ViewBag.StatusMessage</p>
@if (Model.CurrentLogins.Count > 0)
{
<h4>Registered Logins</h4>
<table class="table">
<tbody>
@foreach (var account in Model.CurrentLogins)
{
<tr>
<td>@account.LoginProvider</td>
<td>
@if (ViewBag.ShowRemoveButton)
{
<form asp-controller="Manage" asp-action="RemoveLogin" method="post" class="form-horizontal" role="form">
<div>
<input asp-for="@account.LoginProvider" type="hidden" />
<input asp-for="@account.ProviderKey" type="hidden" />
<input type="submit" class="btn btn-default" value="Remove" title="Remove this @account.LoginProvider login from your account" />
</div>
</form>
}
else
{
@: &nbsp;
}
</td>
</tr>
}
</tbody>
</table>
}
@if (Model.OtherLogins.Any())
{
<h4>Add another service to log in.</h4>
<hr />
<form asp-controller="Manage" asp-action="LinkLogin" method="post" class="form-horizontal" role="form">
<div id="socialLoginList">
<p>
@foreach (var p in Model.OtherLogins)
{
<button type="submit" class="btn btn-default" id="@p.Name" name="provider" value="@p.Name" title="Log in using your @p.Name account">@p.Name</button>
}
</p>
</div>
</form>
}

View File

@ -0,0 +1,38 @@
@model SetPasswordViewModel
@{
ViewBag.Title = "Set Password";
}
<p class="text-info">
You do not have a local username/password for this site. Add a local
account so you can log in without an external login.
</p>
<form asp-controller="Manage" asp-action="SetPassword" method="post" class="form-horizontal" role="form">
<h4>Set your password</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="NewPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="NewPassword" class="form-control" />
<span asp-validation-for="NewPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Set password" class="btn btn-default" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,34 @@
@model VerifyPhoneNumberViewModel
@{
ViewBag.Title = "Verify Phone Number";
}
<h2>@ViewBag.Title.</h2>
<form asp-controller="Manage" asp-action="VerifyPhoneNumber" method="post" class="form-horizontal" role="form">
<input asp-for="PhoneNumber" type="hidden" />
<h4>Enter verification code</h4>
<p class="text-danger">
For DEMO only: You can type in this code in the below text box to proceed: @ViewBag.Code
<br />
Please change this code to register an SMS service in IdentityConfig to send a text message.
</p>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Code" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Code" class="form-control" />
<span asp-validation-for="Code" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Submit" />
</div>
</div>
</form>
@section Scripts {
@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }
}

View File

@ -0,0 +1,5 @@
@{
ViewBag.Title = "Access denied due to insufficient permissions";
}
<h1 class="text-danger">Access denied due to insufficient permissions.</h1>

View File

@ -0,0 +1,10 @@
@model Album
@if (Model != null)
{
<li>
<br />
<small><i><label title="New Arrivals!" style="color:red">New Arrivals!</label></i></small>
<i><a id="NewArrivalsPanel" asp-area="" asp-controller="Store" asp-action="Details" asp-route-id="@Model.AlbumId">@Model.Title</a></i>
</li>
}

Some files were not shown because too many files have changed in this diff Show More