├── .gitignore ├── LICENSE ├── README.md ├── assets ├── home.png ├── sauce-delete.png ├── sauce-listing.png └── sauce-upsert.png └── src ├── Backend ├── Contoso.Pizza.AdminApi.MVC │ ├── Contoso.Pizza.AdminApi.MVC.csproj │ ├── Contoso.Pizza.AdminApi.MVC.http │ ├── Controllers │ │ ├── PizzasController.cs │ │ ├── SaucesController.cs │ │ └── ToppingsController.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── Contoso.Pizza.AdminApi.Minimal │ ├── AdminApiEndpoints.cs │ ├── Contoso.Pizza.AdminApi.Minimal.csproj │ ├── Contoso.Pizza.AdminApi.Minimal.http │ ├── Endpoints │ │ └── SauceEndpoints.cs │ ├── Handlers │ │ └── SauceEndpointHandler.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── Contoso.Pizza.AdminApi.Models │ ├── BaseEntity.cs │ ├── Contoso.Pizza.AdminApi.Models.csproj │ ├── PizzaEntity.cs │ ├── SauceEntity.cs │ └── ToppingEntity.cs ├── Contoso.Pizza.AdminApi.Services │ ├── Contoso.Pizza.AdminApi.Services.csproj │ ├── Contracts │ │ ├── IPizzaService.cs │ │ ├── ISauceService.cs │ │ ├── IService.cs │ │ └── IToppingService.cs │ ├── Extensions │ │ ├── HostExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ ├── Mappers │ │ ├── PizzaProfile.cs │ │ ├── SauceProfile.cs │ │ └── ToppingProfile.cs │ ├── PizzaService.cs │ ├── SauceService.cs │ └── ToppingService.cs ├── Contoso.Pizza.Console │ ├── Contoso.Pizza.Console.csproj │ ├── Program.cs │ └── appsettings.json ├── Contoso.Pizza.Data.Models │ ├── BaseModel.cs │ ├── Contoso.Pizza.Data.Models.csproj │ ├── Pizza.cs │ ├── PizzaTopping.cs │ ├── Sauce.cs │ └── Topping.cs ├── Contoso.Pizza.Data.Tests │ ├── Contoso.Pizza.Data.Tests.csproj │ └── SauceRepositoryTest.cs └── Contoso.Pizza.Data │ ├── Configutations │ ├── PizzaEntityConfiguration.cs │ ├── PizzaToppingEntityConfigration.cs │ ├── SauceEntityConfiguration.cs │ └── ToppingEntityConfiguration.cs │ ├── Contoso.Pizza.Data.csproj │ ├── ContosoPizzaDataContext.cs │ ├── ContosoPizzaDataContext.dgml │ ├── Contracts │ ├── IPizzaRepository.cs │ ├── IRepository.cs │ ├── ISauceRepository.cs │ └── IToppingRepository.cs │ ├── Extensions │ ├── EntityTypeBuilderExtensions.cs │ └── ServiceCollectionExtensions.cs │ ├── Initializers │ └── DbInitializer.cs │ ├── Migrations │ ├── 20240127141019_Initial.Designer.cs │ ├── 20240127141019_Initial.cs │ └── ContosoPizzaDataContextModelSnapshot.cs │ └── Repositories │ ├── PizzaRepository.cs │ ├── SauceRepository.cs │ └── ToppingRepository.cs ├── Contoso.Pizza.sln └── Frontend ├── Contoso.Pizza.AdminUI.Services ├── Contoso.Pizza.AdminUI.Services.csproj ├── Contracts │ ├── IPizzaService.cs │ ├── ISauceService.cs │ └── IToppingService.cs ├── Extensions │ └── ServiceCollectionExtensions.cs ├── PizzaService.cs ├── SauceService.cs └── ToppingService.cs └── Contoso.Pizza.AdminUI ├── Components ├── App.razor ├── Layout │ ├── MainLayout.razor │ ├── NavMenu.razor │ └── NavMenu.razor.css ├── Pages │ ├── BasePage.cs │ ├── Error.razor │ ├── Home.razor │ ├── Pizza │ │ ├── PizzaUpsertPanel.razor │ │ ├── PizzaUpsertPanel.razor.cs │ │ ├── PizzasPage.razor │ │ └── PizzasPage.razor.cs │ ├── Sauce │ │ ├── SauceUpsertPanel.razor │ │ ├── SaucesPage.razor │ │ └── SaucesPage.razor.cs │ └── Topping │ │ ├── ToppingUpsertPanel.razor │ │ ├── ToppingsPage.razor │ │ └── ToppingsPage.razor.cs ├── Routes.razor ├── Shared │ └── ProgressLoader.razor └── _Imports.razor ├── Contoso.Pizza.AdminUI.csproj ├── Program.cs ├── Properties └── launchSettings.json ├── appsettings.Development.json ├── appsettings.json └── wwwroot ├── app.css ├── favicon.ico └── images ├── authrequired.svg ├── counter.svg ├── home.svg └── weather.svg /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.tlog 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 298 | *.vbp 299 | 300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 301 | *.dsw 302 | *.dsp 303 | 304 | # Visual Studio 6 technical files 305 | *.ncb 306 | *.aps 307 | 308 | # Visual Studio LightSwitch build output 309 | **/*.HTMLClient/GeneratedArtifacts 310 | **/*.DesktopClient/GeneratedArtifacts 311 | **/*.DesktopClient/ModelManifest.xml 312 | **/*.Server/GeneratedArtifacts 313 | **/*.Server/ModelManifest.xml 314 | _Pvt_Extensions 315 | 316 | # Paket dependency manager 317 | .paket/paket.exe 318 | paket-files/ 319 | 320 | # FAKE - F# Make 321 | .fake/ 322 | 323 | # CodeRush personal settings 324 | .cr/personal 325 | 326 | # Python Tools for Visual Studio (PTVS) 327 | __pycache__/ 328 | *.pyc 329 | 330 | # Cake - Uncomment if you are using it 331 | # tools/** 332 | # !tools/packages.config 333 | 334 | # Tabs Studio 335 | *.tss 336 | 337 | # Telerik's JustMock configuration file 338 | *.jmconfig 339 | 340 | # BizTalk build output 341 | *.btp.cs 342 | *.btm.cs 343 | *.odx.cs 344 | *.xsd.cs 345 | 346 | # OpenCover UI analysis results 347 | OpenCover/ 348 | 349 | # Azure Stream Analytics local run output 350 | ASALocalRun/ 351 | 352 | # MSBuild Binary and Structured Log 353 | *.binlog 354 | 355 | # NVidia Nsight GPU debugger configuration file 356 | *.nvuser 357 | 358 | # MFractors (Xamarin productivity tool) working folder 359 | .mfractor/ 360 | 361 | # Local History for Visual Studio 362 | .localhistory/ 363 | 364 | # Visual Studio History (VSHistory) files 365 | .vshistory/ 366 | 367 | # BeatPulse healthcheck temp database 368 | healthchecksdb 369 | 370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 371 | MigrationBackup/ 372 | 373 | # Ionide (cross platform F# VS Code tools) working folder 374 | .ionide/ 375 | 376 | # Fody - auto-generated XML schema 377 | FodyWeavers.xsd 378 | 379 | # VS Code files for those working on multiple tools 380 | .vscode/* 381 | !.vscode/settings.json 382 | !.vscode/tasks.json 383 | !.vscode/launch.json 384 | !.vscode/extensions.json 385 | *.code-workspace 386 | 387 | # Local History for Visual Studio Code 388 | .history/ 389 | 390 | # Windows Installer files from build outputs 391 | *.cab 392 | *.msi 393 | *.msix 394 | *.msm 395 | *.msp 396 | 397 | # JetBrains Rider 398 | *.sln.iml 399 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lohith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor + AspNet Web API + EFCore Sample 2 | 3 | This application is a demo source code that showcases a [Blazor Server Web App](https://blazor.net) with Fluent UI Library. The backend API is built using ASP.NET Core Web API with Entity Framework Core. 4 | 5 | The [Blazor](https://blazor.net) Server Web App allows for building interactive web applications using C# instead of JavaScript. The [Fluent UI](fluentui-blazor.net) Library provides a set of reusable UI components that can be easily customized and integrated into the application. 6 | 7 | The [ASP.NET Core](https://dotnet.microsoft.com/en-us/apps/aspnet) Web API serves as the backend for the application, providing data and functionality to the Blazor frontend. It is built using the powerful and flexible ASP.NET Core framework, allowing for easy integration with other services and databases. 8 | 9 | [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) is used as the ORM (Object-Relational Mapping) tool to interact with the database. It provides a convenient way to define and work with database entities, making it easier to perform CRUD operations and manage data. 10 | 11 | This sample application serves as a starting point for developers who want to explore and learn about building Blazor Server Web Apps with Fluent UI and ASP.NET Core Web API with Entity Framework Core. Feel free to explore the code and customize it to fit your own requirements. 12 | 13 | ## Usage 14 | 15 | To use this sample application, follow these steps: 16 | 17 | 1. Clone the repository. 18 | 2. Open the solution in Visual Studio or your preferred IDE. 19 | 3. Set up API project: 20 | - Under `src/Backend/Contoso.Pizza.AdminAPI.MVC`, update the database connection string in the `appsettings.json` file to point to your desired database. 21 | ``` 22 | "ConnectionStrings": { 23 | "ContosoPizza": "" 24 | } 25 | ``` 26 | - Build the solution to restore NuGet packages and compile the code. 27 | - Run the database migrations to create the necessary tables and seed initial data. 28 | - Start the application `src/Backend/Contoso.Pizza.AdminAPI.MVC`. This is a ASP.NET MVC Core Web API project. It will open up a swagger page where you can test the API endpoints. 29 | 4. Set up Blazor project: 30 | - Under `src/Frontend/Contoso.Pizza.AdminUI`, build the solution to restore NuGet packages and compile the code. 31 | - Make sure to update the API Url in appsettings.json file to point to the API project. 32 | ``` 33 | "Api": { 34 | "Url": "https://localhost:7110" 35 | } 36 | ``` 37 | - Start the application `src/Frontend/Contoso.Pizza.AdminUI`. This is a Blazor Server Web App project. It will open up a web page where you can interact with the application. 38 | 39 | 40 | ## Screenshots 41 | Home Page 42 | 43 | ![Home Page](./assets/home.png) 44 | 45 | Sauce Listing 46 | 47 | ![Sauce Listing](./assets/sauce-listing.png) 48 | 49 | Sauce Upsert 50 | 51 | ![Sauce Upsert](./assets/sauce-upsert.png) 52 | 53 | Sauce Delete 54 | 55 | ![Sauce Delete](./assets/sauce-delete.png) 56 | 57 | Other entity UI are similar to the Sauce UI. -------------------------------------------------------------------------------- /assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-aspnetwebapi-efcore-sample/36c2329589e1233578e52911e32cbed598a06766/assets/home.png -------------------------------------------------------------------------------- /assets/sauce-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-aspnetwebapi-efcore-sample/36c2329589e1233578e52911e32cbed598a06766/assets/sauce-delete.png -------------------------------------------------------------------------------- /assets/sauce-listing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-aspnetwebapi-efcore-sample/36c2329589e1233578e52911e32cbed598a06766/assets/sauce-listing.png -------------------------------------------------------------------------------- /assets/sauce-upsert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-aspnetwebapi-efcore-sample/36c2329589e1233578e52911e32cbed598a06766/assets/sauce-upsert.png -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Contoso.Pizza.AdminApi.MVC.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | d094aa2a-1297-4849-99af-9a7e979fb248 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Contoso.Pizza.AdminApi.MVC.http: -------------------------------------------------------------------------------- 1 | @https-host = https://localhost:7110 2 | 3 | GET {{https-host}}/api/sauces 4 | 5 | ### 6 | 7 | POST {{https-host}}/api/sauces 8 | Content-Type: application/json 9 | 10 | { 11 | "name":"New Sauce 1", 12 | "description":"New Sauce 1 description" 13 | } 14 | 15 | ### -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Controllers/PizzasController.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminApi.Services.Contracts; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Contoso.Pizza.AdminApi.MVC.Controllers; 7 | 8 | [Route("api/[controller]")] 9 | [ApiController] 10 | public class PizzasController : ControllerBase 11 | { 12 | private readonly IPizzaService _service; 13 | 14 | public PizzasController(IPizzaService service) 15 | { 16 | _service = service; 17 | } 18 | 19 | [HttpGet( Name = nameof(GetPizzas))] 20 | [ProducesResponseType(StatusCodes.Status200OK)] 21 | public async Task> GetPizzas() 22 | { 23 | var pizzas = await _service.GetAllAsync(); 24 | return pizzas; 25 | } 26 | 27 | [HttpGet("{id:guid}", Name = "PizzaById")] 28 | [ProducesResponseType(StatusCodes.Status200OK)] 29 | [ProducesResponseType(StatusCodes.Status404NotFound)] 30 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 31 | public async Task>> GetPizzaById(Guid id) 32 | { 33 | var pizza = await _service.GetByIdAsync(id); 34 | 35 | return pizza == null ? TypedResults.NotFound() : 36 | TypedResults.Ok(pizza); 37 | } 38 | 39 | [HttpPost] 40 | [ProducesResponseType(StatusCodes.Status201Created)] 41 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 42 | public async Task Post([FromBody] PizzaEntity newPizza) 43 | { 44 | var createdPizza = await _service.AddAsync(newPizza); 45 | return CreatedAtAction(nameof(GetPizzaById), new { id = createdPizza.Id }, createdPizza); 46 | } 47 | 48 | 49 | [HttpPut("{id:guid}")] 50 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 51 | [ProducesResponseType(StatusCodes.Status404NotFound)] 52 | [ProducesResponseType(StatusCodes.Status200OK)] 53 | public async Task Put(Guid id, [FromBody] PizzaEntity pizza) 54 | { 55 | pizza.Id = id; 56 | var result = await _service.UpdateAsync(pizza); 57 | return result == 1 ? Ok() : NotFound(); 58 | } 59 | 60 | // DELETE api/sauces/5 61 | [HttpDelete("{id:guid}")] 62 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 63 | [ProducesResponseType(StatusCodes.Status404NotFound)] 64 | [ProducesResponseType(StatusCodes.Status409Conflict)] 65 | [ProducesResponseType(StatusCodes.Status200OK)] 66 | public async Task Delete(Guid id) 67 | { 68 | try 69 | { 70 | var result = await _service.DeleteAsync(id); 71 | return result == 1 ? Ok() : NotFound(); 72 | } 73 | catch (InvalidOperationException ex) 74 | { 75 | return Conflict(ex.Message); 76 | } 77 | catch 78 | { 79 | return BadRequest(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Controllers/SaucesController.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminApi.Services.Contracts; 3 | using Contoso.Pizza.Data.Models; 4 | using Microsoft.AspNetCore.Http.HttpResults; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Contoso.Pizza.AdminApi.MVC.Controllers; 8 | 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class SaucesController : ControllerBase 12 | { 13 | private readonly ISauceService _service; 14 | 15 | public SaucesController(ISauceService service) 16 | { 17 | _service = service; 18 | } 19 | 20 | [HttpGet] 21 | public async Task> GetSauces() 22 | { 23 | var sauces = await _service.GetAllAsync(); 24 | return sauces; 25 | } 26 | 27 | [HttpGet("{id:guid}", Name = "SauceById")] 28 | [ProducesResponseType(StatusCodes.Status200OK)] 29 | [ProducesResponseType(StatusCodes.Status404NotFound)] 30 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 31 | public async Task>> GetSauceById(Guid id) 32 | { 33 | var sauce = await _service.GetByIdAsync(id); 34 | 35 | return sauce == null ? TypedResults.NotFound() : 36 | TypedResults.Ok(sauce); 37 | } 38 | 39 | [HttpPost] 40 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 41 | [ProducesResponseType(StatusCodes.Status201Created)] 42 | public async Task Post([FromBody] SauceEntity newSauce) 43 | { 44 | var createdSauce = await _service.AddAsync(newSauce); 45 | return CreatedAtAction(nameof(GetSauceById), new { id = createdSauce.Id }, createdSauce); 46 | } 47 | 48 | 49 | [HttpPut("{id:guid}")] 50 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 51 | [ProducesResponseType(StatusCodes.Status404NotFound)] 52 | [ProducesResponseType(StatusCodes.Status200OK)] 53 | public async Task Put(Guid id, [FromBody] SauceEntity sauce) 54 | { 55 | sauce.Id = id; 56 | var result = await _service.UpdateAsync(sauce); 57 | return result == 1 ? Ok() : NotFound(); 58 | } 59 | 60 | [HttpDelete("{id:guid}")] 61 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 62 | [ProducesResponseType(StatusCodes.Status404NotFound)] 63 | [ProducesResponseType(StatusCodes.Status409Conflict)] 64 | [ProducesResponseType(StatusCodes.Status200OK)] 65 | public async Task Delete(Guid id) 66 | { 67 | try 68 | { 69 | var result = await _service.DeleteAsync(id); 70 | return result == 1 ? Ok() : NotFound(); 71 | } 72 | catch (InvalidOperationException ex) 73 | { 74 | return Conflict(ex.Message); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Controllers/ToppingsController.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminApi.Services.Contracts; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Contoso.Pizza.AdminApi.MVC.Controllers; 7 | 8 | [Route("api/[controller]")] 9 | [ApiController] 10 | public class ToppingsController : ControllerBase 11 | { 12 | private readonly IToppingService _service; 13 | 14 | public ToppingsController(IToppingService service) 15 | { 16 | _service = service; 17 | } 18 | 19 | [HttpGet] 20 | public async Task> GetToppings() 21 | { 22 | var toppings = await _service.GetAllAsync(); 23 | return toppings; 24 | } 25 | 26 | [HttpGet("{id:guid}", Name = "ToppingById")] 27 | [ProducesResponseType(StatusCodes.Status200OK)] 28 | [ProducesResponseType(StatusCodes.Status404NotFound)] 29 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 30 | public async Task>> GetToppingById(Guid id) 31 | { 32 | var topping = await _service.GetByIdAsync(id); 33 | 34 | return topping == null ? TypedResults.NotFound() : 35 | TypedResults.Ok(topping); 36 | } 37 | 38 | [HttpPost] 39 | [ProducesResponseType(StatusCodes.Status201Created)] 40 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 41 | public async Task Post([FromBody] ToppingEntity newTopping) 42 | { 43 | var createdTopping = await _service.AddAsync(newTopping); 44 | return CreatedAtAction(nameof(GetToppingById), new { id = createdTopping.Id }, createdTopping); 45 | } 46 | 47 | 48 | [HttpPut("{id:guid}")] 49 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 50 | [ProducesResponseType(StatusCodes.Status404NotFound)] 51 | [ProducesResponseType(StatusCodes.Status200OK)] 52 | public async Task Put(Guid id, [FromBody] ToppingEntity topping) 53 | { 54 | topping.Id = id; 55 | var result = await _service.UpdateAsync(topping); 56 | return result == 1 ? Ok() : NotFound(); 57 | } 58 | 59 | // DELETE api/sauces/5 60 | [HttpDelete("{id:guid}")] 61 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 62 | [ProducesResponseType(StatusCodes.Status404NotFound)] 63 | [ProducesResponseType(StatusCodes.Status409Conflict)] 64 | [ProducesResponseType(StatusCodes.Status200OK)] 65 | public async Task Delete(Guid id) 66 | { 67 | try 68 | { 69 | var result = await _service.DeleteAsync(id); 70 | return result == 1 ? Ok() : NotFound(); 71 | } 72 | catch (InvalidOperationException ex) 73 | { 74 | return Conflict(ex.Message); 75 | } 76 | catch 77 | { 78 | return BadRequest(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | using Contoso.Pizza.AdminApi.Services.Extensions; 3 | 4 | namespace Contoso.Pizza.AdminApi.MVC; 5 | 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | //Register Contoso App Services 13 | AddApplicationServices(builder); 14 | 15 | //Register MVC Services 16 | AddMvcServices(builder); 17 | 18 | var app = builder.Build(); 19 | 20 | // Configure the HTTP request pipeline. 21 | ConfigureMvcServices(app); 22 | 23 | app.CreateDbIfNotExists(); 24 | 25 | app.Run(); 26 | } 27 | 28 | private static void ConfigureMvcServices(WebApplication app) 29 | { 30 | if (app.Environment.IsDevelopment()) 31 | { 32 | app.UseSwagger(); 33 | app.UseSwaggerUI(); 34 | } 35 | 36 | app.UseHttpsRedirection(); 37 | 38 | app.UseAuthorization(); 39 | 40 | app.MapControllers(); 41 | } 42 | 43 | private static void AddMvcServices(WebApplicationBuilder builder) 44 | { 45 | builder.Services.AddControllers(); 46 | builder.Services.AddEndpointsApiExplorer(); 47 | builder.Services.AddSwaggerGen(); 48 | } 49 | 50 | private static void AddApplicationServices(WebApplicationBuilder builder) 51 | { 52 | builder.Services.AddContosoPizzaServices(builder.Configuration, 53 | builder.Environment.IsProduction()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:59111", 8 | "sslPort": 44385 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5037", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7110;http://localhost:5037", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ConnectionStrings": { 9 | "ContosoPizza": "" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.MVC/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "ContosoPizza": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/AdminApiEndpoints.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | 3 | namespace Contoso.Pizza.AdminApi.Minimal 4 | { 5 | public static class AdminApiEndpoints 6 | { 7 | public static void Map(WebApplication app) 8 | { 9 | app.MapGet("/lohith", async (ISauceRepository sauceRepository) => 10 | { 11 | return await sauceRepository.GetAllAsync(); 12 | }); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Contoso.Pizza.AdminApi.Minimal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | false 8 | 979905d7-be66-42d9-8a01-59a24f5265d1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Contoso.Pizza.AdminApi.Minimal.http: -------------------------------------------------------------------------------- 1 | @Contoso.Pizza.AdminApi.Minimal_HostAddress = http://localhost:5237 2 | 3 | GET {{Contoso.Pizza.AdminApi.Minimal_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Endpoints/SauceEndpoints.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Minimal.Handlers; 2 | 3 | namespace Contoso.Pizza.AdminApi.Minimal.Endpoints; 4 | 5 | public static class SauceEndpoints 6 | { 7 | public static void Map(WebApplication app) 8 | { 9 | var handler = new SauceEndpointHandler(); 10 | app.MapGroup("/api/sauces") 11 | .MapGet("/", handler.GetAllSauces); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Handlers/SauceEndpointHandler.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminApi.Services.Contracts; 3 | using Microsoft.AspNetCore.Http.HttpResults; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Contoso.Pizza.AdminApi.Minimal.Handlers; 7 | 8 | public class SauceEndpointHandler 9 | { 10 | public async Task> GetAllSauces([FromServices] ISauceService service) 11 | { 12 | return await service.GetAllAsync(); 13 | } 14 | 15 | public async Task>> GetSauceById([FromServices] ISauceService service, Guid id) 16 | { 17 | var sauce = await service.GetByIdAsync(id); 18 | 19 | return sauce == null ? TypedResults.NotFound() : 20 | TypedResults.Ok(sauce); 21 | } 22 | 23 | public async Task AddSauce([FromServices] ISauceService service, [FromBody] SauceEntity newSauce) 24 | { 25 | var createdSauce = await service.AddAsync(newSauce); 26 | return TypedResults.CreatedAtRoute(nameof(GetSauceById), new { id = createdSauce.Id }); 27 | } 28 | 29 | public async Task> UpdateSauce([FromServices] ISauceService service, Guid id, [FromBody] SauceEntity sauce) 30 | { 31 | sauce.Id = id; 32 | var result = await service.UpdateAsync(sauce); 33 | return result == 1 ? TypedResults.Ok() : TypedResults.NotFound(); 34 | } 35 | 36 | public async Task>> DeleteSauce([FromServices] ISauceService service, Guid id) 37 | { 38 | try 39 | { 40 | var result = await service.DeleteAsync(id); 41 | return result == 1 ? TypedResults.Ok() : TypedResults.NotFound(); 42 | } 43 | catch (InvalidOperationException ex) 44 | { 45 | return TypedResults.Conflict(ex.Message); 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Program.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Minimal.Endpoints; 2 | using Contoso.Pizza.AdminApi.Minimal.Handlers; 3 | using Contoso.Pizza.AdminApi.Services.Extensions; 4 | 5 | namespace Contoso.Pizza.AdminApi.Minimal; 6 | 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | var builder = WebApplication.CreateBuilder(args); 12 | 13 | // Add services to the container. 14 | AddContosoServices(builder); 15 | 16 | // Add minimal API services to the container. 17 | AddMinimalApiServices(builder); 18 | 19 | var app = builder.Build(); 20 | 21 | // Configure the HTTP request pipeline. 22 | ConfigureMinimalApiServices(app); 23 | 24 | // Configure endpoints. 25 | ConfigureEndpoints(app); 26 | 27 | app.Run(); 28 | } 29 | 30 | private static void ConfigureEndpoints(WebApplication app) 31 | { 32 | SauceEndpoints.Map(app); 33 | } 34 | 35 | private static void ConfigureMinimalApiServices(WebApplication app) 36 | { 37 | if (app.Environment.IsDevelopment()) 38 | { 39 | app.UseSwagger(); 40 | app.UseSwaggerUI(); 41 | } 42 | 43 | app.UseHttpsRedirection(); 44 | 45 | app.UseAuthorization(); 46 | } 47 | 48 | private static void AddMinimalApiServices(WebApplicationBuilder builder) 49 | { 50 | builder.Services.AddAuthorization(); 51 | builder.Services.AddEndpointsApiExplorer(); 52 | builder.Services.AddSwaggerGen(); 53 | } 54 | 55 | private static void AddContosoServices(WebApplicationBuilder builder) 56 | { 57 | builder.Services.AddContosoPizzaServices(builder.Configuration, builder.Environment.IsProduction()); 58 | builder.Services.AddScoped(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:45712", 8 | "sslPort": 44307 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5237", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7010;http://localhost:5237", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Minimal/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "ContosoPizza": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Models/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.AdminApi.Models; 2 | 3 | public abstract class BaseEntity 4 | { 5 | public Guid Id { get; set; } 6 | public DateTime Created { get; set; } 7 | public DateTime? Modified { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Models/Contoso.Pizza.AdminApi.Models.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Models/PizzaEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.AdminApi.Models; 2 | 3 | public class PizzaEntity : BaseEntity 4 | { 5 | public required string Name { get; set; } 6 | public string? Description { get; set; } = string.Empty; 7 | public Guid SauceId { get; set; } 8 | public SauceEntity? Sauce { get; set; } 9 | public List Toppings { get; set; } = new(); 10 | } 11 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Models/SauceEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.AdminApi.Models; 2 | 3 | public class SauceEntity : BaseEntity 4 | { 5 | public string Name { get; set; } = string.Empty; 6 | public string? Description { get; set; } 7 | public bool IsVegan { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Models/ToppingEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.AdminApi.Models; 2 | 3 | public class ToppingEntity : BaseEntity 4 | { 5 | public required string Name { get; set; } 6 | public string Description { get; set; } = string.Empty; 7 | public int Calories { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Contoso.Pizza.AdminApi.Services.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Contracts/IPizzaService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminApi.Services.Contracts; 4 | 5 | public interface IPizzaService : IService 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Contracts/ISauceService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminApi.Services.Contracts; 4 | 5 | public interface ISauceService : IService 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Contracts/IService.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.AdminApi.Services.Contracts 2 | { 3 | public interface IService 4 | where T : class 5 | { 6 | Task GetByIdAsync(Guid id); 7 | Task> GetAllAsync(); 8 | Task AddAsync(T entity); 9 | Task UpdateAsync(T entity); 10 | Task DeleteAsync(Guid id); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Contracts/IToppingService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminApi.Services.Contracts; 4 | 5 | public interface IToppingService : IService 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Extensions/HostExtensions.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data; 2 | using Contoso.Pizza.Data.Initializers; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | 6 | namespace Contoso.Pizza.AdminApi.Services.Extensions; 7 | 8 | public static class HostExtensions 9 | { 10 | public static void CreateDbIfNotExists(this IHost host) 11 | { 12 | using (var scope = host.Services.CreateScope()) 13 | { 14 | var services = scope.ServiceProvider; 15 | var context = services.GetRequiredService(); 16 | context.Database.EnsureCreated(); 17 | DbInitializer.Initialize(context); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Services.Contracts; 2 | using Contoso.Pizza.Data.Extensions; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System.Reflection; 6 | 7 | namespace Contoso.Pizza.AdminApi.Services.Extensions 8 | { 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddContosoPizzaServices(this IServiceCollection services, 12 | IConfiguration configuration, 13 | bool isProduction = true) 14 | { 15 | services.AddContosoPizzaData(configuration, isProduction); 16 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 17 | services.AddScoped(); 18 | services.AddScoped(); 19 | services.AddScoped(); 20 | return services; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Mappers/PizzaProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using DM = Contoso.Pizza.Data.Models; 4 | 5 | namespace Contoso.Pizza.AdminApi.Services.Mappers; 6 | 7 | public class PizzaProfile : Profile 8 | { 9 | public PizzaProfile() 10 | { 11 | CreateMap() 12 | .ReverseMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Mappers/SauceProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using Contoso.Pizza.Data.Models; 4 | 5 | namespace Contoso.Pizza.AdminApi.Services.Mappers 6 | { 7 | public class SauceProfile : Profile 8 | { 9 | public SauceProfile() 10 | { 11 | CreateMap() 12 | .ReverseMap(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/Mappers/ToppingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using Contoso.Pizza.Data.Models; 4 | 5 | namespace Contoso.Pizza.AdminApi.Services.Mappers; 6 | 7 | public class ToppingProfile : Profile 8 | { 9 | public ToppingProfile() 10 | { 11 | CreateMap() 12 | .ReverseMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/PizzaService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using Contoso.Pizza.AdminApi.Services.Contracts; 4 | using Contoso.Pizza.Data.Contracts; 5 | using DM = Contoso.Pizza.Data.Models; 6 | 7 | namespace Contoso.Pizza.AdminApi.Services; 8 | 9 | public class PizzaService : IPizzaService 10 | { 11 | private readonly IPizzaRepository _repository; 12 | private readonly IMapper _mapper; 13 | 14 | public PizzaService(IPizzaRepository repository, IMapper mapper) 15 | { 16 | _repository = repository; 17 | _mapper = mapper; 18 | } 19 | 20 | public async Task> GetAllAsync() 21 | { 22 | var pizzas = await _repository.GetAllAsync(); 23 | return _mapper.Map>(pizzas); 24 | } 25 | 26 | public async Task GetByIdAsync(Guid id) 27 | { 28 | var pizza = await _repository.GetByIdAsync(id); 29 | return _mapper.Map(pizza); 30 | } 31 | 32 | public async Task AddAsync(PizzaEntity entity) 33 | { 34 | var newPizza = _mapper.Map(entity); 35 | await _repository.AddAsync(newPizza); 36 | return _mapper.Map(newPizza); 37 | } 38 | 39 | public async Task UpdateAsync(PizzaEntity entity) 40 | { 41 | var pizzaToUpdate = _mapper.Map(entity); 42 | return await _repository.UpdateAsync(pizzaToUpdate); 43 | } 44 | 45 | public async Task DeleteAsync(Guid id) 46 | { 47 | return await _repository.DeleteAsync(id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/SauceService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using Contoso.Pizza.AdminApi.Services.Contracts; 4 | using Contoso.Pizza.Data.Contracts; 5 | using Contoso.Pizza.Data.Models; 6 | using Contoso.Pizza.Data.Repositories; 7 | 8 | namespace Contoso.Pizza.AdminApi.Services; 9 | 10 | public class SauceService : ISauceService 11 | { 12 | private readonly ISauceRepository _repository; 13 | private readonly IMapper _mapper; 14 | 15 | public SauceService(ISauceRepository repository, IMapper mapper) 16 | { 17 | _repository = repository; 18 | _mapper = mapper; 19 | } 20 | 21 | public async Task> GetAllAsync() 22 | { 23 | var sauces = await _repository.GetAllAsync(); 24 | 25 | return _mapper.Map>(sauces); 26 | } 27 | 28 | public async Task GetByIdAsync(Guid id) 29 | { 30 | var sauce = await _repository.GetByIdAsync(id); 31 | return _mapper.Map(sauce); 32 | } 33 | 34 | public async Task AddAsync(SauceEntity entity) 35 | { 36 | var newSauce = _mapper.Map(entity); 37 | await _repository.AddAsync(newSauce); 38 | return _mapper.Map(newSauce); 39 | } 40 | 41 | public async Task UpdateAsync(SauceEntity entity) 42 | { 43 | var sauceToUpdate = _mapper.Map(entity); 44 | return await _repository.UpdateAsync(sauceToUpdate); 45 | } 46 | 47 | public async Task DeleteAsync(Guid id) 48 | { 49 | return await _repository.DeleteAsync(id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.AdminApi.Services/ToppingService.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Contoso.Pizza.AdminApi.Models; 3 | using Contoso.Pizza.AdminApi.Services.Contracts; 4 | using Contoso.Pizza.Data.Contracts; 5 | using Contoso.Pizza.Data.Models; 6 | 7 | namespace Contoso.Pizza.AdminApi.Services; 8 | 9 | public class ToppingService : IToppingService 10 | { 11 | private readonly IToppingRepository _repository; 12 | private readonly IMapper _mapper; 13 | 14 | public ToppingService(IToppingRepository repository, IMapper mapper) 15 | { 16 | _repository = repository; 17 | _mapper = mapper; 18 | } 19 | 20 | public async Task> GetAllAsync() 21 | { 22 | var toppings = await _repository.GetAllAsync(); 23 | 24 | return _mapper.Map>(toppings); 25 | } 26 | 27 | public async Task GetByIdAsync(Guid id) 28 | { 29 | var sauce = await _repository.GetByIdAsync(id); 30 | return _mapper.Map(sauce); 31 | } 32 | 33 | public async Task AddAsync(ToppingEntity entity) 34 | { 35 | var newTopping = _mapper.Map(entity); 36 | await _repository.AddAsync(newTopping); 37 | return _mapper.Map(newTopping); 38 | } 39 | 40 | public async Task UpdateAsync(ToppingEntity entity) 41 | { 42 | var toppingToUpdate = _mapper.Map(entity); 43 | return await _repository.UpdateAsync(toppingToUpdate); 44 | } 45 | 46 | public async Task DeleteAsync(Guid id) 47 | { 48 | return await _repository.DeleteAsync(id); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Console/Contoso.Pizza.Console.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | a756ae8d-0e97-4705-922d-874b9d5d7264 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Console/Program.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using Contoso.Pizza.Data.Extensions; 3 | using Contoso.Pizza.Data.Models; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Contoso.Pizza.Console 8 | { 9 | internal class Program 10 | { 11 | static async Task Main(string[] args) 12 | { 13 | //load configuration 14 | var configuration = new ConfigurationBuilder() 15 | .AddJsonFile("appsettings.json") 16 | .Build(); 17 | 18 | //setup DI 19 | var serviceProvider = new ServiceCollection() 20 | .AddLogging() 21 | .AddContosoPizzaData(configuration, false) 22 | .BuildServiceProvider(); 23 | 24 | //Get Sauce Repository 25 | var sauceRepository = serviceProvider.GetService()!; 26 | 27 | //Get all sauces 28 | var sauces = await sauceRepository.GetAllAsync(); 29 | if (sauces.Count() == 0) 30 | { 31 | _ = await sauceRepository.AddAsync(new Sauce 32 | { 33 | Name = $"Tomato Sauce {DateTime.Now:yyMMddhhmmss}", 34 | Description = "This is a great sauce", 35 | }); 36 | } 37 | 38 | //Display all sauces 39 | foreach (var sauce in sauces) 40 | { 41 | System.Console.WriteLine($"Sauce Id: {sauce.Id}, Name: {sauce.Name}"); 42 | } 43 | System.Console.WriteLine("Press any key to close..."); 44 | System.Console.ReadKey(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Console/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ContosoPizza": "" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Warning", 8 | "Microsoft": "Warning" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/BaseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Models; 2 | 3 | public abstract class BaseModel 4 | { 5 | public Guid Id { get; set; } 6 | public DateTime Created { get; set; } = DateTime.UtcNow; 7 | public DateTime? Modified { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/Contoso.Pizza.Data.Models.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/Pizza.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Models; 2 | 3 | public class Pizza : BaseModel 4 | { 5 | public required string Name { get; set; } 6 | public string? Description { get; set; } = string.Empty; 7 | public Guid SauceId { get; set; } 8 | public required Sauce Sauce { get; set; } 9 | public List Toppings { get; set; } = new(); 10 | public List PizzaToppings { get; set; } = new(); 11 | } -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/PizzaTopping.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Models; 2 | 3 | public class PizzaTopping : BaseModel 4 | { 5 | public Guid PizzaId { get; set; } 6 | public Guid ToppingId { get; set; } 7 | public Pizza Pizza { get; set; } = null!; 8 | public Topping Topping { get; set; } = null!; 9 | } 10 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/Sauce.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Models; 2 | 3 | public class Sauce : BaseModel 4 | { 5 | public required string Name { get; set; } 6 | public string? Description { get; set; } 7 | public bool IsVegan { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Models/Topping.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Models; 2 | 3 | public class Topping : BaseModel 4 | { 5 | public required string Name { get; set; } 6 | public string Description { get; set; } = string.Empty; 7 | public int Calories { get; set; } 8 | public List Pizzas { get; set; } = new(); 9 | public List PizzaToppings { get; set; } = new(); 10 | } -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Tests/Contoso.Pizza.Data.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data.Tests/SauceRepositoryTest.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using Contoso.Pizza.Data.Models; 3 | using Moq; 4 | using System.Reflection.Metadata; 5 | using Xunit; 6 | 7 | namespace Contoso.Pizza.Data.Tests 8 | { 9 | public class SauceRepositoryTests 10 | { 11 | private readonly Mock _mockRepo; 12 | 13 | public SauceRepositoryTests() 14 | { 15 | _mockRepo = new Mock(); 16 | } 17 | 18 | [Fact] 19 | public async Task GetAll_ReturnsAllSauces() 20 | { 21 | // Arrange 22 | var sauce1Id = Guid.NewGuid(); 23 | var expectedSauces = new List { new Sauce() { Id = sauce1Id, Name = "Sauce1", Created = DateTime.UtcNow } }; 24 | _mockRepo.Setup(repo => repo.GetAllAsync()).ReturnsAsync(expectedSauces); 25 | 26 | // Act 27 | var result = await _mockRepo.Object.GetAllAsync(); 28 | 29 | // Assert 30 | Assert.Equal(expectedSauces, result); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Configutations/PizzaEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using D=Contoso.Pizza.Data.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using Contoso.Pizza.Data.Models; 5 | using Contoso.Pizza.Data.Extensions; 6 | 7 | namespace Contoso.Pizza.Data.Configutations; 8 | 9 | public class PizzaEntityConfiguration : IEntityTypeConfiguration 10 | { 11 | public void Configure(EntityTypeBuilder builder) 12 | { 13 | builder.ToTable("Pizzas"); 14 | 15 | builder.ConfigureBaseEntity(); 16 | 17 | builder.Property(s => s.Name) 18 | .IsRequired() 19 | .HasColumnType("nvarchar(100)"); 20 | 21 | builder.Property(s => s.Description) 22 | .HasColumnType("nvarchar(300)"); 23 | 24 | builder.HasOne(s => s.Sauce) 25 | .WithMany() 26 | .HasForeignKey(s => s.SauceId) 27 | .OnDelete(DeleteBehavior.Restrict); 28 | 29 | builder.HasMany(p => p.Toppings) 30 | .WithMany(t => t.Pizzas) 31 | .UsingEntity( 32 | l => l.HasOne(pt => pt.Topping).WithMany(t => t.PizzaToppings), 33 | r => r.HasOne(pt => pt.Pizza).WithMany(p => p.PizzaToppings) 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Configutations/PizzaToppingEntityConfigration.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Extensions; 2 | using Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace Contoso.Pizza.Data.Configutations; 7 | 8 | public class PizzaToppingEntityConfigration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("PizzaToppings"); 13 | builder.ConfigureBaseEntity(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Configutations/SauceEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Extensions; 2 | using Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace Contoso.Pizza.Data.Configutations; 7 | 8 | public class SauceEntityConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("Sauces"); 13 | 14 | builder.ConfigureBaseEntity(); 15 | 16 | builder.Property(s => s.Name) 17 | .IsRequired() 18 | .HasColumnType("nvarchar(100)"); 19 | 20 | builder.Property(s => s.Description) 21 | .HasColumnType("nvarchar(300)"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Configutations/ToppingEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | using D=Contoso.Pizza.Data.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using Contoso.Pizza.Data.Extensions; 5 | using Contoso.Pizza.Data.Models; 6 | 7 | namespace Contoso.Pizza.Data.Configutations; 8 | 9 | public class ToppingEntityConfiguration : IEntityTypeConfiguration 10 | { 11 | public void Configure(EntityTypeBuilder builder) 12 | { 13 | builder.ToTable("Toppings"); 14 | 15 | builder.ConfigureBaseEntity(); 16 | 17 | builder.Property(s => s.Name) 18 | .IsRequired() 19 | .HasColumnType("nvarchar(100)"); 20 | 21 | builder.Property(s => s.Description) 22 | .HasColumnType("nvarchar(300)"); 23 | 24 | builder.HasMany(p => p.Pizzas) 25 | .WithMany(p => p.Toppings) 26 | .UsingEntity( 27 | l => l.HasOne(pt => pt.Pizza).WithMany(p => p.PizzaToppings), 28 | r => r.HasOne(pt => pt.Topping).WithMany(p => p.PizzaToppings) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Contoso.Pizza.Data.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/ContosoPizzaDataContext.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Configutations; 2 | using Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | using DM = Contoso.Pizza.Data.Models; 5 | namespace Contoso.Pizza.Data; 6 | 7 | public class ContosoPizzaDataContext : DbContext 8 | { 9 | public DbSet Sauces { get; set; } 10 | public DbSet Pizza { get; set; } 11 | public DbSet Toppings { get; set; } 12 | 13 | public ContosoPizzaDataContext(DbContextOptions options) 14 | : base(options) { } 15 | 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | new SauceEntityConfiguration().Configure(modelBuilder.Entity()); 20 | new PizzaEntityConfiguration().Configure(modelBuilder.Entity()); 21 | new ToppingEntityConfiguration().Configure(modelBuilder.Entity()); 22 | new PizzaToppingEntityConfigration().Configure(modelBuilder.Entity()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/ContosoPizzaDataContext.dgml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 8 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 69 | 73 | 77 | 81 | 85 | 89 | 93 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Contracts/IPizzaRepository.cs: -------------------------------------------------------------------------------- 1 | using DM = Contoso.Pizza.Data.Models; 2 | 3 | namespace Contoso.Pizza.Data.Contracts; 4 | 5 | public interface IPizzaRepository : IRepository 6 | { } 7 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Contracts/IRepository.cs: -------------------------------------------------------------------------------- 1 | namespace Contoso.Pizza.Data.Contracts; 2 | 3 | public interface IRepository where T : class 4 | { 5 | Task GetByIdAsync(Guid id); 6 | Task> GetAllAsync(); 7 | Task AddAsync(T entity); 8 | Task UpdateAsync(T entity); 9 | Task DeleteAsync(Guid id); 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Contracts/ISauceRepository.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Models; 2 | 3 | namespace Contoso.Pizza.Data.Contracts; 4 | 5 | public interface ISauceRepository : IRepository 6 | {} 7 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Contracts/IToppingRepository.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Models; 2 | 3 | namespace Contoso.Pizza.Data.Contracts; 4 | 5 | public interface IToppingRepository : IRepository 6 | { } 7 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Extensions/EntityTypeBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Contoso.Pizza.Data.Extensions; 6 | 7 | public static class EntityTypeBuilderExtensions 8 | { 9 | public static EntityTypeBuilder ConfigureBaseEntity(this EntityTypeBuilder builder) 10 | where T : BaseModel 11 | { 12 | builder.HasKey(e => e.Id); 13 | 14 | builder.Property(s => s.Created) 15 | .HasColumnType("datetime2") 16 | .HasDefaultValueSql("getdate()"); 17 | 18 | builder.Property(s => s.Modified) 19 | .HasColumnType("datetime2"); 20 | 21 | return builder; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using Contoso.Pizza.Data.Repositories; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Contoso.Pizza.Data.Extensions; 8 | 9 | public static class ServiceCollectionExtensions 10 | { 11 | public static IServiceCollection AddContosoPizzaData(this IServiceCollection services, 12 | IConfiguration configuration, 13 | bool isProduction = true) 14 | { 15 | var contosoPizzaDbConnectionString = configuration["ConnectionStrings:ContosoPizza"]; 16 | 17 | services.AddDbContext(options => 18 | { 19 | options.UseSqlServer(contosoPizzaDbConnectionString); 20 | //If we are not in production, log to console 21 | if(!isProduction) 22 | { 23 | options.LogTo(Console.WriteLine); 24 | } 25 | }); 26 | services.AddScoped(); 27 | services.AddScoped(); 28 | services.AddScoped(); 29 | 30 | return services; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Initializers/DbInitializer.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Models; 2 | using DM = Contoso.Pizza.Data.Models; 3 | 4 | namespace Contoso.Pizza.Data.Initializers; 5 | 6 | public static class DbInitializer 7 | { 8 | public static void Initialize(ContosoPizzaDataContext context) 9 | { 10 | if(context.Pizza.Any() 11 | && context.Toppings.Any() 12 | && context.Sauces.Any()) 13 | { 14 | return; 15 | } 16 | 17 | var pepperoniTopping = new Topping { Name = "Pepperoni", Calories = 130 }; 18 | var sausageTopping = new Topping { Name = "Sausage", Calories = 100 }; 19 | var hamTopping = new Topping { Name = "Ham", Calories = 70 }; 20 | var chickenTopping = new Topping { Name = "Chicken", Calories = 50 }; 21 | var pineappleTopping = new Topping { Name = "Pineapple", Calories = 75 }; 22 | 23 | var tomatoSauce = new Sauce { Name = "Tomato", IsVegan = true }; 24 | var alfredoSauce = new Sauce { Name = "Alfredo", IsVegan = false }; 25 | 26 | var pizzas = new DM.Pizza[] 27 | { 28 | new DM.Pizza 29 | { 30 | Name = "Meat Lovers", 31 | Sauce = tomatoSauce, 32 | Toppings = new List 33 | { 34 | pepperoniTopping, 35 | sausageTopping, 36 | hamTopping, 37 | chickenTopping 38 | } 39 | }, 40 | new DM.Pizza 41 | { 42 | Name = "Hawaiian", 43 | Sauce = tomatoSauce, 44 | Toppings = new List 45 | { 46 | pineappleTopping, 47 | hamTopping 48 | } 49 | }, 50 | new DM.Pizza 51 | { 52 | Name="Alfredo Chicken", 53 | Sauce = alfredoSauce, 54 | Toppings = new List 55 | { 56 | chickenTopping 57 | } 58 | } 59 | }; 60 | 61 | context.Pizza.AddRange(pizzas); 62 | context.SaveChanges(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Migrations/20240127141019_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Contoso.Pizza.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | #nullable disable 11 | 12 | namespace Contoso.Pizza.Data.Migrations 13 | { 14 | [DbContext(typeof(ContosoPizzaDataContext))] 15 | [Migration("20240127141019_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "8.0.1") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 25 | 26 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 29 | { 30 | b.Property("Id") 31 | .ValueGeneratedOnAdd() 32 | .HasColumnType("uniqueidentifier"); 33 | 34 | b.Property("Created") 35 | .ValueGeneratedOnAdd() 36 | .HasColumnType("datetime2") 37 | .HasDefaultValueSql("getdate()"); 38 | 39 | b.Property("Description") 40 | .HasColumnType("nvarchar(300)"); 41 | 42 | b.Property("Modified") 43 | .HasColumnType("datetime2"); 44 | 45 | b.Property("Name") 46 | .IsRequired() 47 | .HasColumnType("nvarchar(100)"); 48 | 49 | b.Property("SauceId") 50 | .HasColumnType("uniqueidentifier"); 51 | 52 | b.HasKey("Id"); 53 | 54 | b.HasIndex("SauceId"); 55 | 56 | b.ToTable("Pizzas", (string)null); 57 | }); 58 | 59 | modelBuilder.Entity("Contoso.Pizza.Data.Models.PizzaTopping", b => 60 | { 61 | b.Property("Id") 62 | .ValueGeneratedOnAdd() 63 | .HasColumnType("uniqueidentifier"); 64 | 65 | b.Property("Created") 66 | .ValueGeneratedOnAdd() 67 | .HasColumnType("datetime2") 68 | .HasDefaultValueSql("getdate()"); 69 | 70 | b.Property("Modified") 71 | .HasColumnType("datetime2"); 72 | 73 | b.Property("PizzaId") 74 | .HasColumnType("uniqueidentifier"); 75 | 76 | b.Property("ToppingId") 77 | .HasColumnType("uniqueidentifier"); 78 | 79 | b.HasKey("Id"); 80 | 81 | b.HasIndex("PizzaId"); 82 | 83 | b.HasIndex("ToppingId"); 84 | 85 | b.ToTable("PizzaToppings", (string)null); 86 | }); 87 | 88 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Sauce", b => 89 | { 90 | b.Property("Id") 91 | .ValueGeneratedOnAdd() 92 | .HasColumnType("uniqueidentifier"); 93 | 94 | b.Property("Created") 95 | .ValueGeneratedOnAdd() 96 | .HasColumnType("datetime2") 97 | .HasDefaultValueSql("getdate()"); 98 | 99 | b.Property("Description") 100 | .HasColumnType("nvarchar(300)"); 101 | 102 | b.Property("IsVegan") 103 | .HasColumnType("bit"); 104 | 105 | b.Property("Modified") 106 | .HasColumnType("datetime2"); 107 | 108 | b.Property("Name") 109 | .IsRequired() 110 | .HasColumnType("nvarchar(100)"); 111 | 112 | b.HasKey("Id"); 113 | 114 | b.ToTable("Sauces", (string)null); 115 | }); 116 | 117 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Topping", b => 118 | { 119 | b.Property("Id") 120 | .ValueGeneratedOnAdd() 121 | .HasColumnType("uniqueidentifier"); 122 | 123 | b.Property("Calories") 124 | .HasColumnType("int"); 125 | 126 | b.Property("Created") 127 | .ValueGeneratedOnAdd() 128 | .HasColumnType("datetime2") 129 | .HasDefaultValueSql("getdate()"); 130 | 131 | b.Property("Description") 132 | .IsRequired() 133 | .HasColumnType("nvarchar(300)"); 134 | 135 | b.Property("Modified") 136 | .HasColumnType("datetime2"); 137 | 138 | b.Property("Name") 139 | .IsRequired() 140 | .HasColumnType("nvarchar(100)"); 141 | 142 | b.HasKey("Id"); 143 | 144 | b.ToTable("Toppings", (string)null); 145 | }); 146 | 147 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 148 | { 149 | b.HasOne("Contoso.Pizza.Data.Models.Sauce", "Sauce") 150 | .WithMany() 151 | .HasForeignKey("SauceId") 152 | .OnDelete(DeleteBehavior.Restrict) 153 | .IsRequired(); 154 | 155 | b.Navigation("Sauce"); 156 | }); 157 | 158 | modelBuilder.Entity("Contoso.Pizza.Data.Models.PizzaTopping", b => 159 | { 160 | b.HasOne("Contoso.Pizza.Data.Models.Pizza", "Pizza") 161 | .WithMany("PizzaToppings") 162 | .HasForeignKey("PizzaId") 163 | .OnDelete(DeleteBehavior.Cascade) 164 | .IsRequired(); 165 | 166 | b.HasOne("Contoso.Pizza.Data.Models.Topping", "Topping") 167 | .WithMany("PizzaToppings") 168 | .HasForeignKey("ToppingId") 169 | .OnDelete(DeleteBehavior.Cascade) 170 | .IsRequired(); 171 | 172 | b.Navigation("Pizza"); 173 | 174 | b.Navigation("Topping"); 175 | }); 176 | 177 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 178 | { 179 | b.Navigation("PizzaToppings"); 180 | }); 181 | 182 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Topping", b => 183 | { 184 | b.Navigation("PizzaToppings"); 185 | }); 186 | #pragma warning restore 612, 618 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Migrations/20240127141019_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace Contoso.Pizza.Data.Migrations 7 | { 8 | /// 9 | public partial class Initial : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "Sauces", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uniqueidentifier", nullable: false), 19 | Name = table.Column(type: "nvarchar(100)", nullable: false), 20 | Description = table.Column(type: "nvarchar(300)", nullable: true), 21 | IsVegan = table.Column(type: "bit", nullable: false), 22 | Created = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), 23 | Modified = table.Column(type: "datetime2", nullable: true) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_Sauces", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "Toppings", 32 | columns: table => new 33 | { 34 | Id = table.Column(type: "uniqueidentifier", nullable: false), 35 | Name = table.Column(type: "nvarchar(100)", nullable: false), 36 | Description = table.Column(type: "nvarchar(300)", nullable: false), 37 | Calories = table.Column(type: "int", nullable: false), 38 | Created = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), 39 | Modified = table.Column(type: "datetime2", nullable: true) 40 | }, 41 | constraints: table => 42 | { 43 | table.PrimaryKey("PK_Toppings", x => x.Id); 44 | }); 45 | 46 | migrationBuilder.CreateTable( 47 | name: "Pizzas", 48 | columns: table => new 49 | { 50 | Id = table.Column(type: "uniqueidentifier", nullable: false), 51 | Name = table.Column(type: "nvarchar(100)", nullable: false), 52 | Description = table.Column(type: "nvarchar(300)", nullable: true), 53 | SauceId = table.Column(type: "uniqueidentifier", nullable: false), 54 | Created = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), 55 | Modified = table.Column(type: "datetime2", nullable: true) 56 | }, 57 | constraints: table => 58 | { 59 | table.PrimaryKey("PK_Pizzas", x => x.Id); 60 | table.ForeignKey( 61 | name: "FK_Pizzas_Sauces_SauceId", 62 | column: x => x.SauceId, 63 | principalTable: "Sauces", 64 | principalColumn: "Id", 65 | onDelete: ReferentialAction.Restrict); 66 | }); 67 | 68 | migrationBuilder.CreateTable( 69 | name: "PizzaToppings", 70 | columns: table => new 71 | { 72 | Id = table.Column(type: "uniqueidentifier", nullable: false), 73 | PizzaId = table.Column(type: "uniqueidentifier", nullable: false), 74 | ToppingId = table.Column(type: "uniqueidentifier", nullable: false), 75 | Created = table.Column(type: "datetime2", nullable: false, defaultValueSql: "getdate()"), 76 | Modified = table.Column(type: "datetime2", nullable: true) 77 | }, 78 | constraints: table => 79 | { 80 | table.PrimaryKey("PK_PizzaToppings", x => x.Id); 81 | table.ForeignKey( 82 | name: "FK_PizzaToppings_Pizzas_PizzaId", 83 | column: x => x.PizzaId, 84 | principalTable: "Pizzas", 85 | principalColumn: "Id", 86 | onDelete: ReferentialAction.Cascade); 87 | table.ForeignKey( 88 | name: "FK_PizzaToppings_Toppings_ToppingId", 89 | column: x => x.ToppingId, 90 | principalTable: "Toppings", 91 | principalColumn: "Id", 92 | onDelete: ReferentialAction.Cascade); 93 | }); 94 | 95 | migrationBuilder.CreateIndex( 96 | name: "IX_Pizzas_SauceId", 97 | table: "Pizzas", 98 | column: "SauceId"); 99 | 100 | migrationBuilder.CreateIndex( 101 | name: "IX_PizzaToppings_PizzaId", 102 | table: "PizzaToppings", 103 | column: "PizzaId"); 104 | 105 | migrationBuilder.CreateIndex( 106 | name: "IX_PizzaToppings_ToppingId", 107 | table: "PizzaToppings", 108 | column: "ToppingId"); 109 | } 110 | 111 | /// 112 | protected override void Down(MigrationBuilder migrationBuilder) 113 | { 114 | migrationBuilder.DropTable( 115 | name: "PizzaToppings"); 116 | 117 | migrationBuilder.DropTable( 118 | name: "Pizzas"); 119 | 120 | migrationBuilder.DropTable( 121 | name: "Toppings"); 122 | 123 | migrationBuilder.DropTable( 124 | name: "Sauces"); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Migrations/ContosoPizzaDataContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Contoso.Pizza.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace Contoso.Pizza.Data.Migrations 12 | { 13 | [DbContext(typeof(ContosoPizzaDataContext))] 14 | partial class ContosoPizzaDataContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "8.0.1") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 22 | 23 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnType("uniqueidentifier"); 30 | 31 | b.Property("Created") 32 | .ValueGeneratedOnAdd() 33 | .HasColumnType("datetime2") 34 | .HasDefaultValueSql("getdate()"); 35 | 36 | b.Property("Description") 37 | .HasColumnType("nvarchar(300)"); 38 | 39 | b.Property("Modified") 40 | .HasColumnType("datetime2"); 41 | 42 | b.Property("Name") 43 | .IsRequired() 44 | .HasColumnType("nvarchar(100)"); 45 | 46 | b.Property("SauceId") 47 | .HasColumnType("uniqueidentifier"); 48 | 49 | b.HasKey("Id"); 50 | 51 | b.HasIndex("SauceId"); 52 | 53 | b.ToTable("Pizzas", (string)null); 54 | }); 55 | 56 | modelBuilder.Entity("Contoso.Pizza.Data.Models.PizzaTopping", b => 57 | { 58 | b.Property("Id") 59 | .ValueGeneratedOnAdd() 60 | .HasColumnType("uniqueidentifier"); 61 | 62 | b.Property("Created") 63 | .ValueGeneratedOnAdd() 64 | .HasColumnType("datetime2") 65 | .HasDefaultValueSql("getdate()"); 66 | 67 | b.Property("Modified") 68 | .HasColumnType("datetime2"); 69 | 70 | b.Property("PizzaId") 71 | .HasColumnType("uniqueidentifier"); 72 | 73 | b.Property("ToppingId") 74 | .HasColumnType("uniqueidentifier"); 75 | 76 | b.HasKey("Id"); 77 | 78 | b.HasIndex("PizzaId"); 79 | 80 | b.HasIndex("ToppingId"); 81 | 82 | b.ToTable("PizzaToppings", (string)null); 83 | }); 84 | 85 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Sauce", b => 86 | { 87 | b.Property("Id") 88 | .ValueGeneratedOnAdd() 89 | .HasColumnType("uniqueidentifier"); 90 | 91 | b.Property("Created") 92 | .ValueGeneratedOnAdd() 93 | .HasColumnType("datetime2") 94 | .HasDefaultValueSql("getdate()"); 95 | 96 | b.Property("Description") 97 | .HasColumnType("nvarchar(300)"); 98 | 99 | b.Property("IsVegan") 100 | .HasColumnType("bit"); 101 | 102 | b.Property("Modified") 103 | .HasColumnType("datetime2"); 104 | 105 | b.Property("Name") 106 | .IsRequired() 107 | .HasColumnType("nvarchar(100)"); 108 | 109 | b.HasKey("Id"); 110 | 111 | b.ToTable("Sauces", (string)null); 112 | }); 113 | 114 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Topping", b => 115 | { 116 | b.Property("Id") 117 | .ValueGeneratedOnAdd() 118 | .HasColumnType("uniqueidentifier"); 119 | 120 | b.Property("Calories") 121 | .HasColumnType("int"); 122 | 123 | b.Property("Created") 124 | .ValueGeneratedOnAdd() 125 | .HasColumnType("datetime2") 126 | .HasDefaultValueSql("getdate()"); 127 | 128 | b.Property("Description") 129 | .IsRequired() 130 | .HasColumnType("nvarchar(300)"); 131 | 132 | b.Property("Modified") 133 | .HasColumnType("datetime2"); 134 | 135 | b.Property("Name") 136 | .IsRequired() 137 | .HasColumnType("nvarchar(100)"); 138 | 139 | b.HasKey("Id"); 140 | 141 | b.ToTable("Toppings", (string)null); 142 | }); 143 | 144 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 145 | { 146 | b.HasOne("Contoso.Pizza.Data.Models.Sauce", "Sauce") 147 | .WithMany() 148 | .HasForeignKey("SauceId") 149 | .OnDelete(DeleteBehavior.Restrict) 150 | .IsRequired(); 151 | 152 | b.Navigation("Sauce"); 153 | }); 154 | 155 | modelBuilder.Entity("Contoso.Pizza.Data.Models.PizzaTopping", b => 156 | { 157 | b.HasOne("Contoso.Pizza.Data.Models.Pizza", "Pizza") 158 | .WithMany("PizzaToppings") 159 | .HasForeignKey("PizzaId") 160 | .OnDelete(DeleteBehavior.Cascade) 161 | .IsRequired(); 162 | 163 | b.HasOne("Contoso.Pizza.Data.Models.Topping", "Topping") 164 | .WithMany("PizzaToppings") 165 | .HasForeignKey("ToppingId") 166 | .OnDelete(DeleteBehavior.Cascade) 167 | .IsRequired(); 168 | 169 | b.Navigation("Pizza"); 170 | 171 | b.Navigation("Topping"); 172 | }); 173 | 174 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Pizza", b => 175 | { 176 | b.Navigation("PizzaToppings"); 177 | }); 178 | 179 | modelBuilder.Entity("Contoso.Pizza.Data.Models.Topping", b => 180 | { 181 | b.Navigation("PizzaToppings"); 182 | }); 183 | #pragma warning restore 612, 618 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Repositories/PizzaRepository.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using DM = Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | using Contoso.Pizza.Data.Models; 5 | 6 | namespace Contoso.Pizza.Data.Repositories; 7 | 8 | public class PizzaRepository : IPizzaRepository 9 | { 10 | private readonly ContosoPizzaDataContext _dbContext; 11 | 12 | public PizzaRepository(ContosoPizzaDataContext context) 13 | { 14 | _dbContext = context; 15 | } 16 | 17 | public async Task> GetAllAsync() 18 | { 19 | return await _dbContext.Pizza 20 | .Include(p => p.Sauce) 21 | .Include(p => p.Toppings) 22 | .OrderByDescending(t => t.Modified) 23 | .ThenBy(t => t.Created) 24 | .AsNoTracking() 25 | .ToListAsync(); 26 | } 27 | 28 | public async Task GetByIdAsync(Guid id) 29 | { 30 | return await _dbContext.Pizza 31 | .Include(p => p.Sauce) 32 | .Include(p => p.Toppings) 33 | .AsNoTracking() 34 | .FirstOrDefaultAsync(p => p.Id == id); 35 | } 36 | 37 | public async Task AddAsync(DM.Pizza pizza) 38 | { 39 | pizza.Id = Guid.NewGuid(); 40 | 41 | _dbContext.Entry(pizza.Sauce).State = EntityState.Unchanged; 42 | 43 | // Load the toppings from the database 44 | foreach (var topping in pizza.Toppings) 45 | { 46 | _dbContext.Entry(topping).State = EntityState.Unchanged; 47 | } 48 | 49 | await _dbContext.Pizza.AddAsync(pizza); 50 | await _dbContext.SaveChangesAsync(); 51 | 52 | return pizza; 53 | } 54 | 55 | public async Task UpdateAsync(DM.Pizza pizza) 56 | { 57 | var storedPizza = await _dbContext.Pizza 58 | .Include(p => p.Toppings) 59 | .Where(t => t.Id == pizza.Id) 60 | .FirstOrDefaultAsync(); 61 | 62 | if (storedPizza == null) 63 | { 64 | return -1; 65 | } 66 | storedPizza.Name = pizza.Name; 67 | storedPizza.Description = pizza.Description; 68 | storedPizza.SauceId = pizza.SauceId; 69 | storedPizza.Modified = DateTime.UtcNow; 70 | 71 | storedPizza.Toppings.RemoveAll(topping => !pizza.Toppings.Any(p => p.Id == topping.Id)); 72 | 73 | foreach (var topping in pizza.Toppings) 74 | { 75 | if (!storedPizza.Toppings.Any(p => p.Id == topping.Id)) 76 | { 77 | storedPizza.Toppings.Add(topping); 78 | } 79 | } 80 | 81 | var result = await _dbContext.SaveChangesAsync(); 82 | 83 | return result; 84 | } 85 | 86 | public async Task DeleteAsync(Guid id) 87 | { 88 | var result = await _dbContext.Pizza 89 | .Where(t => t.Id == id) 90 | .ExecuteDeleteAsync(); 91 | return result; 92 | } 93 | } -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Repositories/SauceRepository.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Contoso.Pizza.Data.Repositories; 6 | 7 | public class SauceRepository : ISauceRepository 8 | { 9 | private readonly ContosoPizzaDataContext _dbContext; 10 | 11 | public SauceRepository(ContosoPizzaDataContext context) 12 | { 13 | _dbContext = context; 14 | } 15 | 16 | public async Task> GetAllAsync() 17 | { 18 | return await _dbContext.Sauces.OrderByDescending(s => s.Modified) 19 | .ThenBy(s => s.Created) 20 | .AsNoTracking() 21 | .ToListAsync(); 22 | } 23 | 24 | public async Task GetByIdAsync(Guid id) 25 | { 26 | return await _dbContext.Sauces.FindAsync(id); 27 | } 28 | 29 | public async Task AddAsync(Sauce sauce) 30 | { 31 | sauce.Id = Guid.NewGuid(); 32 | await _dbContext.Sauces.AddAsync(sauce); 33 | await _dbContext.SaveChangesAsync(); 34 | return sauce; 35 | } 36 | 37 | public async Task UpdateAsync(Sauce sauce) 38 | { 39 | var result = await _dbContext.Sauces.Where(s => s.Id == sauce.Id) 40 | .ExecuteUpdateAsync(setter => setter 41 | .SetProperty(s => s.Name, sauce.Name) 42 | .SetProperty(s => s.Description, sauce.Description) 43 | .SetProperty(s => s.IsVegan, sauce.IsVegan) 44 | .SetProperty(s => s.Modified, DateTime.UtcNow) 45 | ); 46 | return result; 47 | } 48 | 49 | 50 | public async Task DeleteAsync(Guid id) 51 | { 52 | var childCount = await _dbContext.Pizza 53 | .Where(p => p.SauceId == id) 54 | .CountAsync(); 55 | if (childCount > 0) 56 | { 57 | throw new InvalidOperationException("Sauce with ID " + id + " cannot be deleted because it is currently associated with a pizza."); 58 | } 59 | return await _dbContext.Sauces.Where(s => s.Id == id).ExecuteDeleteAsync(); 60 | } 61 | } -------------------------------------------------------------------------------- /src/Backend/Contoso.Pizza.Data/Repositories/ToppingRepository.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.Data.Contracts; 2 | using Contoso.Pizza.Data.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Contoso.Pizza.Data.Repositories; 6 | 7 | public class ToppingRepository : IToppingRepository 8 | { 9 | private readonly ContosoPizzaDataContext _dbContext; 10 | 11 | public ToppingRepository(ContosoPizzaDataContext context) 12 | { 13 | _dbContext = context; 14 | } 15 | 16 | public async Task> GetAllAsync() 17 | { 18 | return await _dbContext.Toppings 19 | .OrderByDescending(t => t.Modified) 20 | .ThenBy(t => t.Created) 21 | .AsNoTracking() 22 | .ToListAsync(); 23 | } 24 | 25 | public async Task GetByIdAsync(Guid id) 26 | { 27 | return await _dbContext.Toppings.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id); 28 | } 29 | 30 | public async Task AddAsync(Topping topping) 31 | { 32 | topping.Id = Guid.NewGuid(); 33 | await _dbContext.Toppings.AddAsync(topping); 34 | await _dbContext.SaveChangesAsync(); 35 | return topping; 36 | } 37 | 38 | public async Task UpdateAsync(Topping topping) 39 | { 40 | var result = await _dbContext.Toppings.Where(t => t.Id == topping.Id) 41 | .ExecuteUpdateAsync(setter => setter 42 | .SetProperty(t => t.Name, topping.Name) 43 | .SetProperty(t => t.Description, topping.Description) 44 | .SetProperty(t => t.Calories, topping.Calories) 45 | .SetProperty(t => t.Modified, DateTime.UtcNow) 46 | ); 47 | return result; 48 | } 49 | 50 | public async Task DeleteAsync(Guid id) 51 | { 52 | var childCount = await _dbContext.Toppings 53 | .Include(t => t.PizzaToppings) 54 | .Where(t => t.Id == id) 55 | .CountAsync(t => t.PizzaToppings.Any()); 56 | if (childCount > 0) 57 | { 58 | throw new InvalidOperationException("Topping with ID " + id + " cannot be deleted because it is currently associated with a pizza."); 59 | } 60 | var result = await _dbContext.Toppings.Where(t => t.Id == id).ExecuteDeleteAsync(); 61 | return result; 62 | } 63 | } -------------------------------------------------------------------------------- /src/Contoso.Pizza.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.9.34511.98 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminApi.Minimal", "Backend\Contoso.Pizza.AdminApi.Minimal\Contoso.Pizza.AdminApi.Minimal.csproj", "{2C9ED30D-AB76-4D52-8865-99D4C8B7799C}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Backend", "Backend", "{03225E75-120E-4ECE-9712-AB359A511BEF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminApi.MVC", "Backend\Contoso.Pizza.AdminApi.MVC\Contoso.Pizza.AdminApi.MVC.csproj", "{6F668A78-B315-4E05-A96C-10FCCC192D4F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminApi.Services", "Backend\Contoso.Pizza.AdminApi.Services\Contoso.Pizza.AdminApi.Services.csproj", "{8F30D3E3-65C3-490B-B6B9-12274275AE43}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.Console", "Backend\Contoso.Pizza.Console\Contoso.Pizza.Console.csproj", "{08B5D48E-2C76-45CE-8834-5C7B2EDC2B84}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.Data", "Backend\Contoso.Pizza.Data\Contoso.Pizza.Data.csproj", "{40250D70-AAB1-4599-8EE7-18EBFB0083AF}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.Data.Models", "Backend\Contoso.Pizza.Data.Models\Contoso.Pizza.Data.Models.csproj", "{7F04A5D8-0F6E-4047-9C13-54DDF7356376}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frontend", "Frontend", "{28D8C81F-8FC5-4FCC-A786-830818DFD80F}" 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminUI", "Frontend\Contoso.Pizza.AdminUI\Contoso.Pizza.AdminUI.csproj", "{EC757511-4EF8-4246-8F2B-97C39432B0DE}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminApi.Models", "Backend\Contoso.Pizza.AdminApi.Models\Contoso.Pizza.AdminApi.Models.csproj", "{4AA2F32E-65AC-4B18-A865-FA504BC7FDA5}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Api", "Api", "{617DDB77-9F87-49B3-8C61-48D78625D96F}" 27 | EndProject 28 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{4E61F8A1-61B7-4228-8B96-EEAAD0ACF97E}" 29 | EndProject 30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Contoso.Pizza.AdminUI.Services", "Frontend\Contoso.Pizza.AdminUI.Services\Contoso.Pizza.AdminUI.Services.csproj", "{6429C3EC-75B5-48B8-8570-C9B968404324}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {2C9ED30D-AB76-4D52-8865-99D4C8B7799C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {2C9ED30D-AB76-4D52-8865-99D4C8B7799C}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {2C9ED30D-AB76-4D52-8865-99D4C8B7799C}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {2C9ED30D-AB76-4D52-8865-99D4C8B7799C}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {6F668A78-B315-4E05-A96C-10FCCC192D4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {6F668A78-B315-4E05-A96C-10FCCC192D4F}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {6F668A78-B315-4E05-A96C-10FCCC192D4F}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {6F668A78-B315-4E05-A96C-10FCCC192D4F}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {8F30D3E3-65C3-490B-B6B9-12274275AE43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {8F30D3E3-65C3-490B-B6B9-12274275AE43}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {8F30D3E3-65C3-490B-B6B9-12274275AE43}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {8F30D3E3-65C3-490B-B6B9-12274275AE43}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {08B5D48E-2C76-45CE-8834-5C7B2EDC2B84}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {08B5D48E-2C76-45CE-8834-5C7B2EDC2B84}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {08B5D48E-2C76-45CE-8834-5C7B2EDC2B84}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {08B5D48E-2C76-45CE-8834-5C7B2EDC2B84}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {40250D70-AAB1-4599-8EE7-18EBFB0083AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {40250D70-AAB1-4599-8EE7-18EBFB0083AF}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {40250D70-AAB1-4599-8EE7-18EBFB0083AF}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {40250D70-AAB1-4599-8EE7-18EBFB0083AF}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {7F04A5D8-0F6E-4047-9C13-54DDF7356376}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {7F04A5D8-0F6E-4047-9C13-54DDF7356376}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {7F04A5D8-0F6E-4047-9C13-54DDF7356376}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {7F04A5D8-0F6E-4047-9C13-54DDF7356376}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {EC757511-4EF8-4246-8F2B-97C39432B0DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {EC757511-4EF8-4246-8F2B-97C39432B0DE}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {EC757511-4EF8-4246-8F2B-97C39432B0DE}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {EC757511-4EF8-4246-8F2B-97C39432B0DE}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {4AA2F32E-65AC-4B18-A865-FA504BC7FDA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {4AA2F32E-65AC-4B18-A865-FA504BC7FDA5}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {4AA2F32E-65AC-4B18-A865-FA504BC7FDA5}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {4AA2F32E-65AC-4B18-A865-FA504BC7FDA5}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {6429C3EC-75B5-48B8-8570-C9B968404324}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 71 | {6429C3EC-75B5-48B8-8570-C9B968404324}.Debug|Any CPU.Build.0 = Debug|Any CPU 72 | {6429C3EC-75B5-48B8-8570-C9B968404324}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {6429C3EC-75B5-48B8-8570-C9B968404324}.Release|Any CPU.Build.0 = Release|Any CPU 74 | EndGlobalSection 75 | GlobalSection(SolutionProperties) = preSolution 76 | HideSolutionNode = FALSE 77 | EndGlobalSection 78 | GlobalSection(NestedProjects) = preSolution 79 | {2C9ED30D-AB76-4D52-8865-99D4C8B7799C} = {617DDB77-9F87-49B3-8C61-48D78625D96F} 80 | {6F668A78-B315-4E05-A96C-10FCCC192D4F} = {617DDB77-9F87-49B3-8C61-48D78625D96F} 81 | {8F30D3E3-65C3-490B-B6B9-12274275AE43} = {617DDB77-9F87-49B3-8C61-48D78625D96F} 82 | {08B5D48E-2C76-45CE-8834-5C7B2EDC2B84} = {4E61F8A1-61B7-4228-8B96-EEAAD0ACF97E} 83 | {40250D70-AAB1-4599-8EE7-18EBFB0083AF} = {4E61F8A1-61B7-4228-8B96-EEAAD0ACF97E} 84 | {7F04A5D8-0F6E-4047-9C13-54DDF7356376} = {4E61F8A1-61B7-4228-8B96-EEAAD0ACF97E} 85 | {EC757511-4EF8-4246-8F2B-97C39432B0DE} = {28D8C81F-8FC5-4FCC-A786-830818DFD80F} 86 | {4AA2F32E-65AC-4B18-A865-FA504BC7FDA5} = {617DDB77-9F87-49B3-8C61-48D78625D96F} 87 | {617DDB77-9F87-49B3-8C61-48D78625D96F} = {03225E75-120E-4ECE-9712-AB359A511BEF} 88 | {4E61F8A1-61B7-4228-8B96-EEAAD0ACF97E} = {03225E75-120E-4ECE-9712-AB359A511BEF} 89 | {6429C3EC-75B5-48B8-8570-C9B968404324} = {28D8C81F-8FC5-4FCC-A786-830818DFD80F} 90 | EndGlobalSection 91 | GlobalSection(ExtensibilityGlobals) = postSolution 92 | SolutionGuid = {41FBC7CD-53B2-4C0F-ADAC-34EFEE49A663} 93 | EndGlobalSection 94 | EndGlobal 95 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/Contoso.Pizza.AdminUI.Services.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/Contracts/IPizzaService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminUI.Services.Contracts; 4 | 5 | public interface IPizzaService 6 | { 7 | Task> GetAllPizzasAsync(); 8 | Task AddPizzaAsync(PizzaEntity entity); 9 | Task UpdatePizzaAsync(PizzaEntity entity); 10 | Task DeletePizzaAsync(PizzaEntity entity); 11 | } 12 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/Contracts/ISauceService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminUI.Services.Contracts; 4 | 5 | public interface ISauceService 6 | { 7 | Task> GetAllSaucesAsync(); 8 | Task AddSauceAsync(SauceEntity entity); 9 | Task UpdateSauceAsync(SauceEntity entity); 10 | Task DeleteSauceAsync(SauceEntity entity); 11 | } 12 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/Contracts/IToppingService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | 3 | namespace Contoso.Pizza.AdminUI.Services.Contracts; 4 | 5 | public interface IToppingService 6 | { 7 | Task> GetAllToppingsAsync(); 8 | Task AddToppingAsync(ToppingEntity entity); 9 | Task UpdateToppingAsync(ToppingEntity entity); 10 | Task DeleteToppingAsync(ToppingEntity entity); 11 | } 12 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminUI.Services.Contracts; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Contoso.Pizza.AdminUI.Services.Extensions; 6 | 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static IServiceCollection AddAdminUIServices(this IServiceCollection services, IConfiguration configuration) 10 | { 11 | var apiUrl = configuration["Api:Url"]!; 12 | services.AddHttpClient("AdminApi",client => 13 | { 14 | client.BaseAddress = new Uri(apiUrl, UriKind.Absolute); 15 | }); 16 | services.AddScoped(); 17 | services.AddScoped(); 18 | services.AddScoped(); 19 | return services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/PizzaService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using System.Net.Http.Json; 4 | 5 | namespace Contoso.Pizza.AdminUI.Services; 6 | 7 | public class PizzaService : IPizzaService 8 | { 9 | private readonly HttpClient _httpClient; 10 | private const string _baseUri = "/api/pizzas"; 11 | 12 | public PizzaService(IHttpClientFactory httpClientFactory) 13 | { 14 | _httpClient = httpClientFactory.CreateClient("AdminApi"); 15 | } 16 | 17 | public async Task AddPizzaAsync(PizzaEntity entity) 18 | { 19 | var response = await _httpClient.PostAsJsonAsync(_baseUri, entity); 20 | response.EnsureSuccessStatusCode(); 21 | return (await response.Content.ReadFromJsonAsync())!; 22 | } 23 | 24 | public async Task DeletePizzaAsync(PizzaEntity entity) 25 | { 26 | var deleteUri = $"{_baseUri}/{entity.Id}"; 27 | var response = await _httpClient.DeleteAsync(deleteUri); 28 | response.EnsureSuccessStatusCode(); 29 | } 30 | 31 | public async Task> GetAllPizzasAsync() 32 | { 33 | return await _httpClient.GetFromJsonAsync>(_baseUri) ?? Array.Empty(); 34 | } 35 | 36 | public async Task UpdatePizzaAsync(PizzaEntity entity) 37 | { 38 | var updateUri = $"{_baseUri}/{entity.Id}"; 39 | var response = await _httpClient.PutAsJsonAsync(updateUri, entity); 40 | response.EnsureSuccessStatusCode(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/SauceService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using System.Net.Http.Json; 4 | 5 | namespace Contoso.Pizza.AdminUI.Services; 6 | 7 | public class SauceService : ISauceService 8 | { 9 | private readonly HttpClient _httpClient; 10 | private const string _baseUri = "/api/sauces"; 11 | 12 | public SauceService(IHttpClientFactory clientFactory) 13 | { 14 | _httpClient = clientFactory.CreateClient("AdminApi"); 15 | } 16 | 17 | public async Task AddSauceAsync(SauceEntity entity) 18 | { 19 | var response = await _httpClient.PostAsJsonAsync(_baseUri, entity); 20 | response.EnsureSuccessStatusCode(); 21 | return (await response.Content.ReadFromJsonAsync())!; 22 | } 23 | 24 | public async Task DeleteSauceAsync(SauceEntity entity) 25 | { 26 | var deleteUri = $"{_baseUri}/{entity.Id}"; 27 | var response = await _httpClient.DeleteAsync(deleteUri); 28 | response.EnsureSuccessStatusCode(); 29 | } 30 | 31 | public async Task> GetAllSaucesAsync() 32 | { 33 | return await _httpClient.GetFromJsonAsync>(_baseUri) ?? Array.Empty(); 34 | } 35 | 36 | public async Task UpdateSauceAsync(SauceEntity entity) 37 | { 38 | var updateUri = $"{_baseUri}/{entity.Id}"; 39 | var response = await _httpClient.PutAsJsonAsync(updateUri, entity); 40 | response.EnsureSuccessStatusCode(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI.Services/ToppingService.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using System.Net.Http.Json; 4 | 5 | namespace Contoso.Pizza.AdminUI.Services; 6 | 7 | public class ToppingService : IToppingService 8 | { 9 | private readonly HttpClient _httpClient; 10 | private const string _baseUri = "/api/toppings"; 11 | 12 | public ToppingService(IHttpClientFactory httpClientFactory) 13 | { 14 | _httpClient = httpClientFactory.CreateClient("AdminApi"); 15 | } 16 | 17 | public async Task AddToppingAsync(ToppingEntity entity) 18 | { 19 | var response = await _httpClient.PostAsJsonAsync(_baseUri, entity); 20 | response.EnsureSuccessStatusCode(); 21 | return (await response.Content.ReadFromJsonAsync())!; 22 | } 23 | 24 | public async Task DeleteToppingAsync(ToppingEntity entity) 25 | { 26 | var deleteUri = $"{_baseUri}/{entity.Id}"; 27 | var response = await _httpClient.DeleteAsync(deleteUri); 28 | response.EnsureSuccessStatusCode(); 29 | } 30 | 31 | public async Task> GetAllToppingsAsync() 32 | { 33 | return await _httpClient.GetFromJsonAsync>(_baseUri) ?? Array.Empty(); 34 | } 35 | 36 | public async Task UpdateToppingAsync(ToppingEntity entity) 37 | { 38 | var updateUri = $"{_baseUri}/{entity.Id}"; 39 | var response = await _httpClient.PutAsJsonAsync(updateUri, entity); 40 | response.EnsureSuccessStatusCode(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | @* *@ 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | Contoso Pizzas AdminUI 5 | 6 | 7 | 8 | 9 |
10 | @Body 11 |
12 |
13 |
14 | 15 | 18 |
19 | About Blazor 20 |
21 |
22 |
23 | 24 | 25 |
26 | An unhandled error has occurred. 27 | Reload 28 | 🗙 29 |
30 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @* To get access to all Fluent UI System icons, install the Microsoft.FluentUI.AspNetCore.Compoents.Icon package *@ 2 | @* See all the diffeent ways on how to use icons at https://www.fluentui-blazor.net/Icon *@ 3 | 4 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .fluent-nav-icon { 2 | padding-top: 5px; 3 | } 4 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/BasePage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.FluentUI.AspNetCore.Components; 3 | 4 | namespace Contoso.Pizza.AdminUI.Components.Pages; 5 | 6 | public class BasePage : ComponentBase 7 | { 8 | [Inject] 9 | public required IDialogService DialogService { get; set; } 10 | 11 | [Inject] 12 | public required IToastService ToastService { get; set; } 13 | 14 | protected enum Operation 15 | { 16 | Add, 17 | Update, 18 | Delete 19 | }; 20 | 21 | protected async Task ShowConfirmationDialogAsync(string title, string message) 22 | { 23 | var dialog = await DialogService.ShowMessageBoxAsync(new DialogParameters() 24 | { 25 | Content = new() 26 | { 27 | Title = title, 28 | MarkupMessage = new MarkupString(message), 29 | Icon = new Icons.Regular.Size24.QuestionCircle(), 30 | IconColor = Color.Error, 31 | }, 32 | PrimaryAction = "Ok", 33 | SecondaryAction = "Cancel" 34 | }); 35 | return await dialog.Result; 36 | } 37 | 38 | protected void ShowSuccessToast(string entityType, string entityName, Operation operation = Operation.Add) 39 | { 40 | var sentenceCasedEntityTypeName = ToSentenceCase(entityType); 41 | var title = PrepareSuccessToastTitle(operation, sentenceCasedEntityTypeName); 42 | string message = PrepareSuccessToastMessage(entityName, operation, sentenceCasedEntityTypeName); 43 | ToastService.ShowCommunicationToast(new ToastParameters 44 | { 45 | Intent = ToastIntent.Success, 46 | Title = title, 47 | Timeout = 5000, 48 | Content = new CommunicationToastContent 49 | { 50 | Details = message 51 | } 52 | }); 53 | } 54 | 55 | protected void ShowFailureToast(string entityType, string entityName, Operation operation = Operation.Add, string failureMessage = "") 56 | { 57 | var sentenceCasedEntityTypeName = ToSentenceCase(entityType); 58 | var title = PrepareFailureToastTitle(operation, sentenceCasedEntityTypeName); 59 | var message = failureMessage == string.Empty ? PrepareFailureToastMessage(entityName, operation, sentenceCasedEntityTypeName) 60 | : failureMessage; 61 | ToastService.ShowCommunicationToast(new ToastParameters 62 | { 63 | Intent = ToastIntent.Error, 64 | Title = title, 65 | Timeout = 5000, 66 | Content = new CommunicationToastContent 67 | { 68 | Details = message 69 | } 70 | }); 71 | } 72 | 73 | protected void ShowProgressToast(string id, string entityType, string entityName, Operation operation = Operation.Add) 74 | { 75 | var sentenceCasedEntityTypeName = ToSentenceCase(entityType); 76 | ToastService.ShowProgressToast(new ToastParameters 77 | { 78 | Id = id, 79 | Intent = ToastIntent.Progress, 80 | Title = PrepareProgressToastTitle(operation, sentenceCasedEntityTypeName), 81 | Content = new ProgressToastContent 82 | { 83 | Details = PrepareProgressToastMessage(entityName, operation), 84 | } 85 | }); 86 | } 87 | 88 | protected void CloseProgressToast(string id) 89 | { 90 | ToastService.CloseToast(id); 91 | } 92 | 93 | private static string PrepareProgressToastTitle(Operation operation, string sentenceCasedEntityTypeName) 94 | { 95 | return $"{operation switch 96 | { 97 | Operation.Add => "Creating", 98 | Operation.Update => "Updating", 99 | Operation.Delete => "Deleting", 100 | _ => "Creating/Updating/Deleting" 101 | }} {sentenceCasedEntityTypeName}"; 102 | } 103 | 104 | private string PrepareProgressToastMessage(string entityName, Operation operation) 105 | { 106 | return $"{operation switch 107 | { 108 | Operation.Add => "Creating", 109 | Operation.Update => "Updating", 110 | Operation.Delete => "Deleting", 111 | _ => "Creating/Updating/Deleting" 112 | }} {entityName}. Please wait..."; 113 | } 114 | 115 | 116 | 117 | private string PrepareSuccessToastTitle(Operation operation, string entityType) 118 | { 119 | return $"{entityType} {operation switch 120 | { 121 | Operation.Add => "created", 122 | Operation.Update => "updated", 123 | Operation.Delete => "deleted", 124 | _ => "created/updated/deleted" 125 | }} successfully"; 126 | } 127 | 128 | private string PrepareSuccessToastMessage(string entityName, Operation operation, string entityType) 129 | { 130 | return $"{entityType}: {entityName} was {operation switch 131 | { 132 | Operation.Add => "created", 133 | Operation.Update => "updated", 134 | Operation.Delete => "deleted", 135 | _ => throw new InvalidOperationException("Invalid operation") 136 | }} successfully"; 137 | } 138 | 139 | private static string PrepareFailureToastTitle(Operation operation, string entityName) 140 | { 141 | return $"{operation switch 142 | { 143 | Operation.Add => "Adding", 144 | Operation.Update => "Updating", 145 | Operation.Delete => "Deleting", 146 | _ => "Add/Update/Delete action on " 147 | }} {entityName} failed"; 148 | } 149 | 150 | private string PrepareFailureToastMessage(string entityName, Operation operation, string entityType) 151 | { 152 | return $"Error {entityType}: {entityName} was {operation switch 153 | { 154 | Operation.Add => "created", 155 | Operation.Update => "updated", 156 | Operation.Delete => "deleted", 157 | _ => "created/updated/deleted" 158 | }} successfully"; 159 | } 160 | 161 | private string ToSentenceCase(string str) 162 | { 163 | if (string.IsNullOrEmpty(str)) 164 | return str; 165 | 166 | string lowerCase = str.ToLower(); 167 | return char.ToUpper(lowerCase[0]) + lowerCase.Substring(1); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | public HttpContext? HttpContext { get; set; } 30 | 31 | public string? RequestId { get; set; } 32 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Contoso Pizzas 4 | 5 | 6 | 7 |

