diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Sales/Sales.csproj b/Sales/Sales.csproj new file mode 100644 index 0000000..2c53f40 --- /dev/null +++ b/Sales/Sales.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Sales/Sales.csproj b/Sales/Sales.csproj new file mode 100644 index 0000000..2c53f40 --- /dev/null +++ b/Sales/Sales.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/Sales/SalesDbContext.cs b/Sales/SalesDbContext.cs new file mode 100644 index 0000000..0a77307 --- /dev/null +++ b/Sales/SalesDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sales +{ + public class SalesDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=sales.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + + modelBuilder.Entity().HasData(new SalesProduct + { + Sku = "abc123", + Price = 85, + ForSale = true, + FreeShipping = false, + QuantityOnHand = 25 + }); + } + } + + public class SalesProduct + { + public string Sku { get; set; } + public decimal Price { get; set; } + public int QuantityOnHand { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Sales/Sales.csproj b/Sales/Sales.csproj new file mode 100644 index 0000000..2c53f40 --- /dev/null +++ b/Sales/Sales.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/Sales/SalesDbContext.cs b/Sales/SalesDbContext.cs new file mode 100644 index 0000000..0a77307 --- /dev/null +++ b/Sales/SalesDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sales +{ + public class SalesDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=sales.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + + modelBuilder.Entity().HasData(new SalesProduct + { + Sku = "abc123", + Price = 85, + ForSale = true, + FreeShipping = false, + QuantityOnHand = 25 + }); + } + } + + public class SalesProduct + { + public string Sku { get; set; } + public decimal Price { get; set; } + public int QuantityOnHand { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + + } +} \ No newline at end of file diff --git a/Warehouse/ConfigureServices.cs b/Warehouse/ConfigureServices.cs new file mode 100644 index 0000000..908b632 --- /dev/null +++ b/Warehouse/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Warehouse +{ + public static class ConfigureServices + { + public static void ConfigureWarehouseServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddWarehouseControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Sales/Sales.csproj b/Sales/Sales.csproj new file mode 100644 index 0000000..2c53f40 --- /dev/null +++ b/Sales/Sales.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/Sales/SalesDbContext.cs b/Sales/SalesDbContext.cs new file mode 100644 index 0000000..0a77307 --- /dev/null +++ b/Sales/SalesDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sales +{ + public class SalesDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=sales.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + + modelBuilder.Entity().HasData(new SalesProduct + { + Sku = "abc123", + Price = 85, + ForSale = true, + FreeShipping = false, + QuantityOnHand = 25 + }); + } + } + + public class SalesProduct + { + public string Sku { get; set; } + public decimal Price { get; set; } + public int QuantityOnHand { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + + } +} \ No newline at end of file diff --git a/Warehouse/ConfigureServices.cs b/Warehouse/ConfigureServices.cs new file mode 100644 index 0000000..908b632 --- /dev/null +++ b/Warehouse/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Warehouse +{ + public static class ConfigureServices + { + public static void ConfigureWarehouseServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddWarehouseControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Warehouse/Warehouse.csproj b/Warehouse/Warehouse.csproj new file mode 100644 index 0000000..12efb50 --- /dev/null +++ b/Warehouse/Warehouse.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1148ecd --- /dev/null +++ b/.gitignore @@ -0,0 +1,259 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +**/wwwroot/lib/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml +pub/ +/src/Web/WebMVC/Properties/PublishProfiles/eShopOnContainersWebMVC2016 - Web Deploy-publish.ps1 +/src/Web/WebMVC/Properties/PublishProfiles/publish-module.psm1 +/src/Services/Identity/eShopOnContainers.Identity/Properties/launchSettings.json + +#Ignore marker-file used to know which docker files we have. +.eshopdocker_* diff --git a/AspNetCore/AspNetCore.csproj b/AspNetCore/AspNetCore.csproj new file mode 100644 index 0000000..7797cb1 --- /dev/null +++ b/AspNetCore/AspNetCore.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + Demo + + + + + + + + + + + diff --git a/AspNetCore/Controllers/MvcController.cs b/AspNetCore/Controllers/MvcController.cs new file mode 100644 index 0000000..d6b4aae --- /dev/null +++ b/AspNetCore/Controllers/MvcController.cs @@ -0,0 +1,53 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Catalog.Products +{ + public class MvcController : Controller + { + private readonly CatalogDbContext _db; + + public MvcController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [HttpGet("/mvc/{sku}")] + public async Task Get([FromRoute]string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + return View("mvc", product); + } + + [HttpGet("/mvc/{sku}/images")] + public async Task Images([FromQuery]string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + return View("images", images); + } + + [HttpPost("/mvc/{sku}/save")] + public async Task Save([FromRoute]string sku, [FromForm]ProductResponse productResponse) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Name = productResponse.Name; + product.Description = productResponse.Description; + await _db.SaveChangesAsync(); + + return RedirectToAction("Get", new { sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Controllers/ProductCrudController.cs b/AspNetCore/Controllers/ProductCrudController.cs new file mode 100644 index 0000000..a3d1bf8 --- /dev/null +++ b/AspNetCore/Controllers/ProductCrudController.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Demo.Controllers +{ + public class ProductCrudController : Controller + { + private readonly CatalogDbContext _catalogDbContext; + + public ProductCrudController(CatalogDbContext catalogDbContext) + { + _catalogDbContext = catalogDbContext; + _catalogDbContext.Database.EnsureCreated(); + } + + [HttpGet("/productCrud/{sku}")] + public async Task GetProduct(string sku) + { + var record = await _catalogDbContext.Products + .AsNoTracking() + .SingleOrDefaultAsync(x => x.Sku == sku); + + if (record == null) + { + return NotFound(); + } + + return View(record); + } + + [HttpPost("/productCrud/{sku}")] + public async Task UpdateProduct([FromRoute] string sku, [FromForm]CatalogProduct product) + { + var record = await _catalogDbContext.Products.SingleOrDefaultAsync(x => x.Sku == sku); + + if (product == null) + { + return NotFound(); + } + + record.Description = product.Description; + record.Name = product.Name; + /* + record.Price = product.Price; + record.Cost = product.Cost; + record.QuantityOnHand = product.QuantityOnHand; + record.ForSale = product.Price > 0 && product.QuantityOnHand > 0 && product.ForSale; + record.FreeShipping = product.FreeShipping; + */ + + await _catalogDbContext.SaveChangesAsync(); + + return RedirectToAction("GetProduct", new { sku = sku }); + } + } +} \ No newline at end of file diff --git a/AspNetCore/Program.cs b/AspNetCore/Program.cs new file mode 100644 index 0000000..df97a9f --- /dev/null +++ b/AspNetCore/Program.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Demo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/AspNetCore/Properties/launchSettings.json b/AspNetCore/Properties/launchSettings.json new file mode 100644 index 0000000..1ba3181 --- /dev/null +++ b/AspNetCore/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:28524", + "sslPort": 44370 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "TaskBasedUI_HTTPAPI": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/AspNetCore/Startup.cs b/AspNetCore/Startup.cs new file mode 100644 index 0000000..79d0e89 --- /dev/null +++ b/AspNetCore/Startup.cs @@ -0,0 +1,81 @@ +using System; +using Catalog; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using Purchasing; +using Sales; +using Warehouse; + +namespace Demo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddSingleton(); + services.TryAddSingleton(); + services.AddScoped(x => { + var actionContext = x.GetRequiredService().ActionContext; + var factory = x.GetRequiredService(); + return factory.GetUrlHelper(actionContext); + }); + + services.AddSwaggerGen(c => + { + c.EnableAnnotations(); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); + }); + + services.ConfigureCatalogServices(); + + var mvcBuilder = services.AddControllersWithViews(); + mvcBuilder.AddCatalogControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseDeveloperExceptionPage(); + + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.DisplayOperationId(); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.UseHttpsRedirection(); + app.UseDefaultFiles(); + app.UseStaticFiles(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller=Home}/{action=Index}/{id?}"); + }); + } + } +} diff --git a/AspNetCore/Views/Mvc/images.cshtml b/AspNetCore/Views/Mvc/images.cshtml new file mode 100644 index 0000000..cbbf118 --- /dev/null +++ b/AspNetCore/Views/Mvc/images.cshtml @@ -0,0 +1,26 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ @foreach(var image in Model) + { + + } +
+ + + + \ No newline at end of file diff --git a/AspNetCore/Views/Mvc/mvc.cshtml b/AspNetCore/Views/Mvc/mvc.cshtml new file mode 100644 index 0000000..3da9095 --- /dev/null +++ b/AspNetCore/Views/Mvc/mvc.cshtml @@ -0,0 +1,179 @@ +@model dynamic +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@{ + Layout = null; +} + + + + + + + + Task + + + +
+ +
+
+
+
+ +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+ +
+ +
+ +
+
+ + + +
+ +
+ $ + 50 + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/AspNetCore/Views/ProductCrud/GetProduct.cshtml b/AspNetCore/Views/ProductCrud/GetProduct.cshtml new file mode 100644 index 0000000..d26dece --- /dev/null +++ b/AspNetCore/Views/ProductCrud/GetProduct.cshtml @@ -0,0 +1,71 @@ +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@model Catalog.CatalogProduct + + + + + + CRUD + + + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
+ $ + + CAD +
+
+
+ +
+ $ + + CAD +
+
+
+ + +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ + + +
+ + + + + \ No newline at end of file diff --git a/AspNetCore/appsettings.Development.json b/AspNetCore/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/AspNetCore/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/AspNetCore/appsettings.json b/AspNetCore/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/AspNetCore/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/AspNetCore/catalog.db b/AspNetCore/catalog.db new file mode 100644 index 0000000..5d4bc29 --- /dev/null +++ b/AspNetCore/catalog.db Binary files differ diff --git a/AspNetCore/wwwroot/css/site.css b/AspNetCore/wwwroot/css/site.css new file mode 100644 index 0000000..e679a8e --- /dev/null +++ b/AspNetCore/wwwroot/css/site.css @@ -0,0 +1,71 @@ +/* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification +for details on configuring this project to bundle and minify static web assets. */ + +a.navbar-brand { + white-space: normal; + text-align: center; + word-break: break-all; +} + +/* Provide sufficient contrast against white background */ +a { + color: #0366d6; +} + +.btn-primary { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +.nav-pills .nav-link.active, .nav-pills .show > .nav-link { + color: #fff; + background-color: #1b6ec2; + border-color: #1861ac; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + font-size: 14px; +} +@media (min-width: 768px) { + html { + font-size: 16px; + } +} + +.border-top { + border-top: 1px solid #e5e5e5; +} +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +button.accept-policy { + font-size: 1rem; + line-height: inherit; +} + +/* Sticky footer styles +-------------------------------------------------- */ +html { + position: relative; + min-height: 100%; +} + +body { + /* Margin bottom by footer height */ + margin-bottom: 60px; +} +.footer { + position: absolute; + bottom: 0; + width: 100%; + white-space: nowrap; + line-height: 60px; /* Vertically center the text there */ +} diff --git a/AspNetCore/wwwroot/favicon.ico b/AspNetCore/wwwroot/favicon.ico new file mode 100644 index 0000000..63e859b --- /dev/null +++ b/AspNetCore/wwwroot/favicon.ico Binary files differ diff --git a/AspNetCore/wwwroot/js/site.js b/AspNetCore/wwwroot/js/site.js new file mode 100644 index 0000000..7c45a88 --- /dev/null +++ b/AspNetCore/wwwroot/js/site.js @@ -0,0 +1,150 @@ +const sku = 'abc123'; + +async function saveProduct(sku, name, description) { + const url = `/catalog/products/${sku}`; + await fetch(url, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + name: name, + description: description + }) + }); +} + +async function increasePrice(sku, price) { + const url = `/sales/products/${sku}/increasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function decreasePrice(sku, price) { + const url = `/sales/products/${sku}/decreasePrice`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + price: price + }) + }); + + const product = await getSalesProduct(sku); + await setSalesInfo(product); +} + +async function inventoryAdjustment(sku, adjustmentQuantity) { + const url = `/warehouse/products/${sku}/inventoryAdjustment`; + await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + adjustmentQuantity: adjustmentQuantity + }) + }); + + const product = await getQuantityOnHand(sku); + setQuantityOnHandLabel(product); + + await loadSales(); +} + +async function getQuantityOnHand(sku) { + const url = `/warehouse/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setQuantityOnHandLabel(product) { + document.getElementById('quantityOnHand').innerText = product.quantityOnHand; +} + +async function getProductInfo(sku) { + const url = `/catalog/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +function setProductInfo(product) { + const productName = document.getElementById('productName'); + if (productName) { + productName.value = product.name; + } + + const productDescription = document.getElementById('productDescription'); + if (productDescription) { + productDescription.value = product.description; + } +} + +async function getSalesProduct(sku) { + const url = `/sales/products/${sku}`; + const response = await fetch(url); + return await response.json(); +} + +async function setSalesInfo(product) { + document.getElementById('productPrice').innerText = product.price; + document.getElementById('forSaleStatus').innerText = product.forSale ? 'Available' : 'Unavailable'; + + const setAsUnavailableLink = product.actions.find(x => x.name === 'UnavailableForSale'); + const setAsUnavailableItem = document.getElementById('setAsUnavailable'); + setAsUnavailableItem.style.display = setAsUnavailableLink ? 'block' : 'none'; + setAsUnavailableItem.dataset.href = setAsUnavailableLink?.href; + + const setAsAvailableLink = product.actions.find(x => x.name === 'AvailableForSale'); + const setAsAvailableItem = document.getElementById('setAsAvailable'); + setAsAvailableItem.style.display = setAsAvailableLink ? 'block' : 'none'; + setAsAvailableItem.dataset.href = setAsAvailableLink?.href; +} + +async function setAsAvailable() { + const item = document.getElementById('setAsAvailable'); + const result = await fetch(item.dataset.href,{ method: 'POST' }); + if (result.status === 400) { + const resultBody = await result.json(); + alert(resultBody.detail); + } else { + await loadSales(); + } + +} + +async function setAsUnavailable() { + const item = document.getElementById('setAsUnavailable'); + await fetch(item.dataset.href,{ method: 'POST' }); + await loadSales(); +} + +async function loadSales() { + const salesProduct = await getSalesProduct(sku); + await setSalesInfo(salesProduct); +} + +async function load() { + + const warehouseProduct = await getQuantityOnHand(sku); + setQuantityOnHandLabel(warehouseProduct); + + const catalogProduct = await getProductInfo(sku); + setProductInfo(catalogProduct); + + await loadSales(sku); +} + +load(); \ No newline at end of file diff --git a/CRUDTask.sln b/CRUDTask.sln new file mode 100644 index 0000000..aaedc25 --- /dev/null +++ b/CRUDTask.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspNetCore", "AspNetCore\AspNetCore.csproj", "{44389B47-D737-4056-8A18-887344A11864}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Catalog", "Catalog\Catalog.csproj", "{8A07E074-32E9-4EE8-9FEF-6425F248D0E1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x64.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.ActiveCfg = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Debug|x86.Build.0 = Debug|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|Any CPU.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x64.Build.0 = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.ActiveCfg = Release|Any CPU + {44389B47-D737-4056-8A18-887344A11864}.Release|x86.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x64.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.ActiveCfg = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Debug|x86.Build.0 = Debug|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|Any CPU.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x64.Build.0 = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.ActiveCfg = Release|Any CPU + {8A07E074-32E9-4EE8-9FEF-6425F248D0E1}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Catalog/Catalog.csproj b/Catalog/Catalog.csproj new file mode 100644 index 0000000..f849b1b --- /dev/null +++ b/Catalog/Catalog.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Catalog/CatalogDbContext.cs b/Catalog/CatalogDbContext.cs new file mode 100644 index 0000000..bcde7e1 --- /dev/null +++ b/Catalog/CatalogDbContext.cs @@ -0,0 +1,142 @@ +using System; +using Microsoft.EntityFrameworkCore; + +namespace Catalog +{ + public class CatalogDbContext : DbContext + { + public DbSet Products { get; set; } + public DbSet ProductImages { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=catalog.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + modelBuilder.Entity().HasKey(product => product.CatalogProductImageId); + + var product = new CatalogProduct("abc123", "Domain-Driven Design: Tackling Complexity in the Heart of Software", + "Leading software designers have recognized domain modeling and design as critical topics for at least twenty years, yet surprisingly little has been written about what needs to be done or how to do it. Although it has never been clearly formulated, a philosophy has developed as an undercurrent in the object community, which I call domain-driven design.", + 80, + 50); + product.CanSetAvailable(); + + modelBuilder.Entity().HasData(product); + + modelBuilder.Entity().HasData(new CatalogProductImage + { + CatalogProductImageId = 1, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/51sZW87slRL.jpg" + }, + new CatalogProductImage + { + CatalogProductImageId = 2, + Sku = "abc123", + Url = "https://images-na.ssl-images-amazon.com/images/I/81Tx5LAzr8L.jpg" + }); + } + } + + public class CatalogProduct + { + public CatalogProduct() + { + + } + + public CatalogProduct(string sku, string name, string description, decimal price, decimal cost) + { + Sku = sku; + Name = name; + Description = description; + Price = price; + Cost = cost; + } + + public string Sku { get; private set; } + public string Name { get; set; } + public string Description { get; set; } + public decimal Price { get; private set; } + public decimal Cost { get; private set; } + public int QuantityOnHand { get; private set; } + public bool ForSale { get; private set; } + public bool FreeShipping { get; private set; } + + public void SetAvailable() + { + if (CanSetAvailable()) + { + ForSale = true; + } + } + + public bool CanSetAvailable() + { + return ForSale == false && QuantityOnHand > 0; + } + + public string ValidationError() + { + return CanSetAvailable() + ? null + : "Product is must be unavailable and Quantity greater than 0"; + } + + public bool CanSetUnavailable() + { + return ForSale; + } + + public void InventoryAdjustment(int adjustment) + { + QuantityOnHand += adjustment; + if (QuantityOnHand <= 0) + { + UnavailableForSale(); + } + } + + public void IncreasePrice(decimal newPrice) + { + if (newPrice < Price) + { + throw new InvalidOperationException("New price must be greater than current price."); + } + + Price = newPrice; + } + + public void DecreasePrice(decimal newPrice) + { + if (newPrice > Price) + { + throw new InvalidOperationException("New price must be less than current price."); + } + + Price = newPrice; + + if (Price <= 0) + { + UnavailableForSale(); + } + } + + public void UnavailableForSale() + { + ForSale = false; + } + } + + public class CatalogProductImage + { + public int CatalogProductImageId { get; set; } + public string Sku { get; set; } + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/ConfigureServices.cs b/Catalog/ConfigureServices.cs new file mode 100644 index 0000000..92c2c93 --- /dev/null +++ b/Catalog/ConfigureServices.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Catalog +{ + public static class ConfigureServices + { + public static void ConfigureCatalogServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddCatalogControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/AvailableForSale.cs b/Catalog/Products/AvailableForSale.cs new file mode 100644 index 0000000..2f1f217 --- /dev/null +++ b/Catalog/Products/AvailableForSale.cs @@ -0,0 +1,46 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class AvailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public AvailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "AvailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/availableForSale")] + public async Task AvailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + if (product.CanSetAvailable() == false) + { + return BadRequest(new ProblemDetails + { + Detail = product.ValidationError() + }); + } + + product.SetAvailable(); + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/DecreasePrice.cs b/Catalog/Products/DecreasePrice.cs new file mode 100644 index 0000000..64ddda3 --- /dev/null +++ b/Catalog/Products/DecreasePrice.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.JSInterop.Infrastructure; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class DecreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public DecreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "DecreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/decreasePrice")] + public async Task DecreasePrice([FromRoute] string sku, [FromBody] PriceDecreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.DecreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceDecreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetQuantityOnHand.cs b/Catalog/Products/GetQuantityOnHand.cs new file mode 100644 index 0000000..6ac685f --- /dev/null +++ b/Catalog/Products/GetQuantityOnHand.cs @@ -0,0 +1,85 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetQuantityOnHandController : ControllerBase + { + private readonly IMediator _mediator; + + public GetQuantityOnHandController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "GetQuantityOnHand", + Tags = new[] { "Warehouse" })] + [HttpGet("/warehouse/products/{sku}", Name = "GetQuantityOnHand")] + public async Task GetQuantityOnHand([FromRoute] string sku) + { + var result = await _mediator.Send(new GetQuantityOnHandRequest(sku)); + if (result.Exists) + { + return Ok(result.Result); + } + + return NotFound(); + } + } + + public class GetQuantityOnHandRequest : IRequest<(bool Exists, GetQuantityOnHandResult Result)> + { + public GetQuantityOnHandRequest(string sku) + { + Sku = sku; + } + + public string Sku { get; set; } + } + + public class GetQuantityOnHandResult + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } + + public class GetQuantityOnHandHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + + public GetQuantityOnHandHandler(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + public async Task<(bool Exists, GetQuantityOnHandResult Result)> Handle(GetQuantityOnHandRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products + .Where(x => x.Sku == request.Sku) + .Select(x => new {x.QuantityOnHand}) + .SingleOrDefaultAsync(cancellationToken); + + if (product == null) + { + return (false, null); + } + + var result = new GetQuantityOnHandResult + { + Sku = request.Sku, + QuantityOnHand = product.QuantityOnHand + }; + + return (true, result); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/GetSalesProduct.cs b/Catalog/Products/GetSalesProduct.cs new file mode 100644 index 0000000..c0cd377 --- /dev/null +++ b/Catalog/Products/GetSalesProduct.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Sales; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class GetSalesProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public GetSalesProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _db.Database.EnsureCreated(); + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "GetSalesProduct", + Tags = new[] { "Sales" })] + [HttpGet("/sales/products/{sku}")] + public async Task GetSalesProduct([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductDto + { + Sku = sku, + Price = product.Price, + ForSale = product.ForSale, + FreeShipping = product.FreeShipping + }; + + result.Actions.Add(new Action("IncreasePrice", _urlHelper.Action("IncreasePrice", "IncreasePrice", new { sku }))); + result.Actions.Add(new Action("DecreasePrice", _urlHelper.Action("DecreasePrice", "DecreasePrice", new { sku }))); + result.Actions.Add(new Action("AvailableForSale", _urlHelper.Action("AvailableForSale", "AvailableForSale", new { sku = sku }))); + + if (product.CanSetUnavailable()) + { + result.Actions.Add(new Action("UnavailableForSale", _urlHelper.Action("UnavailableForSale", "UnavailableForSale", new {sku = sku}))); + } + + return Ok(result); + } + } + + public class ProductDto + { + public string Sku { get; set; } + public decimal Price { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + public IList Links { get; } = new List(); + public IList Actions { get; } = new List(); + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + +} \ No newline at end of file diff --git a/Catalog/Products/IncreasePrice.cs b/Catalog/Products/IncreasePrice.cs new file mode 100644 index 0000000..9a20bb0 --- /dev/null +++ b/Catalog/Products/IncreasePrice.cs @@ -0,0 +1,43 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + [ApiController] + public class IncreasePriceController : ControllerBase + { + private readonly CatalogDbContext _db; + + public IncreasePriceController(CatalogDbContext db) + { + _db = db; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "IncreasePrice", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/increasePrice")] + public async Task IncreasePrice([FromRoute] string sku, [FromBody] PriceIncreaseDto dto) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.IncreasePrice(dto.Price); + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class PriceIncreaseDto + { + public decimal Price { get; set; } + } +} \ No newline at end of file diff --git a/Catalog/Products/InventoryAdjustment.cs b/Catalog/Products/InventoryAdjustment.cs new file mode 100644 index 0000000..94c4cf3 --- /dev/null +++ b/Catalog/Products/InventoryAdjustment.cs @@ -0,0 +1,80 @@ +using System; +using System.Diagnostics.Eventing.Reader; +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Warehouse.Products +{ + [ApiController] + public class InventoryAdjustmentController : ControllerBase + { + private readonly IMediator _mediator; + + public InventoryAdjustmentController(IMediator mediator) + { + _mediator = mediator; + } + + [SwaggerOperation( + OperationId = "InventoryAdjustment", + Tags = new[] { "Warehouse" })] + [HttpPost("/warehouse/products/{sku}/inventoryAdjustment", Name = "InventoryAdjustment")] + public async Task InventoryAdjustment([FromRoute] string sku, [FromBody] InventoryAdjustmentRequest request) + { + request.Sku = sku; + await _mediator.Send(request); + + return NoContent(); + } + } + + public class InventoryAdjustmentRequest : IRequest + { + public string Sku { get; set; } + public int AdjustmentQuantity { get; set; } + } + + public class InventoryAdjustmentHandler : IRequestHandler + { + private readonly CatalogDbContext _db; + private readonly IMediator _mediator; + + public InventoryAdjustmentHandler(CatalogDbContext db, IMediator mediator) + { + _db = db; + _mediator = mediator; + _db.Database.EnsureCreated(); + } + + public async Task Handle(InventoryAdjustmentRequest request, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == request.Sku); + if (product == null) + { + throw new InvalidOperationException(); + } + + product.InventoryAdjustment(request.AdjustmentQuantity); + await _db.SaveChangesAsync(); + + return Unit.Value; + } + } + + public class InventoryAdjusted : INotification + { + public string Sku { get; } + public int QuantityOnHand { get; } + + public InventoryAdjusted(string sku, int quantityOnHand) + { + Sku = sku; + QuantityOnHand = quantityOnHand; + } + } +} \ No newline at end of file diff --git a/Catalog/Products/PlacePurchaseOrder.cs b/Catalog/Products/PlacePurchaseOrder.cs new file mode 100644 index 0000000..f16c21c --- /dev/null +++ b/Catalog/Products/PlacePurchaseOrder.cs @@ -0,0 +1,12 @@ +using MediatR; + +namespace Purchasing.Products +{ + public class PurchaseOrderRequisition : IRequest + { + public PurchaseOrderRequisition(string sku) + { + + } + } +} \ No newline at end of file diff --git a/Catalog/Products/ProductController.cs b/Catalog/Products/ProductController.cs new file mode 100644 index 0000000..6ad5ffe --- /dev/null +++ b/Catalog/Products/ProductController.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Catalog.Products +{ + [ApiController] + public class ProductController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public ProductController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + _db.Database.EnsureCreated(); + } + + [SwaggerOperation( + OperationId = "GetCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}")] + public async Task Get([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + var result = new ProductResponse + { + Sku = sku, + Name = product.Name, + Description = product.Description, + }; + + //result.Links.Add(new Link("GetCatalogProductImages", _urlHelper.Action("GetImages", new { sku }))); + //result.Actions.Add(new Action("UpdateCatalogProduct", _urlHelper.Action("Update", new { sku }))); + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "GetCatalogProductImages", + Tags = new[] { "Catalog" })] + [HttpGet("/catalog/products/{sku}/images")] + public async Task GetImages([FromRoute] string sku) + { + var images = await _db.ProductImages.Where(x => x.Sku == sku).ToArrayAsync(); + + var result = new ProductImagesResponse + { + Sku = sku, + Images = images.Select(x => x.Url).ToArray() + }; + + return Ok(result); + } + + [SwaggerOperation( + OperationId = "UpdateCatalogProduct", + Tags = new[] { "Catalog" })] + [HttpPut("/catalog/products/{sku}", Name = "Update")] + public async Task Update([FromRoute] string sku, [FromBody] ProductUpdateRequest updateRequest) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.Description = updateRequest.Description; + product.Name = updateRequest.Name; + await _db.SaveChangesAsync(); + + return NoContent(); + } + } + + public class ProductResponse + { + public string Sku { get; set; } + public string Name { get; set; } + public string Description { get; set; } + + //public IList Links { get; set; } = new List(); + //public IList Actions { get; set; } = new List(); + } + + public class ProductImagesResponse + { + public string Sku { get; set; } + public string[] Images { get; set; } + } + + public class ProductUpdateRequest + { + public string Name { get; set; } + public string Description { get; set; } + } + + public record Link(string Rel, string Href); + public record Action(string Name, string Href); + + +} \ No newline at end of file diff --git a/Catalog/Products/SetAsUnavailable.cs b/Catalog/Products/SetAsUnavailable.cs new file mode 100644 index 0000000..7aac8d2 --- /dev/null +++ b/Catalog/Products/SetAsUnavailable.cs @@ -0,0 +1,26 @@ +using System.Threading; +using System.Threading.Tasks; +using Catalog; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Warehouse.Products; + +namespace Sales.Products +{ + public class SetAsUnavailable : INotificationHandler + { + private readonly CatalogDbContext _db; + + public SetAsUnavailable(CatalogDbContext db) + { + _db = db; + } + + public async Task Handle(InventoryAdjusted notification, CancellationToken cancellationToken) + { + var product = await _db.Products.SingleAsync(x => x.Sku == notification.Sku, cancellationToken); + product.InventoryAdjustment(notification.QuantityOnHand); + await _db.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/Catalog/Products/UnavailableForSale.cs b/Catalog/Products/UnavailableForSale.cs new file mode 100644 index 0000000..dbf40dd --- /dev/null +++ b/Catalog/Products/UnavailableForSale.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Catalog; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; + +namespace Sales.Products +{ + public class UnavailableForSaleController : ControllerBase + { + private readonly CatalogDbContext _db; + private readonly IUrlHelper _urlHelper; + + public UnavailableForSaleController(CatalogDbContext db, IUrlHelper urlHelper) + { + _db = db; + _urlHelper = urlHelper; + } + + [SwaggerOperation( + OperationId = "UnavailableForSale", + Tags = new[] { "Sales" })] + [HttpPost("/sales/products/{sku}/unavailableForSale")] + public async Task UnavailableForSale([FromRoute] string sku) + { + var product = await _db.Products.SingleOrDefaultAsync(x => x.Sku == sku); + if (product == null) + { + return NotFound(); + } + + product.UnavailableForSale(); + + await _db.SaveChangesAsync(); + + return Created(_urlHelper.Action("GetSalesProduct", "GetSalesProduct", new { sku }), null); + } + } +} \ No newline at end of file diff --git a/Purchasing/ConfigureServices.cs b/Purchasing/ConfigureServices.cs new file mode 100644 index 0000000..91798c7 --- /dev/null +++ b/Purchasing/ConfigureServices.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Purchasing +{ + public static class ConfigureServices + { + public static void ConfigurePurchasingServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Purchasing/Purchasing.csproj b/Purchasing/Purchasing.csproj new file mode 100644 index 0000000..c36d812 --- /dev/null +++ b/Purchasing/Purchasing.csproj @@ -0,0 +1,22 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + diff --git a/Purchasing/PurchasingDbContext.cs b/Purchasing/PurchasingDbContext.cs new file mode 100644 index 0000000..8a3e952 --- /dev/null +++ b/Purchasing/PurchasingDbContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; + +namespace Purchasing +{ + public class PurchasingDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=purchasing.db"); + } + } + + public class PurchasingProduct + { + public string Sku { get; set; } + public string Cost { get; set; } + } +} \ No newline at end of file diff --git a/Sales/ConfigureServices.cs b/Sales/ConfigureServices.cs new file mode 100644 index 0000000..117aab3 --- /dev/null +++ b/Sales/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Sales +{ + public static class ConfigureServices + { + public static void ConfigureSalesServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddSalesControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Sales/Sales.csproj b/Sales/Sales.csproj new file mode 100644 index 0000000..2c53f40 --- /dev/null +++ b/Sales/Sales.csproj @@ -0,0 +1,23 @@ + + + + net5.0 + + + + + + + + + + + + + + + + + + + diff --git a/Sales/SalesDbContext.cs b/Sales/SalesDbContext.cs new file mode 100644 index 0000000..0a77307 --- /dev/null +++ b/Sales/SalesDbContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; + +namespace Sales +{ + public class SalesDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=sales.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + + modelBuilder.Entity().HasData(new SalesProduct + { + Sku = "abc123", + Price = 85, + ForSale = true, + FreeShipping = false, + QuantityOnHand = 25 + }); + } + } + + public class SalesProduct + { + public string Sku { get; set; } + public decimal Price { get; set; } + public int QuantityOnHand { get; set; } + public bool ForSale { get; set; } + public bool FreeShipping { get; set; } + + + } +} \ No newline at end of file diff --git a/Warehouse/ConfigureServices.cs b/Warehouse/ConfigureServices.cs new file mode 100644 index 0000000..908b632 --- /dev/null +++ b/Warehouse/ConfigureServices.cs @@ -0,0 +1,19 @@ +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace Warehouse +{ + public static class ConfigureServices + { + public static void ConfigureWarehouseServices(this IServiceCollection collection) + { + collection.AddDbContext(); + collection.AddMediatR(typeof(ConfigureServices).Assembly); + } + + public static void AddWarehouseControllers(this IMvcBuilder mvcBuilder) + { + mvcBuilder.AddApplicationPart(typeof(ConfigureServices).Assembly); + } + } +} \ No newline at end of file diff --git a/Warehouse/Warehouse.csproj b/Warehouse/Warehouse.csproj new file mode 100644 index 0000000..12efb50 --- /dev/null +++ b/Warehouse/Warehouse.csproj @@ -0,0 +1,19 @@ + + + + net5.0 + + + + + + + + + + + + + + + diff --git a/Warehouse/WarehouseDbContext.cs b/Warehouse/WarehouseDbContext.cs new file mode 100644 index 0000000..f2668ab --- /dev/null +++ b/Warehouse/WarehouseDbContext.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; + +namespace Warehouse +{ + public class WarehouseDbContext : DbContext + { + public DbSet Products { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + base.OnConfiguring(optionsBuilder); + optionsBuilder.UseSqlite("Data Source=warehouse.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.Entity().HasKey(product => product.Sku); + + modelBuilder.Entity().HasData(new WarehouseProduct + { + Sku = "abc123", + QuantityOnHand = 25 + }); + } + } + + public class WarehouseProduct + { + public string Sku { get; set; } + public int QuantityOnHand { get; set; } + } +} \ No newline at end of file