Contoso Pizzas

8 | 9 |

The best pizza place in the world...

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

Manage Sauce(s)

18 |
19 |
20 | 21 | 22 | 23 |

Manage Topping(s)

24 |
25 |
26 | 27 | 28 | 29 |

Manage Pizza(s)

30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Pizza/PizzaUpsertPanel.razor: -------------------------------------------------------------------------------- 1 | @implements IDialogContentComponent 2 | @rendermode InteractiveServer 3 | 4 | 5 | @if (isBusy) 6 | { 7 | 8 | } 9 | else 10 | { 11 | 12 | 13 | 14 | 15 | 16 | Name 17 | * 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | Description 27 | 28 | 29 | 30 | 31 | 32 | @if (Content.Id == Guid.Empty) 33 | { 34 | 39 | 40 | 41 | Sauce 42 | 43 | 44 | 45 | 46 | 47 | Topping(s) 48 | 49 | 50 | @foreach (var topping in _toppings!) 51 | { 52 | bool isChecked = Content.Toppings!.Contains(topping); 53 | 55 | 56 | @topping.Name 57 | 58 | 59 | } 60 | 61 | @string.Join(", ", Content.Toppings!.Select(t => t.Name)) 62 | } 63 | @if (Content.Id != Guid.Empty) 64 | { 65 | 66 | Sauce 67 | 68 | @Content.Sauce!.Name 69 | 70 | Topping(s) 71 | 72 | 73 | @foreach (var topping in Content.Toppings) 74 | { 75 | 76 | 77 | @topping.Name 78 | 79 | 80 | } 81 | 82 | 83 | 84 | Created 85 | @Content.Created.ToString("dd-MM-yyyy") 86 | 87 | 88 | @if (Content.Modified.HasValue) 89 | { 90 | 91 | 92 | Modified 93 | @Content.Modified?.ToString("dd-MM-yyyy") 94 | 95 | } 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Pizza/PizzaUpsertPanel.razor.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace Contoso.Pizza.AdminUI.Components.Pages.Pizza; 6 | 7 | public partial class PizzaUpsertPanel 8 | { 9 | [Inject] 10 | SauceService SauceService { get; set; } = default!; 11 | 12 | [Inject] 13 | ToppingService ToppingService { get; set; } = default!; 14 | 15 | [Parameter] 16 | public PizzaEntity Content { get; set; } = default!; 17 | 18 | IEnumerable? _sauces; 19 | List? _toppings = []; 20 | bool isBusy; 21 | 22 | protected override async Task OnInitializedAsync() 23 | { 24 | isBusy = true; 25 | _sauces = await SauceService.GetAllSaucesAsync(); 26 | Content.Sauce = _sauces.FirstOrDefault(); 27 | _toppings = (await ToppingService.GetAllToppingsAsync()).ToList(); 28 | isBusy = false; 29 | } 30 | 31 | protected void OnToppingSelected(ToppingEntity item, bool selected) 32 | { 33 | if(selected) 34 | { 35 | Content.Toppings!.Add(item); 36 | } 37 | else 38 | { 39 | Content.Toppings!.Remove(item); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Pizza/PizzasPage.razor: -------------------------------------------------------------------------------- 1 | @page "/pizzas" 2 | @inherits BasePage 3 | @rendermode InteractiveServer 4 | 5 | Pizzas 6 | 7 |
8 | 9 | 10 | Home 11 | 12 | 13 | 14 | Pizzas 15 | 16 | 17 | 18 |
19 |
20 |

Pizzas

21 |
22 |
23 |

The best sauces in the world.

24 |
25 | @if (_pizzas == null) 26 | { 27 | 28 | } 29 | else 30 | { 31 |
32 | 33 | Add a pizza 35 | 36 |
37 | 38 | 39 | 40 | OnEditPizzaClick(context)) style="font-weight:bold;text-decoration:underline;cursor:pointer">@context.Name 41 | 42 | 43 | 44 | @string.Join(",",context.Toppings.Select(t => t.Name)) 45 | 46 | 47 | 48 | 49 | 52 | 53 | 54 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Pizza/PizzasPage.razor.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.FluentUI.AspNetCore.Components; 5 | 6 | namespace Contoso.Pizza.AdminUI.Components.Pages.Pizza; 7 | 8 | public partial class PizzasPage 9 | { 10 | [Inject] 11 | public required IPizzaService Service { get; set; } 12 | 13 | private IQueryable? _pizzas; 14 | 15 | private IDialogReference? _dialog; 16 | 17 | protected override async Task OnInitializedAsync() 18 | { 19 | await LoadPizzas(); 20 | } 21 | 22 | private async Task LoadPizzas() 23 | { 24 | _pizzas = (await Service.GetAllPizzasAsync()).AsQueryable(); 25 | } 26 | 27 | private async Task OnAddNewPizzaClick() 28 | { 29 | var panelTitle = $"Add a pizza"; 30 | var result = await ShowPanel(panelTitle, new PizzaEntity() { Name = "New Pizza" }); 31 | if (result.Cancelled) 32 | { 33 | return; 34 | } 35 | var entity = result.Data as PizzaEntity; 36 | ShowProgressToast(nameof(OnAddNewPizzaClick), "Pizza", entity!.Name); 37 | _ = await Service.AddPizzaAsync(entity!); 38 | CloseProgressToast(nameof(OnAddNewPizzaClick)); 39 | ShowSuccessToast("Pizza", entity!.Name); 40 | await LoadPizzas(); 41 | } 42 | 43 | private async Task OnEditPizzaClick(PizzaEntity pizza) 44 | { 45 | var panelTitle = $"Edit pizza"; 46 | var result = await ShowPanel(panelTitle, pizza, false); 47 | if (result.Cancelled) 48 | { 49 | return; 50 | } 51 | var entity = result.Data as PizzaEntity; 52 | ShowProgressToast(nameof(OnEditPizzaClick), "Pizza", entity!.Name, Operation.Update); 53 | await Service.UpdatePizzaAsync(entity!); 54 | CloseProgressToast(nameof(OnEditPizzaClick)); 55 | ShowSuccessToast("Pizza", entity!.Name, Operation.Update); 56 | await LoadPizzas(); 57 | } 58 | 59 | private async Task OnDeletePizzaClick(PizzaEntity entity) 60 | { 61 | var confirm = await ShowConfirmationDialogAsync("Delete Pizza", $"Are you sure you want to delete {entity.Name}?"); 62 | if (confirm.Cancelled) 63 | { 64 | return; 65 | } 66 | ShowProgressToast(nameof(OnDeletePizzaClick), "Pizza", entity.Name, Operation.Delete); 67 | await Service.DeletePizzaAsync(entity); 68 | CloseProgressToast(nameof(OnDeletePizzaClick)); 69 | ShowSuccessToast("Pizza", entity.Name, Operation.Delete); 70 | await LoadPizzas(); 71 | } 72 | 73 | private async Task ShowPanel(string title, PizzaEntity pizza, bool isAdd = true) 74 | { 75 | var primaryActionText = isAdd ? "Add" : "Save changes"; 76 | var dialogParameter = new DialogParameters() 77 | { 78 | Content = pizza, 79 | Alignment = HorizontalAlignment.Right, 80 | Title = title, 81 | PrimaryAction = primaryActionText, 82 | Width = "500px", 83 | PreventDismissOnOverlayClick = true, 84 | }; 85 | _dialog = await DialogService.ShowPanelAsync(pizza, dialogParameter); 86 | return await _dialog.Result; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Sauce/SauceUpsertPanel.razor: -------------------------------------------------------------------------------- 1 | @implements IDialogContentComponent 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Name 10 | * 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Description 20 | 21 | 22 | 23 | 24 | Is Vegan 25 | @if(Content.Id != Guid.Empty) 26 | { 27 | 28 | 29 | Created 30 | @Content.Created.ToString("dd-MM-yyyy") 31 | 32 | 33 | @if(Content.Modified.HasValue) 34 | { 35 | 36 | 37 | Modified 38 | @Content.Modified?.ToString("dd-MM-yyyy") 39 | 40 | } 41 | } 42 | 43 | 44 | 45 | @code { 46 | [Parameter] 47 | public SauceEntity Content { get; set; } = default!; 48 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Sauce/SaucesPage.razor: -------------------------------------------------------------------------------- 1 | @page "/sauces" 2 | @inherits BasePage 3 | @rendermode InteractiveServer 4 | 5 | Sauces 6 | 7 |
8 | 9 | 10 | Home 11 | 12 | 13 | 14 | Sauces 15 | 16 | 17 | 18 |
19 |
20 |

Sauces

21 |
22 |
23 |

The best sauces in the world.

24 |
25 | 26 | @if (_sauces == null) 27 | { 28 | 29 | } 30 | else 31 | { 32 |
33 | 34 | Add a sauce 36 | 37 |
38 | 39 | 40 | 41 | OnEditSauceClick(context)) style="font-weight:bold;text-decoration:underline;cursor:pointer">@context.Name 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Sauce/SaucesPage.razor.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.FluentUI.AspNetCore.Components; 5 | 6 | namespace Contoso.Pizza.AdminUI.Components.Pages.Sauce; 7 | 8 | public partial class SaucesPage 9 | { 10 | [Inject] 11 | public required ISauceService Service { get; set; } 12 | 13 | private IQueryable? _sauces; 14 | 15 | private IDialogReference? _dialog; 16 | 17 | protected override async Task OnInitializedAsync() 18 | { 19 | await LoadSauces(); 20 | } 21 | 22 | private async Task LoadSauces() 23 | { 24 | _sauces = (await Service.GetAllSaucesAsync()).AsQueryable(); 25 | } 26 | 27 | private async Task OnAddNewSauceClick() 28 | { 29 | var panelTitle = $"Add a sauce"; 30 | var result = await ShowPanel(panelTitle, new SauceEntity() { Name = "New Sauce" }); 31 | if (result.Cancelled) 32 | { 33 | return; 34 | } 35 | var entity = result.Data as SauceEntity; 36 | ShowProgressToast(nameof(OnAddNewSauceClick), "Sauce", entity!.Name); 37 | await Service.AddSauceAsync(entity!); 38 | CloseProgressToast(nameof(OnAddNewSauceClick)); 39 | ShowSuccessToast("Sauce", entity!.Name); 40 | await LoadSauces(); 41 | } 42 | 43 | private async Task OnEditSauceClick(SauceEntity sauce) 44 | { 45 | var panelTitle = $"Edit sauce"; 46 | var result = await ShowPanel(panelTitle, sauce, false); 47 | if (result.Cancelled) 48 | { 49 | return; 50 | } 51 | var entity = result.Data as SauceEntity; 52 | ShowProgressToast(nameof(OnEditSauceClick), "Sauce", entity!.Name, Operation.Update); 53 | await Service.UpdateSauceAsync(entity!); 54 | CloseProgressToast(nameof(OnEditSauceClick)); 55 | ShowSuccessToast("Sauce", entity!.Name, Operation.Update); 56 | await LoadSauces(); 57 | } 58 | 59 | private async Task OnDeleteSauceClick(SauceEntity entity) 60 | { 61 | var title = "Delete sauce?"; 62 | var message = $"Are you sure you want to delete sauce: {entity.Name}?"; 63 | var result = await ShowConfirmationDialogAsync(title, message); 64 | if (result.Cancelled) 65 | { 66 | return; 67 | } 68 | try 69 | { 70 | ShowProgressToast(nameof(OnDeleteSauceClick), "Sauce", entity!.Name, Operation.Delete); 71 | await Service.DeleteSauceAsync(entity); 72 | CloseProgressToast(nameof(OnDeleteSauceClick)); 73 | ShowSuccessToast("Sauce", entity.Name, Operation.Delete); 74 | await LoadSauces(); 75 | } 76 | catch (HttpRequestException ex) 77 | { 78 | if (ex.StatusCode == System.Net.HttpStatusCode.Conflict) 79 | { 80 | ShowFailureToast("Sauce", entity.Name, Operation.Delete, "The sauce is in use and cannot be deleted."); 81 | } 82 | else 83 | { 84 | ShowFailureToast("Sauce", entity.Name, Operation.Delete, ex.Message); 85 | } 86 | } 87 | } 88 | 89 | private async Task ShowPanel(string title, SauceEntity sauce, bool isAdd = true) 90 | { 91 | var primaryActionText = isAdd ? "Add" : "Save changes"; 92 | var dialogParameter = new DialogParameters() 93 | { 94 | Content = sauce, 95 | Alignment = HorizontalAlignment.Right, 96 | Title = title, 97 | PrimaryAction = primaryActionText, 98 | Width = "500px", 99 | PreventDismissOnOverlayClick = false, 100 | }; 101 | _dialog = await DialogService.ShowPanelAsync(sauce, dialogParameter); 102 | return await _dialog.Result; 103 | } 104 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Topping/ToppingUpsertPanel.razor: -------------------------------------------------------------------------------- 1 | @implements IDialogContentComponent 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Name 10 | * 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | Description 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Calories 28 | 29 | 30 | 31 | @if (Content.Id != Guid.Empty) 32 | { 33 | 34 | 35 | Created 36 | @Content.Created.ToString("dd-MM-yyyy") 37 | 38 | 39 | @if (Content.Modified.HasValue) 40 | { 41 | 42 | 43 | Modified 44 | @Content.Modified?.ToString("dd-MM-yyyy") 45 | 46 | } 47 | } 48 | 49 | 50 | 51 | @code { 52 | [Parameter] 53 | public ToppingEntity Content { get; set; } = default!; 54 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Topping/ToppingsPage.razor: -------------------------------------------------------------------------------- 1 | @page "/toppings" 2 | @inherits BasePage 3 | @rendermode InteractiveServer 4 | 5 | Toppings 6 | 7 |
8 | 9 | 10 | Home 11 | 12 | 13 | 14 | Toppings 15 | 16 | 17 | 18 |
19 |
20 |

Toppings

21 |
22 |
23 |

The best toppings in the world.

24 |
25 | 26 | @if (_toppings == null) 27 | { 28 | 29 | } 30 | else 31 | { 32 |
33 | 34 | Add a topping 35 | 36 |
37 | 38 | 39 | 40 | OnEditToppingClick(context)) style="font-weight:bold;text-decoration:underline;cursor:pointer">@context.Name 41 | 42 | 43 | 44 | 45 | 46 | 49 | 50 | 51 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Pages/Topping/ToppingsPage.razor.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminApi.Models; 2 | using Contoso.Pizza.AdminUI.Services.Contracts; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.FluentUI.AspNetCore.Components; 5 | 6 | namespace Contoso.Pizza.AdminUI.Components.Pages.Topping; 7 | 8 | public partial class ToppingsPage 9 | { 10 | [Inject] 11 | public required IToppingService Service { get; set; } 12 | 13 | private IQueryable? _toppings; 14 | 15 | private IDialogReference? _dialog; 16 | 17 | protected override async Task OnInitializedAsync() 18 | { 19 | await LoadToppings(); 20 | } 21 | 22 | private async Task LoadToppings() 23 | { 24 | _toppings = (await Service.GetAllToppingsAsync()).AsQueryable(); 25 | } 26 | 27 | private async Task OnAddNewToppingClick() 28 | { 29 | var panelTitle = $"Add a topping"; 30 | var result = await ShowPanel(panelTitle, new ToppingEntity() { Name = "New Topping" }); 31 | if (result.Cancelled) 32 | { 33 | return; 34 | } 35 | var entity = result.Data as ToppingEntity; 36 | ShowProgressToast(nameof(OnAddNewToppingClick), "Topping", entity!.Name); 37 | _ = await Service.AddToppingAsync(entity!); 38 | CloseProgressToast(nameof(OnAddNewToppingClick)); 39 | ShowSuccessToast("Topping", entity!.Name); 40 | await LoadToppings(); 41 | } 42 | 43 | private async Task OnEditToppingClick(ToppingEntity topping) 44 | { 45 | var panelTitle = $"Edit topping"; 46 | var result = await ShowPanel(panelTitle, topping, false); 47 | if (result.Cancelled) 48 | { 49 | return; 50 | } 51 | var entity = result.Data as ToppingEntity; 52 | ShowProgressToast(nameof(OnEditToppingClick), "Topping", entity!.Name, Operation.Update); 53 | await Service.UpdateToppingAsync(entity!); 54 | CloseProgressToast(nameof(OnEditToppingClick)); 55 | ShowSuccessToast("Topping", entity!.Name, Operation.Update); 56 | await LoadToppings(); 57 | } 58 | 59 | private async Task OnDeleteToppingClick(ToppingEntity entity) 60 | { 61 | var title = "Delete topping?"; 62 | var message = $"Are you sure you want to delete topping: {entity.Name}?"; 63 | var result = await ShowConfirmationDialogAsync(title, message); 64 | if (result.Cancelled) 65 | { 66 | return; 67 | } 68 | try 69 | { 70 | ShowProgressToast(nameof(OnDeleteToppingClick), "Topping", entity!.Name, Operation.Delete); 71 | await Service.DeleteToppingAsync(entity); 72 | CloseProgressToast(nameof(OnDeleteToppingClick)); 73 | ShowSuccessToast("Topping", entity.Name, Operation.Delete); 74 | await LoadToppings(); 75 | } 76 | catch (HttpRequestException ex) 77 | { 78 | CloseProgressToast(nameof(OnDeleteToppingClick)); 79 | if (ex.StatusCode == System.Net.HttpStatusCode.Conflict) 80 | { 81 | ShowFailureToast("Topping", entity.Name, Operation.Delete, "The topping is in use and cannot be deleted."); 82 | } 83 | else 84 | { 85 | ShowFailureToast("Topping", entity.Name, Operation.Delete, ex.Message); 86 | } 87 | } 88 | } 89 | 90 | private async Task ShowPanel(string title, ToppingEntity topping, bool isAdd = true) 91 | { 92 | var primaryActionText = isAdd ? "Add" : "Save changes"; 93 | var dialogParameter = new DialogParameters() 94 | { 95 | Content = topping, 96 | Alignment = HorizontalAlignment.Right, 97 | Title = title, 98 | PrimaryAction = primaryActionText, 99 | Width = "500px", 100 | PreventDismissOnOverlayClick = false, 101 | }; 102 | _dialog = await DialogService.ShowPanelAsync(topping, dialogParameter); 103 | return await _dialog.Result; 104 | } 105 | } -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/Shared/ProgressLoader.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 |
@ProgressText
4 |
5 | 6 | @for(var rowCounter = 0; rowCounter < 5; rowCounter++) 7 | { 8 | @for(var colCounter = 0; colCounter < Columns; colCounter++) 9 | { 10 | 11 | 12 | 13 | } 14 | } 15 | 16 | 17 | 18 | @code { 19 | 20 | [Parameter] 21 | public string ProgressText { get; set; } = "Loading..."; 22 | 23 | [Parameter] 24 | public int Columns { get; set; } = 5; 25 | } 26 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.FluentUI.AspNetCore.Components 9 | @using Microsoft.JSInterop 10 | @using Contoso.Pizza.AdminUI 11 | @using Contoso.Pizza.AdminUI.Components 12 | @using Contoso.Pizza.AdminApi.Models; 13 | @using Contoso.Pizza.AdminUI.Components.Shared; -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Contoso.Pizza.AdminUI.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4.0.0 4 | 4.0.0.23318 5 | 4.0.0.23318 6 | 4.0.0.23318 7 | 8 | 9 | net8.0 10 | enable 11 | enable 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Program.cs: -------------------------------------------------------------------------------- 1 | using Contoso.Pizza.AdminUI.Components; 2 | using Contoso.Pizza.AdminUI.Services.Extensions; 3 | using Microsoft.FluentUI.AspNetCore.Components; 4 | 5 | namespace Contoso.Pizza.AdminUI 6 | { 7 | public class Program 8 | { 9 | public static void Main(string[] args) 10 | { 11 | var builder = WebApplication.CreateBuilder(args); 12 | 13 | AddAdminUIServices(builder); 14 | 15 | AddBlazorComponents(builder); 16 | 17 | var app = builder.Build(); 18 | 19 | ConfigureBlazorComonents(app); 20 | 21 | app.Run(); 22 | } 23 | 24 | private static void ConfigureBlazorComonents(WebApplication app) 25 | { 26 | if (!app.Environment.IsDevelopment()) 27 | { 28 | app.UseExceptionHandler("/Error"); 29 | app.UseHsts(); 30 | } 31 | 32 | app.UseHttpsRedirection(); 33 | 34 | app.UseStaticFiles(); 35 | app.UseAntiforgery(); 36 | 37 | app.MapRazorComponents() 38 | .AddInteractiveServerRenderMode(); 39 | } 40 | 41 | private static void AddAdminUIServices(WebApplicationBuilder builder) 42 | { 43 | builder.Services.AddAdminUIServices(builder.Configuration); 44 | } 45 | 46 | private static void AddBlazorComponents(WebApplicationBuilder builder) 47 | { 48 | builder.Services.AddRazorComponents() 49 | .AddInteractiveServerComponents(); 50 | 51 | builder.Services.AddFluentUIComponents(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:29623", 8 | "sslPort": 44380 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5013", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7199;http://localhost:5013", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Api": { 10 | "Url": "https://localhost:7110" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | @import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css'; 2 | 3 | body { 4 | --body-font: "Segoe UI Variable", "Segoe UI", sans-serif; 5 | font-family: var(--body-font); 6 | font-size: var(--type-ramp-base-font-size); 7 | line-height: var(--type-ramp-base-line-height); 8 | margin: 0; 9 | } 10 | 11 | .main { 12 | height: calc(100dvh - 102px); 13 | color: var(--neutral-foreground-rest); 14 | } 15 | 16 | .content { 17 | margin: 0 10px; 18 | } 19 | 20 | footer { 21 | display: grid; 22 | grid-template-columns: 10px auto auto 10px; 23 | background: var(--neutral-layer-4); 24 | color: var(--neutral-foreground-rest); 25 | align-items: center; 26 | padding: 10px 10px; 27 | } 28 | 29 | footer .link1 { 30 | grid-column: 2; 31 | justify-content: start; 32 | } 33 | 34 | footer .link2 { 35 | grid-column: 3; 36 | justify-self: end; 37 | } 38 | 39 | footer a { 40 | color: var(--neutral-foreground-rest); 41 | text-decoration: none; 42 | } 43 | 44 | footer a:focus { 45 | outline: 1px dashed; 46 | outline-offset: 3px; 47 | } 48 | 49 | footer a:hover { 50 | text-decoration: underline; 51 | } 52 | 53 | .alert { 54 | border: 1px dashed var(--accent-fill-rest); 55 | padding: 5px; 56 | } 57 | 58 | 59 | #blazor-error-ui { 60 | background: lightyellow; 61 | bottom: 0; 62 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 63 | display: none; 64 | left: 0; 65 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 66 | position: fixed; 67 | width: 100%; 68 | z-index: 1000; 69 | margin: 20px 0; 70 | } 71 | 72 | #blazor-error-ui .dismiss { 73 | cursor: pointer; 74 | position: absolute; 75 | right: 0.75rem; 76 | top: 0.5rem; 77 | } 78 | 79 | .blazor-error-boundary { 80 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 81 | padding: 1rem 1rem 1rem 3.7rem; 82 | color: white; 83 | } 84 | 85 | .blazor-error-boundary::before { 86 | content: "An error has occurred. " 87 | } 88 | 89 | .loading-progress { 90 | position: relative; 91 | display: block; 92 | width: 8rem; 93 | height: 8rem; 94 | margin: 20vh auto 1rem auto; 95 | } 96 | 97 | .loading-progress circle { 98 | fill: none; 99 | stroke: #e0e0e0; 100 | stroke-width: 0.6rem; 101 | transform-origin: 50% 50%; 102 | transform: rotate(-90deg); 103 | } 104 | 105 | .loading-progress circle:last-child { 106 | stroke: #1b6ec2; 107 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%; 108 | transition: stroke-dasharray 0.05s ease-in-out; 109 | } 110 | 111 | .loading-progress-text { 112 | position: absolute; 113 | text-align: center; 114 | font-weight: bold; 115 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem; 116 | } 117 | 118 | .loading-progress-text:after { 119 | content: var(--blazor-load-percentage-text, "Loading"); 120 | } 121 | 122 | code { 123 | color: #c02d76; 124 | } 125 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lohithgn/blazor-aspnetwebapi-efcore-sample/36c2329589e1233578e52911e32cbed598a06766/src/Frontend/Contoso.Pizza.AdminUI/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/images/authrequired.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/images/counter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/images/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Frontend/Contoso.Pizza.AdminUI/wwwroot/images/weather.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------