├── .dockerignore ├── .gitattributes ├── .gitignore ├── ApiGateways ├── OcelotApiGw │ ├── Dockerfile │ ├── OcelotApiGw.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── ocelot.Development.json │ ├── ocelot.json │ └── ocelot.local.json └── Shopping.Aggregator │ ├── Controllers │ └── ShoppingController.cs │ ├── Dockerfile │ ├── Extensions │ └── HttpClientExtensions.cs │ ├── Models │ ├── BasketItemExtendedModel.cs │ ├── BasketModel.cs │ ├── CatalogModel.cs │ ├── OrderResponseModel.cs │ └── ShoppingModel.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Services │ ├── BasketService.cs │ ├── CatalogService.cs │ ├── IBasketService.cs │ ├── ICatalogService.cs │ ├── IOrderService.cs │ └── OrderService.cs │ ├── Shopping - Backup.Aggregator.csproj │ ├── Shopping.Aggregator.csproj │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── BuildingBlocks ├── Common.Logging │ ├── Common.Logging.csproj │ ├── LoggingDelegatingHandler.cs │ └── SeriLogger.cs └── EventBus.Messages │ ├── Common │ └── EventBusConstants.cs │ ├── EventBus.Messages.csproj │ └── Events │ ├── BasketCheckoutEvent.cs │ └── IntegrationBaseEvent.cs ├── Services ├── Basket │ ├── Basket.API │ │ ├── Basket - Backup.API.csproj │ │ ├── Basket.API.csproj │ │ ├── Controllers │ │ │ └── BasketController.cs │ │ ├── Dockerfile │ │ ├── Entities │ │ │ ├── BasketCheckout.cs │ │ │ ├── ShoppingCart.cs │ │ │ └── ShoppingCartItem.cs │ │ ├── GrpcServices │ │ │ └── DiscountGrpcService.cs │ │ ├── Mapper │ │ │ └── BasketProfile.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Repositories │ │ │ ├── BasketRepository.cs │ │ │ └── Interfaces │ │ │ │ └── IBasketRepository.cs │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── Basket.UnitTests │ │ ├── Basket.UnitTests.csproj │ │ └── UnitTest1.cs ├── Catalog │ ├── Catalog.API │ │ ├── Catalog.API.csproj │ │ ├── Controllers │ │ │ └── CatalogController.cs │ │ ├── Data │ │ │ ├── CatalogContext.cs │ │ │ ├── CatalogContextSeed.cs │ │ │ └── Interfaces │ │ │ │ └── ICatalogContext.cs │ │ ├── Dockerfile │ │ ├── Entities │ │ │ └── Product.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Repositories │ │ │ ├── Interfaces │ │ │ │ └── IProductRepository.cs │ │ │ └── ProductRepository.cs │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── Catalog.UnitTests │ │ ├── Catalog.UnitTests.csproj │ │ ├── Properties │ │ └── launchSettings.json │ │ └── UnitTest1.cs ├── Discount │ ├── Discount.API │ │ ├── Controllers │ │ │ └── DiscountController.cs │ │ ├── Discount - Backup.API.csproj │ │ ├── Discount.API.csproj │ │ ├── Dockerfile │ │ ├── Entities │ │ │ └── Coupon.cs │ │ ├── Extensions │ │ │ └── HostExtensions.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Repositories │ │ │ ├── DiscountRepository.cs │ │ │ └── Interfaces │ │ │ │ └── IDiscountRepository.cs │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ ├── Discount.Grpc │ │ ├── Discount.Grpc.csproj │ │ ├── Dockerfile │ │ ├── Entities │ │ │ └── Coupon.cs │ │ ├── Extensions │ │ │ └── HostExtensions.cs │ │ ├── Mapper │ │ │ └── DiscountProfile.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Protos │ │ │ └── discount.proto │ │ ├── Repositories │ │ │ ├── DiscountRepository.cs │ │ │ └── Interfaces │ │ │ │ └── IDiscountRepository.cs │ │ ├── Services │ │ │ └── DiscountService.cs │ │ ├── Startup.cs │ │ ├── appsettings.Development.json │ │ └── appsettings.json │ └── Discount.UnitTests │ │ ├── Discount.UnitTests.csproj │ │ └── UnitTest1.cs └── Ordering │ ├── Ordering.API │ ├── Controllers │ │ └── OrderController.cs │ ├── Dockerfile │ ├── EventBusConsumer │ │ └── BasketCheckoutConsumer.cs │ ├── Extensions │ │ └── HostExtensions.cs │ ├── Mapper │ │ └── OrderingProfile.cs │ ├── Ordering.API.csproj │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.Development.json │ └── appsettings.json │ ├── Ordering.Application │ ├── ApplicationServiceRegistration.cs │ ├── Behaviours │ │ ├── UnhandledExceptionBehaviour.cs │ │ └── ValidationBehaviour.cs │ ├── Contracts │ │ ├── Infrastructure │ │ │ └── IEmailService.cs │ │ └── Persistence │ │ │ ├── IAsyncRepository.cs │ │ │ └── IOrderRepository.cs │ ├── Exceptions │ │ ├── NotFoundException.cs │ │ └── ValidationException.cs │ ├── Features │ │ └── Orders │ │ │ ├── Commands │ │ │ ├── CheckoutOrder │ │ │ │ ├── CheckoutOrderCommand.cs │ │ │ │ ├── CheckoutOrderCommandHandler.cs │ │ │ │ └── CheckoutOrderCommandValidator.cs │ │ │ ├── DeleteOrder │ │ │ │ ├── DeleteOrderCommand.cs │ │ │ │ └── DeleteOrderCommandHandler.cs │ │ │ └── UpdateOrder │ │ │ │ ├── UpdateOrderCommand.cs │ │ │ │ ├── UpdateOrderCommandHandler.cs │ │ │ │ └── UpdateOrderCommandValidator.cs │ │ │ └── Queries │ │ │ └── GetOrdersList │ │ │ ├── GetOrdersListQuery.cs │ │ │ ├── GetOrdersListQueryHandler.cs │ │ │ └── OrdersVm.cs │ ├── Mappings │ │ └── MappingProfile.cs │ ├── Models │ │ ├── Email.cs │ │ └── EmailSettings.cs │ └── Ordering.Application.csproj │ ├── Ordering.Domain │ ├── Common │ │ ├── EntityBase.cs │ │ └── ValueObject.cs │ ├── Entities │ │ └── Order.cs │ └── Ordering.Domain.csproj │ └── Ordering.Infrastructure │ ├── InfrastructureServiceRegistration.cs │ ├── Mail │ └── EmailService.cs │ ├── Migrations │ ├── 20210213134039_InitialCreate.Designer.cs │ ├── 20210213134039_InitialCreate.cs │ └── OrderContextModelSnapshot.cs │ ├── Ordering.Infrastructure.csproj │ ├── Persistence │ ├── OrderContext.cs │ └── OrderContextSeed.cs │ └── Repositories │ ├── OrderRepository.cs │ └── RepositoryBase.cs ├── WebApps ├── AspnetRunBasics │ ├── AspnetRunBasics.csproj │ ├── Dockerfile │ ├── Extensions │ │ └── HttpClientExtensions.cs │ ├── Models │ │ ├── BasketCheckoutModel.cs │ │ ├── BasketItemModel.cs │ │ ├── BasketModel.cs │ │ ├── CatalogModel.cs │ │ └── OrderResponseModel.cs │ ├── Pages │ │ ├── Cart.cshtml │ │ ├── Cart.cshtml.cs │ │ ├── CheckOut.cshtml │ │ ├── CheckOut.cshtml.cs │ │ ├── Confirmation.cshtml │ │ ├── Confirmation.cshtml.cs │ │ ├── Contact.cshtml │ │ ├── Contact.cshtml.cs │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── Index.cshtml │ │ ├── Index.cshtml.cs │ │ ├── Order.cshtml │ │ ├── Order.cshtml.cs │ │ ├── Privacy.cshtml │ │ ├── Privacy.cshtml.cs │ │ ├── Product.cshtml │ │ ├── Product.cshtml.cs │ │ ├── ProductDetail.cshtml │ │ ├── ProductDetail.cshtml.cs │ │ ├── Shared │ │ │ ├── _Layout.cshtml │ │ │ ├── _ProductItemPartial.cshtml │ │ │ ├── _TopProductPartial.cshtml │ │ │ └── _ValidationScriptsPartial.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Services │ │ ├── BasketService.cs │ │ ├── CatalogService.cs │ │ ├── IBasketService.cs │ │ ├── ICatalogService.cs │ │ ├── IOrderService.cs │ │ └── OrderService.cs │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ ├── css │ │ └── style.css │ │ ├── favicon.ico │ │ └── images │ │ ├── banner │ │ ├── banner1.png │ │ ├── banner2.png │ │ └── banner3.png │ │ ├── placeholder.png │ │ └── product │ │ ├── product-1.png │ │ ├── product-2.png │ │ ├── product-3.png │ │ ├── product-4.png │ │ ├── product-5.png │ │ ├── product-6.png │ │ ├── product-7.png │ │ ├── productx1.png │ │ ├── productx2.png │ │ ├── productx3.png │ │ ├── productx4.png │ │ ├── productx5.png │ │ ├── productx6.png │ │ └── productx7.png └── WebStatus │ ├── Controllers │ └── HomeController.cs │ ├── Dockerfile │ ├── Models │ └── ErrorViewModel.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── Views │ ├── Home │ │ ├── Index.cshtml │ │ └── Privacy.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ └── _ValidationScriptsPartial.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml │ ├── WebStatus.csproj │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ ├── js │ └── site.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── aspnetrun-microservices.sln ├── docker-compose.dcproj ├── docker-compose.override.yml └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["ApiGateways/OcelotApiGw/OcelotApiGw.csproj", "ApiGateways/OcelotApiGw/"] 10 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 11 | RUN dotnet restore "ApiGateways/OcelotApiGw/OcelotApiGw.csproj" 12 | COPY . . 13 | WORKDIR "/src/ApiGateways/OcelotApiGw" 14 | RUN dotnet build "OcelotApiGw.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "OcelotApiGw.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "OcelotApiGw.dll"] -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/OcelotApiGw.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\docker-compose.dcproj 6 | Linux 7 | ..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.Hosting; 5 | using Serilog; 6 | 7 | namespace OcelotApiGw 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | CreateHostBuilder(args).Build().Run(); 14 | } 15 | 16 | public static IHostBuilder CreateHostBuilder(string[] args) => 17 | Host.CreateDefaultBuilder(args) 18 | .ConfigureAppConfiguration((hostingContext, config) => 19 | { 20 | config.AddJsonFile($"ocelot.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true); 21 | }) 22 | .UseSerilog(SeriLogger.Configure) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54290/", 7 | "sslPort": 44337 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "OcelotApiGw": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Ocelot.Cache.CacheManager; 7 | using Ocelot.DependencyInjection; 8 | using Ocelot.Middleware; 9 | 10 | namespace OcelotApiGw 11 | { 12 | public class Startup 13 | { 14 | // This method gets called by the runtime. Use this method to add services to the container. 15 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 16 | public void ConfigureServices(IServiceCollection services) 17 | { 18 | services.AddOcelot().AddCacheManager(settings => settings.WithDictionaryHandle()); 19 | } 20 | 21 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 22 | public async void Configure(IApplicationBuilder app, IWebHostEnvironment env) 23 | { 24 | if (env.IsDevelopment()) 25 | { 26 | app.UseDeveloperExceptionPage(); 27 | } 28 | 29 | app.UseRouting(); 30 | 31 | app.UseEndpoints(endpoints => 32 | { 33 | endpoints.MapGet("/", async context => 34 | { 35 | await context.Response.WriteAsync("Hello World!"); 36 | }); 37 | }); 38 | 39 | await app.UseOcelot(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ApiGateways/OcelotApiGw/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Serilog": { 3 | "MinimumLevel": { 4 | "Default": "Information", 5 | "Override": { 6 | "Microsoft": "Information", 7 | "System": "Warning" 8 | } 9 | } 10 | }, 11 | "ElasticConfiguration": { 12 | "Uri": "http://localhost:9200" 13 | }, 14 | "AllowedHosts": "*" 15 | } 16 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Controllers/ShoppingController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Shopping.Aggregator.Models; 3 | using Shopping.Aggregator.Services; 4 | using System; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | 8 | namespace Shopping.Aggregator.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/v1/[controller]")] 12 | public class ShoppingController : ControllerBase 13 | { 14 | private readonly ICatalogService _catalogService; 15 | private readonly IBasketService _basketService; 16 | private readonly IOrderService _orderService; 17 | 18 | public ShoppingController(ICatalogService catalogService, IBasketService basketService, IOrderService orderService) 19 | { 20 | _catalogService = catalogService ?? throw new ArgumentNullException(nameof(catalogService)); 21 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 22 | _orderService = orderService ?? throw new ArgumentNullException(nameof(orderService)); 23 | } 24 | 25 | [HttpGet("{userName}", Name = "GetShopping")] 26 | [ProducesResponseType(typeof(ShoppingModel), (int)HttpStatusCode.OK)] 27 | public async Task> GetShopping(string userName) 28 | { 29 | var basket = await _basketService.GetBasket(userName); 30 | 31 | foreach (var item in basket.Items) 32 | { 33 | var product = await _catalogService.GetCatalog(item.ProductId); 34 | 35 | // set additional product fields 36 | item.ProductName = product.Name; 37 | item.Category = product.Category; 38 | item.Summary = product.Summary; 39 | item.Description = product.Description; 40 | item.ImageFile = product.ImageFile; 41 | } 42 | 43 | var orders = await _orderService.GetOrdersByUserName(userName); 44 | 45 | var shoppingModel = new ShoppingModel 46 | { 47 | UserName = userName, 48 | BasketWithProducts = basket, 49 | Orders = orders 50 | }; 51 | 52 | return Ok(shoppingModel); 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj", "ApiGateways/Shopping.Aggregator/"] 10 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 11 | RUN dotnet restore "ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj" 12 | COPY . . 13 | WORKDIR "/src/ApiGateways/Shopping.Aggregator" 14 | RUN dotnet build "Shopping.Aggregator.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "Shopping.Aggregator.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "Shopping.Aggregator.dll"] -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Extensions/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Text.Json; 4 | using System.Threading.Tasks; 5 | 6 | namespace Shopping.Aggregator.Extensions 7 | { 8 | public static class HttpClientExtensions 9 | { 10 | public static async Task ReadContentAs(this HttpResponseMessage response) 11 | { 12 | if (!response.IsSuccessStatusCode) 13 | throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}"); 14 | 15 | var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 16 | 17 | return JsonSerializer.Deserialize(dataAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Models/BasketItemExtendedModel.cs: -------------------------------------------------------------------------------- 1 | namespace Shopping.Aggregator.Models 2 | { 3 | public class BasketItemExtendedModel 4 | { 5 | public int Quantity { get; set; } 6 | public string Color { get; set; } 7 | public decimal Price { get; set; } 8 | public string ProductId { get; set; } 9 | public string ProductName { get; set; } 10 | 11 | //Product Related Additional Fields 12 | public string Category { get; set; } 13 | public string Summary { get; set; } 14 | public string Description { get; set; } 15 | public string ImageFile { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Models/BasketModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Shopping.Aggregator.Models 4 | { 5 | public class BasketModel 6 | { 7 | public string UserName { get; set; } 8 | public List Items { get; set; } = new List(); 9 | public decimal TotalPrice { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Models/CatalogModel.cs: -------------------------------------------------------------------------------- 1 | namespace Shopping.Aggregator.Models 2 | { 3 | public class CatalogModel 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string Category { get; set; } 8 | public string Summary { get; set; } 9 | public string Description { get; set; } 10 | public string ImageFile { get; set; } 11 | public decimal Price { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Models/OrderResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Shopping.Aggregator.Models 2 | { 3 | public class OrderResponseModel 4 | { 5 | public string UserName { get; set; } 6 | public decimal TotalPrice { get; set; } 7 | 8 | // BillingAddress 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public string EmailAddress { get; set; } 12 | public string AddressLine { get; set; } 13 | public string Country { get; set; } 14 | public string State { get; set; } 15 | public string ZipCode { get; set; } 16 | 17 | // Payment 18 | public string CardName { get; set; } 19 | public string CardNumber { get; set; } 20 | public string Expiration { get; set; } 21 | public string CVV { get; set; } 22 | public int PaymentMethod { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Models/ShoppingModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Shopping.Aggregator.Models 4 | { 5 | public class ShoppingModel 6 | { 7 | public string UserName { get; set; } 8 | public BasketModel BasketWithProducts { get; set; } 9 | public IEnumerable Orders { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | 6 | namespace Shopping.Aggregator 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .UseSerilog(SeriLogger.Configure) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.UseStartup(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54273/", 7 | "sslPort": 44336 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Shopping.Aggregator": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/BasketService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Extensions; 2 | using Shopping.Aggregator.Models; 3 | using System; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | namespace Shopping.Aggregator.Services 8 | { 9 | public class BasketService : IBasketService 10 | { 11 | private readonly HttpClient _client; 12 | 13 | public BasketService(HttpClient client) 14 | { 15 | _client = client ?? throw new ArgumentNullException(nameof(client)); 16 | } 17 | 18 | public async Task GetBasket(string userName) 19 | { 20 | var response = await _client.GetAsync($"/api/v1/Basket/{userName}"); 21 | return await response.ReadContentAs(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/CatalogService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Extensions; 2 | using Shopping.Aggregator.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace Shopping.Aggregator.Services 9 | { 10 | public class CatalogService : ICatalogService 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public CatalogService(HttpClient client) 15 | { 16 | _client = client ?? throw new ArgumentNullException(nameof(client)); 17 | } 18 | 19 | public async Task> GetCatalog() 20 | { 21 | var response = await _client.GetAsync("/api/v1/Catalog"); 22 | return await response.ReadContentAs>(); 23 | } 24 | 25 | public async Task GetCatalog(string id) 26 | { 27 | var response = await _client.GetAsync($"/api/v1/Catalog/{id}"); 28 | return await response.ReadContentAs(); 29 | } 30 | 31 | public async Task> GetCatalogByCategory(string category) 32 | { 33 | var response = await _client.GetAsync($"/api/v1/Catalog/GetProductByCategory/{category}"); 34 | return await response.ReadContentAs>(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/IBasketService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace Shopping.Aggregator.Services 5 | { 6 | public interface IBasketService 7 | { 8 | Task GetBasket(string userName); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/ICatalogService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Models; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Shopping.Aggregator.Services 6 | { 7 | public interface ICatalogService 8 | { 9 | Task> GetCatalog(); 10 | Task> GetCatalogByCategory(string category); 11 | Task GetCatalog(string id); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/IOrderService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Models; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Shopping.Aggregator.Services 6 | { 7 | public interface IOrderService 8 | { 9 | Task> GetOrdersByUserName(string userName); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Services/OrderService.cs: -------------------------------------------------------------------------------- 1 | using Shopping.Aggregator.Extensions; 2 | using Shopping.Aggregator.Models; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | 8 | namespace Shopping.Aggregator.Services 9 | { 10 | public class OrderService : IOrderService 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public OrderService(HttpClient client) 15 | { 16 | _client = client ?? throw new ArgumentNullException(nameof(client)); 17 | } 18 | 19 | public async Task> GetOrdersByUserName(string userName) 20 | { 21 | var response = await _client.GetAsync($"/api/v1/Order/{userName}"); 22 | return await response.ReadContentAs>(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Shopping - Backup.Aggregator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\docker-compose.dcproj 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\docker-compose.dcproj 6 | Linux 7 | ..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ApiGateways/Shopping.Aggregator/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiSettings": { 3 | "CatalogUrl": "http://localhost:8000", 4 | "BasketUrl": "http://localhost:8001", 5 | "OrderingUrl": "http://localhost:8004" 6 | }, 7 | "Serilog": { 8 | "MinimumLevel": { 9 | "Default": "Information", 10 | "Override": { 11 | "Microsoft": "Information", 12 | "System": "Warning" 13 | } 14 | } 15 | }, 16 | "ElasticConfiguration": { 17 | "Uri": "http://localhost:9200" 18 | }, 19 | "AllowedHosts": "*" 20 | } 21 | -------------------------------------------------------------------------------- /BuildingBlocks/Common.Logging/Common.Logging.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BuildingBlocks/Common.Logging/LoggingDelegatingHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Sockets; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Common.Logging 9 | { 10 | public class LoggingDelegatingHandler : DelegatingHandler 11 | { 12 | private readonly ILogger logger; 13 | 14 | public LoggingDelegatingHandler(ILogger logger) 15 | { 16 | this.logger = logger; 17 | } 18 | 19 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 20 | { 21 | try 22 | { 23 | logger.LogInformation("Sending request to {Url}", request.RequestUri); 24 | 25 | var response = await base.SendAsync(request, cancellationToken); 26 | 27 | if (response.IsSuccessStatusCode) 28 | { 29 | logger.LogInformation("Received a success response from {Url}", response.RequestMessage.RequestUri); 30 | } 31 | else 32 | { 33 | logger.LogWarning("Received a non-success status code {StatusCode} from {Url}", 34 | (int)response.StatusCode, response.RequestMessage.RequestUri); 35 | } 36 | 37 | return response; 38 | } 39 | catch (HttpRequestException ex) 40 | when (ex.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionRefused) 41 | { 42 | var hostWithPort = request.RequestUri != null && request.RequestUri.IsDefaultPort 43 | ? request.RequestUri.DnsSafeHost 44 | : $"{request.RequestUri.DnsSafeHost}:{request.RequestUri.Port}"; 45 | 46 | logger.LogCritical(ex, "Unable to connect to {Host}. Please check the " + 47 | "configuration to ensure the correct URL for the service " + 48 | "has been configured.", hostWithPort); 49 | } 50 | 51 | return new HttpResponseMessage(HttpStatusCode.BadGateway) 52 | { 53 | RequestMessage = request 54 | }; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /BuildingBlocks/Common.Logging/SeriLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.Hosting; 3 | using Serilog; 4 | using Serilog.Sinks.Elasticsearch; 5 | using System; 6 | 7 | namespace Common.Logging 8 | { 9 | public static class SeriLogger 10 | { 11 | public static Action Configure => 12 | (context, configuration) => 13 | { 14 | var elasticUri = context.Configuration.GetValue("ElasticConfiguration:Uri"); 15 | 16 | configuration 17 | .Enrich.FromLogContext() 18 | .Enrich.WithMachineName() 19 | .WriteTo.Debug() 20 | .WriteTo.Console() 21 | .WriteTo.Elasticsearch( 22 | new ElasticsearchSinkOptions(new Uri(elasticUri)) 23 | { 24 | IndexFormat = $"applogs-{context.HostingEnvironment.ApplicationName?.ToLower().Replace(".", "-")}-{context.HostingEnvironment.EnvironmentName?.ToLower().Replace(".", "-")}-{DateTime.UtcNow:yyyy-MM}", 25 | AutoRegisterTemplate = true, 26 | NumberOfShards = 2, 27 | NumberOfReplicas = 1 28 | }) 29 | .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName) 30 | .Enrich.WithProperty("Application", context.HostingEnvironment.ApplicationName) 31 | .ReadFrom.Configuration(context.Configuration); 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BuildingBlocks/EventBus.Messages/Common/EventBusConstants.cs: -------------------------------------------------------------------------------- 1 | namespace EventBus.Messages.Common 2 | { 3 | public static class EventBusConstants 4 | { 5 | public const string BasketCheckoutQueue = "basketcheckout-queue"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BuildingBlocks/EventBus.Messages/Events/BasketCheckoutEvent.cs: -------------------------------------------------------------------------------- 1 | namespace EventBus.Messages.Events 2 | { 3 | public class BasketCheckoutEvent : IntegrationBaseEvent 4 | { 5 | public string UserName { get; set; } 6 | public decimal TotalPrice { get; set; } 7 | 8 | // BillingAddress 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public string EmailAddress { get; set; } 12 | public string AddressLine { get; set; } 13 | public string Country { get; set; } 14 | public string State { get; set; } 15 | public string ZipCode { get; set; } 16 | 17 | // Payment 18 | public string CardName { get; set; } 19 | public string CardNumber { get; set; } 20 | public string Expiration { get; set; } 21 | public string CVV { get; set; } 22 | public int PaymentMethod { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BuildingBlocks/EventBus.Messages/Events/IntegrationBaseEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EventBus.Messages.Events 4 | { 5 | public class IntegrationBaseEvent 6 | { 7 | public IntegrationBaseEvent() 8 | { 9 | Id = Guid.NewGuid(); 10 | CreationDate = DateTime.UtcNow; 11 | } 12 | 13 | public IntegrationBaseEvent(Guid id, DateTime createDate) 14 | { 15 | Id = id; 16 | CreationDate = createDate; 17 | } 18 | 19 | public Guid Id { get; private set; } 20 | 21 | public DateTime CreationDate { get; private set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Basket - Backup.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Basket.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Protos\discount.proto 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["Services/Basket/Basket.API/Basket.API.csproj", "Services/Basket/Basket.API/"] 10 | COPY ["BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj", "BuildingBlocks/EventBus.Messages/"] 11 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 12 | RUN dotnet restore "Services/Basket/Basket.API/Basket.API.csproj" 13 | COPY . . 14 | WORKDIR "/src/Services/Basket/Basket.API" 15 | RUN dotnet build "Basket.API.csproj" -c Release -o /app/build 16 | 17 | FROM build AS publish 18 | RUN dotnet publish "Basket.API.csproj" -c Release -o /app/publish 19 | 20 | FROM base AS final 21 | WORKDIR /app 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "Basket.API.dll"] -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Entities/BasketCheckout.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Basket.API.Entities 7 | { 8 | public class BasketCheckout 9 | { 10 | public string UserName { get; set; } 11 | public decimal TotalPrice { get; set; } 12 | 13 | // BillingAddress 14 | public string FirstName { get; set; } 15 | public string LastName { get; set; } 16 | public string EmailAddress { get; set; } 17 | public string AddressLine { get; set; } 18 | public string Country { get; set; } 19 | public string State { get; set; } 20 | public string ZipCode { get; set; } 21 | 22 | // Payment 23 | public string CardName { get; set; } 24 | public string CardNumber { get; set; } 25 | public string Expiration { get; set; } 26 | public string CVV { get; set; } 27 | public int PaymentMethod { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Entities/ShoppingCart.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Basket.API.Entities 4 | { 5 | public class ShoppingCart 6 | { 7 | public string UserName { get; set; } 8 | public List Items { get; set; } = new List(); 9 | 10 | public ShoppingCart() 11 | { 12 | } 13 | 14 | public ShoppingCart(string userName) 15 | { 16 | UserName = userName; 17 | } 18 | 19 | public decimal TotalPrice 20 | { 21 | get 22 | { 23 | decimal totalprice = 0; 24 | foreach (var item in Items) 25 | { 26 | totalprice += item.Price * item.Quantity; 27 | } 28 | return totalprice; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Entities/ShoppingCartItem.cs: -------------------------------------------------------------------------------- 1 | namespace Basket.API.Entities 2 | { 3 | public class ShoppingCartItem 4 | { 5 | public int Quantity { get; set; } 6 | public string Color { get; set; } 7 | public decimal Price { get; set; } 8 | public string ProductId { get; set; } 9 | public string ProductName { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/GrpcServices/DiscountGrpcService.cs: -------------------------------------------------------------------------------- 1 | using Discount.Grpc.Protos; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace Basket.API.GrpcServices 6 | { 7 | public class DiscountGrpcService 8 | { 9 | private readonly DiscountProtoService.DiscountProtoServiceClient _discountProtoService; 10 | 11 | public DiscountGrpcService(DiscountProtoService.DiscountProtoServiceClient discountProtoService) 12 | { 13 | _discountProtoService = discountProtoService ?? throw new ArgumentNullException(nameof(discountProtoService)); 14 | } 15 | 16 | public async Task GetDiscount(string productName) 17 | { 18 | var discountRequest = new GetDiscountRequest { ProductName = productName }; 19 | 20 | return await _discountProtoService.GetDiscountAsync(discountRequest); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Mapper/BasketProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Basket.API.Entities; 3 | using EventBus.Messages.Events; 4 | 5 | namespace Basket.API.Mapper 6 | { 7 | public class BasketProfile : Profile 8 | { 9 | public BasketProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | 6 | namespace Basket.API 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .UseSerilog(SeriLogger.Configure) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.UseStartup(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54287/", 7 | "sslPort": 44332 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Basket.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Repositories/BasketRepository.cs: -------------------------------------------------------------------------------- 1 | using Basket.API.Entities; 2 | using Basket.API.Repositories.Interfaces; 3 | using Microsoft.Extensions.Caching.Distributed; 4 | using Newtonsoft.Json; 5 | using System; 6 | using System.Threading.Tasks; 7 | 8 | namespace Basket.API.Repositories 9 | { 10 | public class BasketRepository : IBasketRepository 11 | { 12 | private readonly IDistributedCache _redisCache; 13 | 14 | public BasketRepository(IDistributedCache cache) 15 | { 16 | _redisCache = cache ?? throw new ArgumentNullException(nameof(cache)); 17 | } 18 | 19 | public async Task GetBasket(string userName) 20 | { 21 | var basket = await _redisCache.GetStringAsync(userName); 22 | 23 | if (String.IsNullOrEmpty(basket)) 24 | return null; 25 | 26 | return JsonConvert.DeserializeObject(basket); 27 | } 28 | 29 | public async Task UpdateBasket(ShoppingCart basket) 30 | { 31 | await _redisCache.SetStringAsync(basket.UserName, JsonConvert.SerializeObject(basket)); 32 | 33 | return await GetBasket(basket.UserName); 34 | } 35 | 36 | public async Task DeleteBasket(string userName) 37 | { 38 | await _redisCache.RemoveAsync(userName); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/Repositories/Interfaces/IBasketRepository.cs: -------------------------------------------------------------------------------- 1 | using Basket.API.Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace Basket.API.Repositories.Interfaces 5 | { 6 | public interface IBasketRepository 7 | { 8 | Task GetBasket(string userName); 9 | Task UpdateBasket(ShoppingCart basket); 10 | Task DeleteBasket(string userName); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Basket/Basket.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CacheSettings": { 3 | "ConnectionString": "localhost:6379" 4 | }, 5 | "GrpcSettings": { 6 | "DiscountUrl": "http://localhost:5003" 7 | }, 8 | "EventBusSettings": { 9 | "HostAddress": "amqp://guest:guest@localhost:5672" 10 | }, 11 | "Serilog": { 12 | "MinimumLevel": { 13 | "Default": "Information", 14 | "Override": { 15 | "Microsoft": "Information", 16 | "System": "Warning" 17 | } 18 | } 19 | }, 20 | "ElasticConfiguration": { 21 | "Uri": "http://localhost:9200" 22 | }, 23 | "AllowedHosts": "*" 24 | } 25 | -------------------------------------------------------------------------------- /Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Services/Basket/Basket.UnitTests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Basket.UnitTests 5 | { 6 | public class UnitTest1 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Catalog.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Data/CatalogContext.cs: -------------------------------------------------------------------------------- 1 | using Catalog.API.Data.Interfaces; 2 | using Catalog.API.Entities; 3 | using Microsoft.Extensions.Configuration; 4 | using MongoDB.Driver; 5 | 6 | namespace Catalog.API.Data 7 | { 8 | public class CatalogContext : ICatalogContext 9 | { 10 | public CatalogContext(IConfiguration configuration) 11 | { 12 | var client = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString")); 13 | var database = client.GetDatabase(configuration.GetValue("DatabaseSettings:DatabaseName")); 14 | 15 | Products = database.GetCollection(configuration.GetValue("DatabaseSettings:CollectionName")); 16 | CatalogContextSeed.SeedData(Products); 17 | } 18 | 19 | public IMongoCollection Products { get; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Data/Interfaces/ICatalogContext.cs: -------------------------------------------------------------------------------- 1 | using Catalog.API.Entities; 2 | using MongoDB.Driver; 3 | 4 | namespace Catalog.API.Data.Interfaces 5 | { 6 | public interface ICatalogContext 7 | { 8 | IMongoCollection Products { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["Services/Catalog/Catalog.API/Catalog.API.csproj", "Services/Catalog/Catalog.API/"] 10 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 11 | RUN dotnet restore "Services/Catalog/Catalog.API/Catalog.API.csproj" 12 | COPY . . 13 | WORKDIR "/src/Services/Catalog/Catalog.API" 14 | RUN dotnet build "Catalog.API.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "Catalog.API.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "Catalog.API.dll"] -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Bson; 2 | using MongoDB.Bson.Serialization.Attributes; 3 | 4 | namespace Catalog.API.Entities 5 | { 6 | public class Product 7 | { 8 | [BsonId] 9 | [BsonRepresentation(BsonType.ObjectId)] 10 | public string Id { get; set; } 11 | 12 | [BsonElement("Name")] 13 | public string Name { get; set; } 14 | public string Category { get; set; } 15 | public string Summary { get; set; } 16 | public string Description { get; set; } 17 | public string ImageFile { get; set; } 18 | public decimal Price { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | 6 | namespace Catalog.API 7 | { 8 | public class Program 9 | { 10 | public static void Main(string[] args) 11 | { 12 | CreateHostBuilder(args).Build().Run(); 13 | } 14 | 15 | public static IHostBuilder CreateHostBuilder(string[] args) => 16 | Host.CreateDefaultBuilder(args) 17 | .UseSerilog(SeriLogger.Configure) 18 | .ConfigureWebHostDefaults(webBuilder => 19 | { 20 | webBuilder.UseStartup(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54271/", 7 | "sslPort": 44316 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Catalog.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Repositories/Interfaces/IProductRepository.cs: -------------------------------------------------------------------------------- 1 | using Catalog.API.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Catalog.API.Repositories.Interfaces 8 | { 9 | public interface IProductRepository 10 | { 11 | Task> GetProducts(); 12 | Task GetProduct(string id); 13 | Task> GetProductByName(string name); 14 | Task> GetProductByCategory(string categoryName); 15 | 16 | Task CreateProduct(Product product); 17 | Task UpdateProduct(Product product); 18 | Task DeleteProduct(string id); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Repositories/ProductRepository.cs: -------------------------------------------------------------------------------- 1 | using Catalog.API.Data.Interfaces; 2 | using Catalog.API.Entities; 3 | using Catalog.API.Repositories.Interfaces; 4 | using MongoDB.Driver; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | 9 | namespace Catalog.API.Repositories 10 | { 11 | public class ProductRepository : IProductRepository 12 | { 13 | private readonly ICatalogContext _context; 14 | 15 | public ProductRepository(ICatalogContext context) 16 | { 17 | _context = context ?? throw new ArgumentNullException(nameof(context)); 18 | } 19 | 20 | public async Task> GetProducts() 21 | { 22 | return await _context 23 | .Products 24 | .Find(p => true) 25 | .ToListAsync(); 26 | } 27 | 28 | public async Task GetProduct(string id) 29 | { 30 | return await _context 31 | .Products 32 | .Find(p => p.Id == id) 33 | .FirstOrDefaultAsync(); 34 | } 35 | 36 | public async Task> GetProductByName(string name) 37 | { 38 | FilterDefinition filter = Builders.Filter.ElemMatch(p => p.Name, name); 39 | 40 | return await _context 41 | .Products 42 | .Find(filter) 43 | .ToListAsync(); 44 | } 45 | 46 | public async Task> GetProductByCategory(string categoryName) 47 | { 48 | FilterDefinition filter = Builders.Filter.Eq(p => p.Category, categoryName); 49 | 50 | return await _context 51 | .Products 52 | .Find(filter) 53 | .ToListAsync(); 54 | } 55 | 56 | 57 | public async Task CreateProduct(Product product) 58 | { 59 | await _context.Products.InsertOneAsync(product); 60 | } 61 | 62 | public async Task UpdateProduct(Product product) 63 | { 64 | var updateResult = await _context 65 | .Products 66 | .ReplaceOneAsync(filter: g => g.Id == product.Id, replacement: product); 67 | 68 | return updateResult.IsAcknowledged 69 | && updateResult.ModifiedCount > 0; 70 | } 71 | 72 | public async Task DeleteProduct(string id) 73 | { 74 | FilterDefinition filter = Builders.Filter.Eq(p => p.Id, id); 75 | 76 | DeleteResult deleteResult = await _context 77 | .Products 78 | .DeleteOneAsync(filter); 79 | 80 | return deleteResult.IsAcknowledged 81 | && deleteResult.DeletedCount > 0; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using Catalog.API.Data; 2 | using Catalog.API.Data.Interfaces; 3 | using Catalog.API.Repositories; 4 | using Catalog.API.Repositories.Interfaces; 5 | using HealthChecks.UI.Client; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Diagnostics.HealthChecks; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.OpenApi.Models; 14 | 15 | namespace Catalog.API 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddScoped(); 30 | services.AddScoped(); 31 | 32 | services.AddControllers(); 33 | services.AddSwaggerGen(c => 34 | { 35 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "Catalog.API", Version = "v1" }); 36 | }); 37 | 38 | services.AddHealthChecks() 39 | .AddMongoDb(Configuration["DatabaseSettings:ConnectionString"], "MongoDb Health", HealthStatus.Degraded); 40 | } 41 | 42 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 43 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 44 | { 45 | if (env.IsDevelopment()) 46 | { 47 | app.UseDeveloperExceptionPage(); 48 | app.UseSwagger(); 49 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Catalog.API v1")); 50 | } 51 | 52 | app.UseRouting(); 53 | 54 | app.UseAuthorization(); 55 | 56 | app.UseEndpoints(endpoints => 57 | { 58 | endpoints.MapControllers(); 59 | endpoints.MapHealthChecks("/hc", new HealthCheckOptions() 60 | { 61 | Predicate = _ => true, 62 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 63 | }); 64 | }); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "mongodb://localhost:27017", 4 | "DatabaseName": "CatalogDb", 5 | "CollectionName": "Products" 6 | }, 7 | "Serilog": { 8 | "MinimumLevel": { 9 | "Default": "Information", 10 | "Override": { 11 | "Microsoft": "Information", 12 | "System": "Warning" 13 | } 14 | } 15 | }, 16 | "ElasticConfiguration": { 17 | "Uri": "http://localhost:9200" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "mongodb://localhost:27017", 4 | "DatabaseName": "CatalogDb", 5 | "CollectionName": "Products" 6 | }, 7 | "Serilog": { 8 | "MinimumLevel": { 9 | "Default": "Information", 10 | "Override": { 11 | "Microsoft": "Information", 12 | "System": "Warning" 13 | } 14 | } 15 | }, 16 | "ElasticConfiguration": { 17 | "Uri": "http://localhost:9200" 18 | }, 19 | "AllowedHosts": "*" 20 | } 21 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Services/Catalog/Catalog.UnitTests/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Catalog.UnitTests": { 4 | "commandName": "Project" 5 | }, 6 | "WSL": { 7 | "commandName": "WSL2", 8 | "environmentVariables": {}, 9 | "distributionName": "" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Services/Catalog/Catalog.UnitTests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Catalog.UnitTests 5 | { 6 | public class UnitTest1 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Controllers/DiscountController.cs: -------------------------------------------------------------------------------- 1 | using Discount.API.Entities; 2 | using Discount.API.Repositories.Interfaces; 3 | using Microsoft.AspNetCore.Mvc; 4 | using System; 5 | using System.Net; 6 | using System.Threading.Tasks; 7 | 8 | namespace Discount.API.Controllers 9 | { 10 | [ApiController] 11 | [Route("api/v1/[controller]")] 12 | public class DiscountController : ControllerBase 13 | { 14 | private readonly IDiscountRepository _repository; 15 | 16 | public DiscountController(IDiscountRepository repository) 17 | { 18 | _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 19 | } 20 | 21 | [HttpGet("{productName}", Name = "GetDiscount")] 22 | [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)] 23 | public async Task> GetDiscount(string productName) 24 | { 25 | var discount = await _repository.GetDiscount(productName); 26 | return Ok(discount); 27 | } 28 | 29 | [HttpPost] 30 | [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)] 31 | public async Task> CreateDiscount([FromBody] Coupon coupon) 32 | { 33 | await _repository.CreateDiscount(coupon); 34 | return CreatedAtRoute("GetDiscount", new { productName = coupon.ProductName }, coupon); 35 | } 36 | 37 | [HttpPut] 38 | [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)] 39 | public async Task> UpdateBasket([FromBody] Coupon coupon) 40 | { 41 | return Ok(await _repository.UpdateDiscount(coupon)); 42 | } 43 | 44 | [HttpDelete("{productName}", Name = "DeleteDiscount")] 45 | [ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)] 46 | public async Task> DeleteDiscount(string productName) 47 | { 48 | return Ok(await _repository.DeleteDiscount(productName)); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Discount - Backup.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Discount.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["Services/Discount/Discount.API/Discount.API.csproj", "Services/Discount/Discount.API/"] 10 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 11 | RUN dotnet restore "Services/Discount/Discount.API/Discount.API.csproj" 12 | COPY . . 13 | WORKDIR "/src/Services/Discount/Discount.API" 14 | RUN dotnet build "Discount.API.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "Discount.API.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "Discount.API.dll"] -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Entities/Coupon.cs: -------------------------------------------------------------------------------- 1 | namespace Discount.API.Entities 2 | { 3 | public class Coupon 4 | { 5 | public int Id { get; set; } 6 | public string ProductName { get; set; } 7 | public string Description { get; set; } 8 | public int Amount { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Discount.API.Extensions; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | using Serilog; 6 | 7 | namespace Discount.API 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | var host = CreateHostBuilder(args).Build(); 14 | host.MigrateDatabase(); 15 | host.Run(); 16 | } 17 | 18 | public static IHostBuilder CreateHostBuilder(string[] args) => 19 | Host.CreateDefaultBuilder(args) 20 | .UseSerilog(SeriLogger.Configure) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54269/", 7 | "sslPort": 44397 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Discount.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Repositories/DiscountRepository.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Discount.API.Entities; 3 | using Discount.API.Repositories.Interfaces; 4 | using Microsoft.Extensions.Configuration; 5 | using Npgsql; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace Discount.API.Repositories 10 | { 11 | public class DiscountRepository : IDiscountRepository 12 | { 13 | private readonly IConfiguration _configuration; 14 | 15 | public DiscountRepository(IConfiguration configuration) 16 | { 17 | _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); 18 | } 19 | 20 | public async Task GetDiscount(string productName) 21 | { 22 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 23 | 24 | var coupon = await connection.QueryFirstOrDefaultAsync 25 | ("SELECT * FROM Coupon WHERE ProductName = @ProductName", new { ProductName = productName }); 26 | 27 | if (coupon == null) 28 | return new Coupon { ProductName = "No Discount", Amount = 0, Description = "No Discount Desc" }; 29 | return coupon; 30 | } 31 | 32 | public async Task CreateDiscount(Coupon coupon) 33 | { 34 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 35 | 36 | var affected = 37 | await connection.ExecuteAsync 38 | ("INSERT INTO Coupon (ProductName, Description, Amount) VALUES (@ProductName, @Description, @Amount)", 39 | new { ProductName = coupon.ProductName, Description = coupon.Description, Amount = coupon.Amount }); 40 | 41 | if (affected == 0) 42 | return false; 43 | 44 | return true; 45 | } 46 | 47 | public async Task UpdateDiscount(Coupon coupon) 48 | { 49 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 50 | 51 | var affected = await connection.ExecuteAsync 52 | ("UPDATE Coupon SET ProductName=@ProductName, Description = @Description, Amount = @Amount WHERE Id = @Id", 53 | new { ProductName = coupon.ProductName, Description = coupon.Description, Amount = coupon.Amount, Id = coupon.Id }); 54 | 55 | if (affected == 0) 56 | return false; 57 | 58 | return true; 59 | } 60 | 61 | public async Task DeleteDiscount(string productName) 62 | { 63 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 64 | 65 | var affected = await connection.ExecuteAsync("DELETE FROM Coupon WHERE ProductName = @ProductName", 66 | new { ProductName = productName }); 67 | 68 | if (affected == 0) 69 | return false; 70 | 71 | return true; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Repositories/Interfaces/IDiscountRepository.cs: -------------------------------------------------------------------------------- 1 | using Discount.API.Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace Discount.API.Repositories.Interfaces 5 | { 6 | public interface IDiscountRepository 7 | { 8 | Task GetDiscount(string productName); 9 | 10 | Task CreateDiscount(Coupon coupon); 11 | Task UpdateDiscount(Coupon coupon); 12 | Task DeleteDiscount(string productName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/Startup.cs: -------------------------------------------------------------------------------- 1 | using Discount.API.Repositories; 2 | using Discount.API.Repositories.Interfaces; 3 | using HealthChecks.UI.Client; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.OpenApi.Models; 11 | 12 | namespace Discount.API 13 | { 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | services.AddScoped(); 27 | 28 | services.AddControllers(); 29 | services.AddSwaggerGen(c => 30 | { 31 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "Discount.API", Version = "v1" }); 32 | }); 33 | 34 | services.AddHealthChecks() 35 | .AddNpgSql(Configuration["DatabaseSettings:ConnectionString"]); 36 | } 37 | 38 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 39 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 40 | { 41 | if (env.IsDevelopment()) 42 | { 43 | app.UseDeveloperExceptionPage(); 44 | app.UseSwagger(); 45 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Discount.API v1")); 46 | } 47 | 48 | app.UseRouting(); 49 | 50 | app.UseAuthorization(); 51 | 52 | app.UseEndpoints(endpoints => 53 | { 54 | endpoints.MapControllers(); 55 | endpoints.MapHealthChecks("/hc", new HealthCheckOptions() 56 | { 57 | Predicate = _ => true, 58 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 59 | }); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Discount/Discount.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "Server=localhost;Port=5432;Database=DiscountDb;User Id=admin;Password=admin1234;" 4 | }, 5 | "Serilog": { 6 | "MinimumLevel": { 7 | "Default": "Information", 8 | "Override": { 9 | "Microsoft": "Information", 10 | "System": "Warning" 11 | } 12 | } 13 | }, 14 | "ElasticConfiguration": { 15 | "Uri": "http://localhost:9200" 16 | }, 17 | "AllowedHosts": "*" 18 | } 19 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Discount.Grpc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["Services/Discount/Discount.Grpc/Discount.Grpc.csproj", "Services/Discount/Discount.Grpc/"] 10 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 11 | RUN dotnet restore "Services/Discount/Discount.Grpc/Discount.Grpc.csproj" 12 | COPY . . 13 | WORKDIR "/src/Services/Discount/Discount.Grpc" 14 | RUN dotnet build "Discount.Grpc.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "Discount.Grpc.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "Discount.Grpc.dll"] -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Entities/Coupon.cs: -------------------------------------------------------------------------------- 1 | namespace Discount.Grpc.Entities 2 | { 3 | public class Coupon 4 | { 5 | public int Id { get; set; } 6 | public string ProductName { get; set; } 7 | public string Description { get; set; } 8 | public int Amount { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Mapper/DiscountProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Discount.Grpc.Entities; 3 | using Discount.Grpc.Protos; 4 | 5 | namespace Discount.Grpc.Mapper 6 | { 7 | public class DiscountProfile : Profile 8 | { 9 | public DiscountProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Program.cs: -------------------------------------------------------------------------------- 1 | using Common.Logging; 2 | using Discount.Grpc.Extensions; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Hosting; 5 | using Serilog; 6 | 7 | namespace Discount.Grpc 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | var host = CreateHostBuilder(args).Build(); 14 | host.MigrateDatabase(); 15 | host.Run(); 16 | } 17 | 18 | // Additional configuration is required to successfully run gRPC on macOS. 19 | // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 20 | public static IHostBuilder CreateHostBuilder(string[] args) => 21 | Host.CreateDefaultBuilder(args) 22 | .UseSerilog(SeriLogger.Configure) 23 | .ConfigureWebHostDefaults(webBuilder => 24 | { 25 | webBuilder.UseStartup(); 26 | }); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54268/", 7 | "sslPort": 44350 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Discount.Grpc": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Protos/discount.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "Discount.Grpc.Protos"; 4 | 5 | service DiscountProtoService { 6 | 7 | rpc GetDiscount (GetDiscountRequest) returns (CouponModel); 8 | 9 | rpc CreateDiscount (CreateDiscountRequest) returns (CouponModel); 10 | rpc UpdateDiscount (UpdateDiscountRequest) returns (CouponModel); 11 | rpc DeleteDiscount (DeleteDiscountRequest) returns (DeleteDiscountResponse); 12 | } 13 | 14 | message GetDiscountRequest { 15 | string productName = 1; 16 | } 17 | 18 | message CouponModel { 19 | int32 id = 1; 20 | string productName = 2; 21 | string description = 3; 22 | int32 amount = 4; 23 | } 24 | 25 | message CreateDiscountRequest { 26 | CouponModel coupon = 1; 27 | } 28 | 29 | message UpdateDiscountRequest { 30 | CouponModel coupon = 1; 31 | } 32 | 33 | message DeleteDiscountRequest { 34 | string productName = 1; 35 | } 36 | 37 | message DeleteDiscountResponse { 38 | bool success = 1; 39 | } -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Repositories/DiscountRepository.cs: -------------------------------------------------------------------------------- 1 | using Dapper; 2 | using Discount.Grpc.Entities; 3 | using Discount.Grpc.Repositories.Interfaces; 4 | using Microsoft.Extensions.Configuration; 5 | using Npgsql; 6 | using System; 7 | using System.Threading.Tasks; 8 | 9 | namespace Discount.Grpc.Repositories 10 | { 11 | public class DiscountRepository : IDiscountRepository 12 | { 13 | private readonly IConfiguration _configuration; 14 | 15 | public DiscountRepository(IConfiguration configuration) 16 | { 17 | _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); 18 | } 19 | 20 | public async Task GetDiscount(string productName) 21 | { 22 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 23 | 24 | var coupon = await connection.QueryFirstOrDefaultAsync 25 | ("SELECT * FROM Coupon WHERE ProductName = @ProductName", new { ProductName = productName }); 26 | 27 | if (coupon == null) 28 | return new Coupon { ProductName = "No Discount", Amount = 0, Description = "No Discount Desc" }; 29 | return coupon; 30 | } 31 | 32 | public async Task CreateDiscount(Coupon coupon) 33 | { 34 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 35 | 36 | var affected = 37 | await connection.ExecuteAsync 38 | ("INSERT INTO Coupon (ProductName, Description, Amount) VALUES (@ProductName, @Description, @Amount)", 39 | new { ProductName = coupon.ProductName, Description = coupon.Description, Amount = coupon.Amount }); 40 | 41 | if (affected == 0) 42 | return false; 43 | 44 | return true; 45 | } 46 | 47 | public async Task UpdateDiscount(Coupon coupon) 48 | { 49 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 50 | 51 | var affected = await connection.ExecuteAsync 52 | ("UPDATE Coupon SET ProductName=@ProductName, Description = @Description, Amount = @Amount WHERE Id = @Id", 53 | new { ProductName = coupon.ProductName, Description = coupon.Description, Amount = coupon.Amount, Id = coupon.Id }); 54 | 55 | if (affected == 0) 56 | return false; 57 | 58 | return true; 59 | } 60 | 61 | public async Task DeleteDiscount(string productName) 62 | { 63 | using var connection = new NpgsqlConnection(_configuration.GetValue("DatabaseSettings:ConnectionString")); 64 | 65 | var affected = await connection.ExecuteAsync("DELETE FROM Coupon WHERE ProductName = @ProductName", 66 | new { ProductName = productName }); 67 | 68 | if (affected == 0) 69 | return false; 70 | 71 | return true; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Repositories/Interfaces/IDiscountRepository.cs: -------------------------------------------------------------------------------- 1 | using Discount.Grpc.Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace Discount.Grpc.Repositories.Interfaces 5 | { 6 | public interface IDiscountRepository 7 | { 8 | Task GetDiscount(string productName); 9 | 10 | Task CreateDiscount(Coupon coupon); 11 | Task UpdateDiscount(Coupon coupon); 12 | Task DeleteDiscount(string productName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/Startup.cs: -------------------------------------------------------------------------------- 1 | using Discount.Grpc.Repositories; 2 | using Discount.Grpc.Repositories.Interfaces; 3 | using Discount.Grpc.Services; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace Discount.Grpc 15 | { 16 | public class Startup 17 | { 18 | // This method gets called by the runtime. Use this method to add services to the container. 19 | // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 20 | public void ConfigureServices(IServiceCollection services) 21 | { 22 | services.AddScoped(); 23 | services.AddAutoMapper(typeof(Startup)); 24 | 25 | services.AddGrpc(); 26 | } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 29 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 30 | { 31 | if (env.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | 36 | app.UseRouting(); 37 | 38 | app.UseEndpoints(endpoints => 39 | { 40 | endpoints.MapGrpcService(); 41 | 42 | endpoints.MapGet("/", async context => 43 | { 44 | await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); 45 | }); 46 | }); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Grpc": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Discount/Discount.Grpc/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "DatabaseSettings": { 3 | "ConnectionString": "Server=localhost;Port=5432;Database=DiscountDb;User Id=admin;Password=admin1234;" 4 | }, 5 | "Serilog": { 6 | "MinimumLevel": { 7 | "Default": "Information", 8 | "Override": { 9 | "Microsoft": "Information", 10 | "System": "Warning" 11 | } 12 | } 13 | }, 14 | "ElasticConfiguration": { 15 | "Uri": "http://localhost:9200" 16 | }, 17 | "AllowedHosts": "*", 18 | "Kestrel": { 19 | "EndpointDefaults": { 20 | "Protocols": "Http2" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Services/Discount/Discount.UnitTests/Discount.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Services/Discount/Discount.UnitTests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Discount.UnitTests 5 | { 6 | public class UnitTest1 7 | { 8 | [Fact] 9 | public void Test1() 10 | { 11 | 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Controllers/OrderController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Ordering.Application.Features.Orders.Commands.CheckoutOrder; 5 | using Ordering.Application.Features.Orders.Commands.UpdateOrder; 6 | using Ordering.Application.Features.Orders.Commands.DeleteOrder; 7 | using Ordering.Application.Features.Orders.Queries.GetOrdersList; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Net; 11 | using System.Threading.Tasks; 12 | 13 | namespace Ordering.API.Controllers 14 | { 15 | [ApiController] 16 | [Route("api/v1/[controller]")] 17 | public class OrderController : ControllerBase 18 | { 19 | private readonly IMediator _mediator; 20 | 21 | public OrderController(IMediator mediator) 22 | { 23 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 24 | } 25 | 26 | [HttpGet("{userName}", Name = "GetOrder")] 27 | [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)] 28 | public async Task>> GetOrdersByUserName(string userName) 29 | { 30 | var query = new GetOrdersListQuery(userName); 31 | var orders = await _mediator.Send(query); 32 | return Ok(orders); 33 | } 34 | 35 | // testing purpose 36 | [HttpPost(Name = "CheckoutOrder")] 37 | [ProducesResponseType((int)HttpStatusCode.OK)] 38 | public async Task> CheckoutOrder([FromBody] CheckoutOrderCommand command) 39 | { 40 | var result = await _mediator.Send(command); 41 | return Ok(result); 42 | } 43 | 44 | [HttpPut(Name = "UpdateOrder")] 45 | [ProducesResponseType(StatusCodes.Status204NoContent)] 46 | [ProducesResponseType(StatusCodes.Status404NotFound)] 47 | [ProducesDefaultResponseType] 48 | public async Task UpdateOrder([FromBody] UpdateOrderCommand command) 49 | { 50 | await _mediator.Send(command); 51 | return NoContent(); 52 | } 53 | 54 | [HttpDelete("{id}", Name = "DeleteOrder")] 55 | [ProducesResponseType(StatusCodes.Status204NoContent)] 56 | [ProducesResponseType(StatusCodes.Status404NotFound)] 57 | [ProducesDefaultResponseType] 58 | public async Task DeleteOrder(int id) 59 | { 60 | var command = new DeleteOrderCommand() { Id = id }; 61 | await _mediator.Send(command); 62 | return NoContent(); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | 7 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 8 | WORKDIR /src 9 | COPY ["Services/Ordering/Ordering.API/Ordering.API.csproj", "Services/Ordering/Ordering.API/"] 10 | COPY ["Services/Ordering/Ordering.Application/Ordering.Application.csproj", "Services/Ordering/Ordering.Application/"] 11 | COPY ["Services/Ordering/Ordering.Domain/Ordering.Domain.csproj", "Services/Ordering/Ordering.Domain/"] 12 | COPY ["BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj", "BuildingBlocks/EventBus.Messages/"] 13 | COPY ["Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj", "Services/Ordering/Ordering.Infrastructure/"] 14 | COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"] 15 | RUN dotnet restore "Services/Ordering/Ordering.API/Ordering.API.csproj" 16 | COPY . . 17 | WORKDIR "/src/Services/Ordering/Ordering.API" 18 | RUN dotnet build "Ordering.API.csproj" -c Release -o /app/build 19 | 20 | FROM build AS publish 21 | RUN dotnet publish "Ordering.API.csproj" -c Release -o /app/publish 22 | 23 | FROM base AS final 24 | WORKDIR /app 25 | COPY --from=publish /app/publish . 26 | ENTRYPOINT ["dotnet", "Ordering.API.dll"] -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/EventBusConsumer/BasketCheckoutConsumer.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EventBus.Messages.Events; 3 | using MassTransit; 4 | using MediatR; 5 | using Microsoft.Extensions.Logging; 6 | using Ordering.Application.Features.Orders.Commands.CheckoutOrder; 7 | using System; 8 | using System.Threading.Tasks; 9 | 10 | namespace Ordering.API.EventBusConsumer 11 | { 12 | public class BasketCheckoutConsumer : IConsumer 13 | { 14 | private readonly IMediator _mediator; 15 | private readonly IMapper _mapper; 16 | private readonly ILogger _logger; 17 | 18 | public BasketCheckoutConsumer(IMediator mediator, IMapper mapper, ILogger logger) 19 | { 20 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 21 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 22 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 23 | } 24 | 25 | public async Task Consume(ConsumeContext context) 26 | { 27 | var command = _mapper.Map(context.Message); 28 | var result = await _mediator.Send(command); 29 | 30 | _logger.LogInformation("BasketCheckoutEvent consumed successfully. Created Order Id : {newOrderId}", result); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Extensions/HostExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Data.SqlClient; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using Polly; 7 | using System; 8 | 9 | namespace Ordering.API.Extensions 10 | { 11 | public static class HostExtensions 12 | { 13 | public static IHost MigrateDatabase(this IHost host, Action seeder) where TContext : DbContext 14 | { 15 | using (var scope = host.Services.CreateScope()) 16 | { 17 | var services = scope.ServiceProvider; 18 | var logger = services.GetRequiredService>(); 19 | var context = services.GetService(); 20 | 21 | try 22 | { 23 | logger.LogInformation("Migrating database associated with context {DbContextName}", typeof(TContext).Name); 24 | 25 | var retry = Policy.Handle() 26 | .WaitAndRetry( 27 | retryCount: 5, 28 | sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2,4,8,16,32 sc 29 | onRetry: (exception, retryCount, context) => 30 | { 31 | logger.LogError($"Retry {retryCount} of {context.PolicyKey} at {context.OperationKey}, due to: {exception}."); 32 | }); 33 | 34 | //if the sql server container is not created on run docker compose this 35 | //migration can't fail for network related exception. The retry options for DbContext only 36 | //apply to transient exceptions 37 | retry.Execute(() => InvokeSeeder(seeder, context, services)); 38 | 39 | logger.LogInformation("Migrated database associated with context {DbContextName}", typeof(TContext).Name); 40 | } 41 | catch (SqlException ex) 42 | { 43 | logger.LogError(ex, "An error occurred while migrating the database used on context {DbContextName}", typeof(TContext).Name); 44 | } 45 | } 46 | 47 | return host; 48 | } 49 | 50 | private static void InvokeSeeder(Action seeder, TContext context, IServiceProvider services) 51 | where TContext : DbContext 52 | { 53 | context.Database.Migrate(); 54 | seeder(context, services); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Mapper/OrderingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EventBus.Messages.Events; 3 | using Ordering.Application.Features.Orders.Commands.CheckoutOrder; 4 | 5 | namespace Ordering.API.Mapper 6 | { 7 | public class OrderingProfile : Profile 8 | { 9 | public OrderingProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Ordering.API.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | ..\..\..\docker-compose.dcproj 6 | Linux 7 | ..\..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Ordering.API.Extensions; 6 | using Ordering.Infrastructure.Persistence; 7 | using Serilog; 8 | using Common.Logging; 9 | 10 | namespace Ordering.API 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args) 17 | .Build() 18 | .MigrateDatabase((context, services) => 19 | { 20 | var logger = services.GetService>(); 21 | OrderContextSeed 22 | .SeedAsync(context, logger) 23 | .Wait(); 24 | }) 25 | .Run(); 26 | } 27 | 28 | public static IHostBuilder CreateHostBuilder(string[] args) => 29 | Host.CreateDefaultBuilder(args) 30 | .UseSerilog(SeriLogger.Configure) 31 | .ConfigureWebHostDefaults(webBuilder => 32 | { 33 | webBuilder.UseStartup(); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54291/", 7 | "sslPort": 44339 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Ordering.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "OrderingConnectionString": "Server=localhost;Database=OrderDb;User Id=sa;Password=SwN12345678;" 4 | }, 5 | "EmailSettings": { 6 | "FromAddress": "ezozkme@gmail.com", 7 | "ApiKey": "", 8 | "FromName": "Mehmet" 9 | }, 10 | "EventBusSettings": { 11 | "HostAddress": "amqp://guest:guest@localhost:5672" 12 | }, 13 | "Serilog": { 14 | "MinimumLevel": { 15 | "Default": "Information", 16 | "Override": { 17 | "Microsoft": "Information", 18 | "System": "Warning" 19 | } 20 | } 21 | }, 22 | "ElasticConfiguration": { 23 | "Uri": "http://localhost:9200" 24 | }, 25 | "AllowedHosts": "*" 26 | } 27 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/ApplicationServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using FluentValidation; 3 | using MediatR; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Ordering.Application.Behaviours; 6 | using System.Reflection; 7 | 8 | namespace Ordering.Application 9 | { 10 | public static class ApplicationServiceRegistration 11 | { 12 | public static IServiceCollection AddApplicationServices(this IServiceCollection services) 13 | { 14 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 15 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); 16 | services.AddMediatR(Assembly.GetExecutingAssembly()); 17 | 18 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); 19 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); 20 | 21 | return services; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Behaviours/UnhandledExceptionBehaviour.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ordering.Application.Behaviours 8 | { 9 | public class UnhandledExceptionBehaviour : IPipelineBehavior 10 | { 11 | private readonly ILogger _logger; 12 | 13 | public UnhandledExceptionBehaviour(ILogger logger) 14 | { 15 | _logger = logger; 16 | } 17 | 18 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 19 | { 20 | try 21 | { 22 | return await next(); 23 | } 24 | catch (Exception ex) 25 | { 26 | var requestName = typeof(TRequest).Name; 27 | _logger.LogError(ex, "Application Request: Unhandled Exception for Request {Name} {@Request}", requestName, request); 28 | throw; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Behaviours/ValidationBehaviour.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MediatR; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using ValidationException = Ordering.Application.Exceptions.ValidationException; 8 | 9 | namespace Ordering.Application.Behaviours 10 | { 11 | public class ValidationBehaviour : IPipelineBehavior 12 | where TRequest : IRequest 13 | { 14 | private readonly IEnumerable> _validators; 15 | 16 | public ValidationBehaviour(IEnumerable> validators) 17 | { 18 | _validators = validators; 19 | } 20 | 21 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 22 | { 23 | if (_validators.Any()) 24 | { 25 | var context = new ValidationContext(request); 26 | 27 | var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); 28 | var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 29 | 30 | if (failures.Count != 0) 31 | throw new ValidationException(failures); 32 | } 33 | return await next(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Contracts/Infrastructure/IEmailService.cs: -------------------------------------------------------------------------------- 1 | using Ordering.Application.Models; 2 | using System.Threading.Tasks; 3 | 4 | namespace Ordering.Application.Contracts.Infrastructure 5 | { 6 | public interface IEmailService 7 | { 8 | Task SendEmail(Email email); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Contracts/Persistence/IAsyncRepository.cs: -------------------------------------------------------------------------------- 1 | using Ordering.Domain.Common; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Linq.Expressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ordering.Application.Contracts.Persistence 9 | { 10 | public interface IAsyncRepository where T : EntityBase 11 | { 12 | Task> GetAllAsync(); 13 | Task> GetAsync(Expression> predicate); 14 | Task> GetAsync(Expression> predicate = null, 15 | Func, IOrderedQueryable> orderBy = null, 16 | string includeString = null, 17 | bool disableTracking = true); 18 | Task> GetAsync(Expression> predicate = null, 19 | Func, IOrderedQueryable> orderBy = null, 20 | List>> includes = null, 21 | bool disableTracking = true); 22 | Task GetByIdAsync(int id); 23 | Task AddAsync(T entity); 24 | Task UpdateAsync(T entity); 25 | Task DeleteAsync(T entity); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Contracts/Persistence/IOrderRepository.cs: -------------------------------------------------------------------------------- 1 | using Ordering.Domain.Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ordering.Application.Contracts.Persistence 9 | { 10 | public interface IOrderRepository : IAsyncRepository 11 | { 12 | Task> GetOrdersByUserName(string userName); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ordering.Application.Exceptions 4 | { 5 | public class NotFoundException : ApplicationException 6 | { 7 | public NotFoundException(string name, object key) 8 | : base($"Entity \"{name}\" ({key}) was not found.") 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Ordering.Application.Exceptions 7 | { 8 | public class ValidationException : ApplicationException 9 | { 10 | public ValidationException() 11 | : base("One or more validation failures have occurred.") 12 | { 13 | Errors = new Dictionary(); 14 | } 15 | 16 | public ValidationException(IEnumerable failures) 17 | : this() 18 | { 19 | Errors = failures 20 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage) 21 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); 22 | } 23 | 24 | public IDictionary Errors { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/CheckoutOrder/CheckoutOrderCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace Ordering.Application.Features.Orders.Commands.CheckoutOrder 4 | { 5 | public class CheckoutOrderCommand : IRequest 6 | { 7 | public string UserName { get; set; } 8 | public decimal TotalPrice { get; set; } 9 | 10 | // BillingAddress 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public string EmailAddress { get; set; } 14 | public string AddressLine { get; set; } 15 | public string Country { get; set; } 16 | public string State { get; set; } 17 | public string ZipCode { get; set; } 18 | 19 | // Payment 20 | public string CardName { get; set; } 21 | public string CardNumber { get; set; } 22 | public string Expiration { get; set; } 23 | public string CVV { get; set; } 24 | public int PaymentMethod { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/CheckoutOrder/CheckoutOrderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | using Ordering.Application.Contracts.Infrastructure; 5 | using Ordering.Application.Contracts.Persistence; 6 | using Ordering.Application.Models; 7 | using Ordering.Domain.Entities; 8 | using System; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace Ordering.Application.Features.Orders.Commands.CheckoutOrder 13 | { 14 | public class CheckoutOrderCommandHandler : IRequestHandler 15 | { 16 | private readonly IOrderRepository _orderRepository; 17 | private readonly IMapper _mapper; 18 | private readonly IEmailService _emailService; 19 | private readonly ILogger _logger; 20 | 21 | public CheckoutOrderCommandHandler(IOrderRepository orderRepository, IMapper mapper, IEmailService emailService, ILogger logger) 22 | { 23 | _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); 24 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 25 | _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService)); 26 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 27 | } 28 | 29 | public async Task Handle(CheckoutOrderCommand request, CancellationToken cancellationToken) 30 | { 31 | var orderEntity = _mapper.Map(request); 32 | var newOrder = await _orderRepository.AddAsync(orderEntity); 33 | 34 | _logger.LogInformation($"Order {newOrder.Id} is successfully created."); 35 | 36 | await SendMail(newOrder); 37 | 38 | return newOrder.Id; 39 | } 40 | 41 | private async Task SendMail(Order order) 42 | { 43 | var email = new Email() { To = "ezozkme@gmail.com", Body = $"Order was created.", Subject = "Order was created" }; 44 | 45 | try 46 | { 47 | await _emailService.SendEmail(email); 48 | } 49 | catch (Exception ex) 50 | { 51 | _logger.LogError($"Order {order.Id} failed due to an error with the mail service: {ex.Message}"); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/CheckoutOrder/CheckoutOrderCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Ordering.Application.Features.Orders.Commands.CheckoutOrder 4 | { 5 | public class CheckoutOrderCommandValidator : AbstractValidator 6 | { 7 | public CheckoutOrderCommandValidator() 8 | { 9 | RuleFor(p => p.UserName) 10 | .NotEmpty().WithMessage("{UserName} is required.") 11 | .NotNull() 12 | .MaximumLength(50).WithMessage("{UserName} must not exceed 50 characters."); 13 | 14 | RuleFor(p => p.EmailAddress) 15 | .NotEmpty().WithMessage("{EmailAddress} is required."); 16 | 17 | RuleFor(p => p.TotalPrice) 18 | .NotEmpty().WithMessage("{TotalPrice} is required.") 19 | .GreaterThan(0).WithMessage("{TotalPrice} should be greater than zero."); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/DeleteOrder/DeleteOrderCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace Ordering.Application.Features.Orders.Commands.DeleteOrder 4 | { 5 | public class DeleteOrderCommand : IRequest 6 | { 7 | public int Id { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/DeleteOrder/DeleteOrderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | using Ordering.Application.Contracts.Persistence; 5 | using Ordering.Application.Exceptions; 6 | using Ordering.Domain.Entities; 7 | using System; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Ordering.Application.Features.Orders.Commands.DeleteOrder 12 | { 13 | public class DeleteOrderCommandHandler : IRequestHandler 14 | { 15 | private readonly IOrderRepository _orderRepository; 16 | private readonly IMapper _mapper; 17 | private readonly ILogger _logger; 18 | 19 | public DeleteOrderCommandHandler(IOrderRepository orderRepository, IMapper mapper, ILogger logger) 20 | { 21 | _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); 22 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 23 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | public async Task Handle(DeleteOrderCommand request, CancellationToken cancellationToken) 27 | { 28 | var orderToDelete = await _orderRepository.GetByIdAsync(request.Id); 29 | if (orderToDelete == null) 30 | { 31 | throw new NotFoundException(nameof(Order), request.Id); 32 | } 33 | 34 | await _orderRepository.DeleteAsync(orderToDelete); 35 | 36 | _logger.LogInformation($"Order {orderToDelete.Id} is successfully deleted."); 37 | 38 | return Unit.Value; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/UpdateOrder/UpdateOrderCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace Ordering.Application.Features.Orders.Commands.UpdateOrder 4 | { 5 | public class UpdateOrderCommand : IRequest 6 | { 7 | public int Id { get; set; } 8 | public string UserName { get; set; } 9 | public decimal TotalPrice { get; set; } 10 | 11 | // BillingAddress 12 | public string FirstName { get; set; } 13 | public string LastName { get; set; } 14 | public string EmailAddress { get; set; } 15 | public string AddressLine { get; set; } 16 | public string Country { get; set; } 17 | public string State { get; set; } 18 | public string ZipCode { get; set; } 19 | 20 | // Payment 21 | public string CardName { get; set; } 22 | public string CardNumber { get; set; } 23 | public string Expiration { get; set; } 24 | public string CVV { get; set; } 25 | public int PaymentMethod { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/UpdateOrder/UpdateOrderCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | using Ordering.Application.Contracts.Persistence; 5 | using Ordering.Application.Exceptions; 6 | using Ordering.Domain.Entities; 7 | using System; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Ordering.Application.Features.Orders.Commands.UpdateOrder 12 | { 13 | public class UpdateOrderCommandHandler : IRequestHandler 14 | { 15 | private readonly IOrderRepository _orderRepository; 16 | private readonly IMapper _mapper; 17 | private readonly ILogger _logger; 18 | 19 | public UpdateOrderCommandHandler(IOrderRepository orderRepository, IMapper mapper, ILogger logger) 20 | { 21 | _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); 22 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 23 | _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | public async Task Handle(UpdateOrderCommand request, CancellationToken cancellationToken) 27 | { 28 | var orderToUpdate = await _orderRepository.GetByIdAsync(request.Id); 29 | if (orderToUpdate == null) 30 | { 31 | throw new NotFoundException(nameof(Order), request.Id); 32 | } 33 | 34 | _mapper.Map(request, orderToUpdate, typeof(UpdateOrderCommand), typeof(Order)); 35 | 36 | await _orderRepository.UpdateAsync(orderToUpdate); 37 | 38 | _logger.LogInformation($"Order {orderToUpdate.Id} is successfully updated."); 39 | 40 | return Unit.Value; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Commands/UpdateOrder/UpdateOrderCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Ordering.Application.Features.Orders.Commands.UpdateOrder 4 | { 5 | public class UpdateOrderCommandValidator : AbstractValidator 6 | { 7 | public UpdateOrderCommandValidator() 8 | { 9 | RuleFor(p => p.UserName) 10 | .NotEmpty().WithMessage("{UserName} is required.") 11 | .NotNull() 12 | .MaximumLength(50).WithMessage("{UserName} must not exceed 50 characters."); 13 | 14 | RuleFor(p => p.EmailAddress) 15 | .NotEmpty().WithMessage("{EmailAddress} is required."); 16 | 17 | RuleFor(p => p.TotalPrice) 18 | .NotEmpty().WithMessage("{TotalPrice} is required.") 19 | .GreaterThan(0).WithMessage("{TotalPrice} should be greater than zero."); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Queries/GetOrdersList/GetOrdersListQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace Ordering.Application.Features.Orders.Queries.GetOrdersList 6 | { 7 | public class GetOrdersListQuery : IRequest> 8 | { 9 | public string UserName { get; set; } 10 | 11 | public GetOrdersListQuery(string userName) 12 | { 13 | UserName = userName ?? throw new ArgumentNullException(nameof(userName)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Queries/GetOrdersList/GetOrdersListQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using MediatR; 3 | using Ordering.Application.Contracts.Persistence; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ordering.Application.Features.Orders.Queries.GetOrdersList 10 | { 11 | public class GetOrdersListQueryHandler : IRequestHandler> 12 | { 13 | private readonly IOrderRepository _orderRepository; 14 | private readonly IMapper _mapper; 15 | 16 | public GetOrdersListQueryHandler(IOrderRepository orderRepository, IMapper mapper) 17 | { 18 | _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); 19 | _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); 20 | } 21 | 22 | public async Task> Handle(GetOrdersListQuery request, CancellationToken cancellationToken) 23 | { 24 | var orderList = await _orderRepository.GetOrdersByUserName(request.UserName); 25 | return _mapper.Map>(orderList); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Features/Orders/Queries/GetOrdersList/OrdersVm.cs: -------------------------------------------------------------------------------- 1 | namespace Ordering.Application.Features.Orders.Queries.GetOrdersList 2 | { 3 | public class OrdersVm 4 | { 5 | public int Id { get; set; } 6 | public string UserName { get; set; } 7 | public decimal TotalPrice { get; set; } 8 | 9 | // BillingAddress 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public string EmailAddress { get; set; } 13 | public string AddressLine { get; set; } 14 | public string Country { get; set; } 15 | public string State { get; set; } 16 | public string ZipCode { get; set; } 17 | 18 | // Payment 19 | public string CardName { get; set; } 20 | public string CardNumber { get; set; } 21 | public string Expiration { get; set; } 22 | public string CVV { get; set; } 23 | public int PaymentMethod { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Mappings/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Ordering.Application.Features.Orders.Commands.CheckoutOrder; 3 | using Ordering.Application.Features.Orders.Commands.UpdateOrder; 4 | using Ordering.Application.Features.Orders.Queries.GetOrdersList; 5 | using Ordering.Domain.Entities; 6 | 7 | namespace Ordering.Application.Mappings 8 | { 9 | public class MappingProfile : Profile 10 | { 11 | public MappingProfile() 12 | { 13 | CreateMap().ReverseMap(); 14 | CreateMap().ReverseMap(); 15 | CreateMap().ReverseMap(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Models/Email.cs: -------------------------------------------------------------------------------- 1 | namespace Ordering.Application.Models 2 | { 3 | public class Email 4 | { 5 | public string To { get; set; } 6 | public string Subject { get; set; } 7 | public string Body { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Models/EmailSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Ordering.Application.Models 2 | { 3 | public class EmailSettings 4 | { 5 | public string ApiKey { get; set; } 6 | public string FromAddress { get; set; } 7 | public string FromName { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Application/Ordering.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Domain/Common/EntityBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Ordering.Domain.Common 4 | { 5 | public abstract class EntityBase 6 | { 7 | public int Id { get; protected set; } 8 | public string CreatedBy { get; set; } 9 | public DateTime CreatedDate { get; set; } 10 | public string LastModifiedBy { get; set; } 11 | public DateTime? LastModifiedDate { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Domain/Common/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Ordering.Domain.Common 5 | { 6 | // Learn more: https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/microservice-ddd-cqrs-patterns/implement-value-objects 7 | public abstract class ValueObject 8 | { 9 | protected static bool EqualOperator(ValueObject left, ValueObject right) 10 | { 11 | if (left is null ^ right is null) 12 | { 13 | return false; 14 | } 15 | 16 | return left?.Equals(right) != false; 17 | } 18 | 19 | protected static bool NotEqualOperator(ValueObject left, ValueObject right) 20 | { 21 | return !(EqualOperator(left, right)); 22 | } 23 | 24 | protected abstract IEnumerable GetEqualityComponents(); 25 | 26 | public override bool Equals(object obj) 27 | { 28 | if (obj == null || obj.GetType() != GetType()) 29 | { 30 | return false; 31 | } 32 | 33 | var other = (ValueObject)obj; 34 | return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 35 | } 36 | 37 | public override int GetHashCode() 38 | { 39 | return GetEqualityComponents() 40 | .Select(x => x != null ? x.GetHashCode() : 0) 41 | .Aggregate((x, y) => x ^ y); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Domain/Entities/Order.cs: -------------------------------------------------------------------------------- 1 | using Ordering.Domain.Common; 2 | 3 | namespace Ordering.Domain.Entities 4 | { 5 | public class Order : EntityBase 6 | { 7 | public string UserName { get; set; } 8 | public decimal TotalPrice { get; set; } 9 | 10 | // BillingAddress 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public string EmailAddress { get; set; } 14 | public string AddressLine { get; set; } 15 | public string Country { get; set; } 16 | public string State { get; set; } 17 | public string ZipCode { get; set; } 18 | 19 | // Payment 20 | public string CardName { get; set; } 21 | public string CardNumber { get; set; } 22 | public string Expiration { get; set; } 23 | public string CVV { get; set; } 24 | public int PaymentMethod { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Domain/Ordering.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/InfrastructureServiceRegistration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Ordering.Application.Contracts.Infrastructure; 5 | using Ordering.Application.Contracts.Persistence; 6 | using Ordering.Application.Models; 7 | using Ordering.Infrastructure.Mail; 8 | using Ordering.Infrastructure.Persistence; 9 | using Ordering.Infrastructure.Repositories; 10 | 11 | namespace Ordering.Infrastructure 12 | { 13 | public static class InfrastructureServiceRegistration 14 | { 15 | public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) 16 | { 17 | services.AddDbContext(options => 18 | options.UseSqlServer(configuration.GetConnectionString("OrderingConnectionString"))); 19 | 20 | services.AddScoped(typeof(IAsyncRepository<>), typeof(RepositoryBase<>)); 21 | services.AddScoped(); 22 | 23 | services.Configure(c => configuration.GetSection("EmailSettings")); 24 | services.AddTransient(); 25 | 26 | return services; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Mail/EmailService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Microsoft.Extensions.Options; 3 | using Ordering.Application.Contracts.Infrastructure; 4 | using Ordering.Application.Models; 5 | using SendGrid; 6 | using SendGrid.Helpers.Mail; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ordering.Infrastructure.Mail 10 | { 11 | public class EmailService : IEmailService 12 | { 13 | public EmailSettings _emailSettings { get; } 14 | public ILogger _logger { get; } 15 | 16 | public EmailService(IOptions mailSettings, ILogger logger) 17 | { 18 | _emailSettings = mailSettings.Value; 19 | _logger = logger; 20 | } 21 | 22 | public async Task SendEmail(Email email) 23 | { 24 | var client = new SendGridClient(_emailSettings.ApiKey); 25 | 26 | var subject = email.Subject; 27 | var to = new EmailAddress(email.To); 28 | var emailBody = email.Body; 29 | 30 | var from = new EmailAddress 31 | { 32 | Email = _emailSettings.FromAddress, 33 | Name = _emailSettings.FromName 34 | }; 35 | 36 | var sendGridMessage = MailHelper.CreateSingleEmail(from, to, subject, emailBody, emailBody); 37 | var response = await client.SendEmailAsync(sendGridMessage); 38 | 39 | _logger.LogInformation("Email sent."); 40 | 41 | if (response.StatusCode == System.Net.HttpStatusCode.Accepted || response.StatusCode == System.Net.HttpStatusCode.OK) 42 | return true; 43 | 44 | _logger.LogError("Email sending failed."); 45 | 46 | return false; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Migrations/20210213134039_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Ordering.Infrastructure.Migrations 5 | { 6 | public partial class InitialCreate : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Orders", 12 | columns: table => new 13 | { 14 | Id = table.Column(type: "int", nullable: false) 15 | .Annotation("SqlServer:Identity", "1, 1"), 16 | UserName = table.Column(type: "nvarchar(max)", nullable: true), 17 | TotalPrice = table.Column(type: "decimal(18,2)", nullable: false), 18 | FirstName = table.Column(type: "nvarchar(max)", nullable: true), 19 | LastName = table.Column(type: "nvarchar(max)", nullable: true), 20 | EmailAddress = table.Column(type: "nvarchar(max)", nullable: true), 21 | AddressLine = table.Column(type: "nvarchar(max)", nullable: true), 22 | Country = table.Column(type: "nvarchar(max)", nullable: true), 23 | State = table.Column(type: "nvarchar(max)", nullable: true), 24 | ZipCode = table.Column(type: "nvarchar(max)", nullable: true), 25 | CardName = table.Column(type: "nvarchar(max)", nullable: true), 26 | CardNumber = table.Column(type: "nvarchar(max)", nullable: true), 27 | Expiration = table.Column(type: "nvarchar(max)", nullable: true), 28 | CVV = table.Column(type: "nvarchar(max)", nullable: true), 29 | PaymentMethod = table.Column(type: "int", nullable: false), 30 | CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), 31 | CreatedDate = table.Column(type: "datetime2", nullable: false), 32 | LastModifiedBy = table.Column(type: "nvarchar(max)", nullable: true), 33 | LastModifiedDate = table.Column(type: "datetime2", nullable: true) 34 | }, 35 | constraints: table => 36 | { 37 | table.PrimaryKey("PK_Orders", x => x.Id); 38 | }); 39 | } 40 | 41 | protected override void Down(MigrationBuilder migrationBuilder) 42 | { 43 | migrationBuilder.DropTable( 44 | name: "Orders"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Ordering.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Persistence/OrderContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Ordering.Domain.Common; 3 | using Ordering.Domain.Entities; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Ordering.Infrastructure.Persistence 9 | { 10 | public class OrderContext : DbContext 11 | { 12 | public OrderContext(DbContextOptions options) : base(options) 13 | { 14 | } 15 | 16 | public DbSet Orders { get; set; } 17 | 18 | public override Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) 19 | { 20 | foreach (var entry in ChangeTracker.Entries()) 21 | { 22 | switch (entry.State) 23 | { 24 | case EntityState.Added: 25 | entry.Entity.CreatedDate = DateTime.Now; 26 | entry.Entity.CreatedBy = "swn"; 27 | break; 28 | case EntityState.Modified: 29 | entry.Entity.LastModifiedDate = DateTime.Now; 30 | entry.Entity.LastModifiedBy = "swn"; 31 | break; 32 | } 33 | } 34 | return base.SaveChangesAsync(cancellationToken); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Persistence/OrderContextSeed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using Ordering.Domain.Entities; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Ordering.Infrastructure.Persistence 8 | { 9 | public class OrderContextSeed 10 | { 11 | public static async Task SeedAsync(OrderContext orderContext, ILogger logger) 12 | { 13 | if (!orderContext.Orders.Any()) 14 | { 15 | orderContext.Orders.AddRange(GetPreconfiguredOrders()); 16 | await orderContext.SaveChangesAsync(); 17 | logger.LogInformation("Seed database associated with context {DbContextName}", typeof(OrderContext).Name); 18 | } 19 | } 20 | 21 | private static IEnumerable GetPreconfiguredOrders() 22 | { 23 | return new List 24 | { 25 | new Order() {UserName = "swn", FirstName = "Mehmet", LastName = "Ozkaya", EmailAddress = "ezozkme@gmail.com", AddressLine = "Bahcelievler", Country = "Turkey", TotalPrice = 350 } 26 | }; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Services/Ordering/Ordering.Infrastructure/Repositories/OrderRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Ordering.Application.Contracts.Persistence; 3 | using Ordering.Domain.Entities; 4 | using Ordering.Infrastructure.Persistence; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Ordering.Infrastructure.Repositories 10 | { 11 | public class OrderRepository : RepositoryBase, IOrderRepository 12 | { 13 | public OrderRepository(OrderContext dbContext) : base(dbContext) 14 | { 15 | } 16 | 17 | public async Task> GetOrdersByUserName(string userName) 18 | { 19 | var orderList = await _dbContext.Orders 20 | .Where(o => o.UserName == userName) 21 | .ToListAsync(); 22 | return orderList; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/AspnetRunBasics.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | cd5124f0-00bb-4cff-b8b9-dc4e11396fa3 5 | Linux 6 | ..\..\docker-compose.dcproj 7 | ..\.. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build 9 | WORKDIR /src 10 | COPY ["WebApps/AspnetRunBasics/AspnetRunBasics.csproj", "WebApps/AspnetRunBasics/"] 11 | RUN dotnet restore "WebApps/AspnetRunBasics/AspnetRunBasics.csproj" 12 | COPY . . 13 | WORKDIR "/src/WebApps/AspnetRunBasics" 14 | RUN dotnet build "AspnetRunBasics.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "AspnetRunBasics.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | COPY --from=publish /app/publish . 22 | ENTRYPOINT ["dotnet", "AspnetRunBasics.dll"] 23 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Extensions/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | 7 | namespace AspnetRunBasics.Extensions 8 | { 9 | public static class HttpClientExtensions 10 | { 11 | public static async Task ReadContentAs(this HttpResponseMessage response) 12 | { 13 | if (!response.IsSuccessStatusCode) 14 | throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}"); 15 | 16 | var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 17 | 18 | return JsonSerializer.Deserialize(dataAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); 19 | } 20 | 21 | public static Task PostAsJson(this HttpClient httpClient, string url, T data) 22 | { 23 | var dataAsString = JsonSerializer.Serialize(data); 24 | var content = new StringContent(dataAsString); 25 | content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); 26 | 27 | return httpClient.PostAsync(url, content); 28 | } 29 | 30 | public static Task PutAsJson(this HttpClient httpClient, string url, T data) 31 | { 32 | var dataAsString = JsonSerializer.Serialize(data); 33 | var content = new StringContent(dataAsString); 34 | content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); 35 | 36 | return httpClient.PutAsync(url, content); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Models/BasketCheckoutModel.cs: -------------------------------------------------------------------------------- 1 | namespace AspnetRunBasics.Models 2 | { 3 | public class BasketCheckoutModel 4 | { 5 | public string UserName { get; set; } 6 | public decimal TotalPrice { get; set; } 7 | 8 | // BillingAddress 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public string EmailAddress { get; set; } 12 | public string AddressLine { get; set; } 13 | public string Country { get; set; } 14 | public string State { get; set; } 15 | public string ZipCode { get; set; } 16 | 17 | // Payment 18 | public string CardName { get; set; } 19 | public string CardNumber { get; set; } 20 | public string Expiration { get; set; } 21 | public string CVV { get; set; } 22 | public int PaymentMethod { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Models/BasketItemModel.cs: -------------------------------------------------------------------------------- 1 | namespace AspnetRunBasics.Models 2 | { 3 | public class BasketItemModel 4 | { 5 | public int Quantity { get; set; } 6 | public string Color { get; set; } 7 | public decimal Price { get; set; } 8 | public string ProductId { get; set; } 9 | public string ProductName { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Models/BasketModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AspnetRunBasics.Models 4 | { 5 | public class BasketModel 6 | { 7 | public string UserName { get; set; } 8 | public List Items { get; set; } = new List(); 9 | public decimal TotalPrice { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Models/CatalogModel.cs: -------------------------------------------------------------------------------- 1 | namespace AspnetRunBasics.Models 2 | { 3 | public class CatalogModel 4 | { 5 | public string Id { get; set; } 6 | public string Name { get; set; } 7 | public string Category { get; set; } 8 | public string Summary { get; set; } 9 | public string Description { get; set; } 10 | public string ImageFile { get; set; } 11 | public decimal Price { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Models/OrderResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace AspnetRunBasics.Models 2 | { 3 | public class OrderResponseModel 4 | { 5 | public string UserName { get; set; } 6 | public decimal TotalPrice { get; set; } 7 | 8 | // BillingAddress 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | public string EmailAddress { get; set; } 12 | public string AddressLine { get; set; } 13 | public string Country { get; set; } 14 | public string State { get; set; } 15 | public string ZipCode { get; set; } 16 | 17 | // Payment 18 | public string CardName { get; set; } 19 | public string CardNumber { get; set; } 20 | public string Expiration { get; set; } 21 | public string CVV { get; set; } 22 | public int PaymentMethod { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Cart.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using AspnetRunBasics.Models; 5 | using AspnetRunBasics.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace AspnetRunBasics 10 | { 11 | public class CartModel : PageModel 12 | { 13 | private readonly IBasketService _basketService; 14 | 15 | public CartModel(IBasketService basketService) 16 | { 17 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 18 | } 19 | 20 | public BasketModel Cart { get; set; } = new BasketModel(); 21 | 22 | public async Task OnGetAsync() 23 | { 24 | var userName = "swn"; 25 | Cart = await _basketService.GetBasket(userName); 26 | 27 | return Page(); 28 | } 29 | 30 | public async Task OnPostRemoveToCartAsync(string productId) 31 | { 32 | var userName = "swn"; 33 | var basket = await _basketService.GetBasket(userName); 34 | 35 | var item = basket.Items.Single(x => x.ProductId == productId); 36 | basket.Items.Remove(item); 37 | 38 | var basketUpdated = await _basketService.UpdateBasket(basket); 39 | 40 | return RedirectToPage(); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/CheckOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AspnetRunBasics.Models; 4 | using AspnetRunBasics.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace AspnetRunBasics 9 | { 10 | public class CheckOutModel : PageModel 11 | { 12 | private readonly IBasketService _basketService; 13 | private readonly IOrderService _orderService; 14 | 15 | public CheckOutModel(IBasketService basketService, IOrderService orderService) 16 | { 17 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 18 | _orderService = orderService ?? throw new ArgumentNullException(nameof(orderService)); 19 | } 20 | 21 | [BindProperty] 22 | public BasketCheckoutModel Order { get; set; } 23 | 24 | public BasketModel Cart { get; set; } = new BasketModel(); 25 | 26 | public async Task OnGetAsync() 27 | { 28 | var userName = "swn"; 29 | Cart = await _basketService.GetBasket(userName); 30 | 31 | return Page(); 32 | } 33 | 34 | public async Task OnPostCheckOutAsync() 35 | { 36 | var userName = "swn"; 37 | Cart = await _basketService.GetBasket(userName); 38 | 39 | if (!ModelState.IsValid) 40 | { 41 | return Page(); 42 | } 43 | 44 | Order.UserName = userName; 45 | Order.TotalPrice = Cart.TotalPrice; 46 | 47 | await _basketService.CheckoutBasket(Order); 48 | 49 | return RedirectToPage("Confirmation", "OrderSubmitted"); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Confirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AspnetRunBasics.ConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Confirmation"; 5 | } 6 | 7 | 8 | 9 | Confirmation 10 | 11 | 12 | @if (!string.IsNullOrEmpty(Model.Message)) 13 | { 14 | @Model.Message 15 | } 16 | 17 | If you have any further questions, you can contact us 501-222-2222. 18 | 19 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Confirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace AspnetRunBasics 4 | { 5 | public class ConfirmationModel : PageModel 6 | { 7 | public string Message { get; set; } 8 | 9 | public void OnGetContact() 10 | { 11 | Message = "Your email was sent."; 12 | } 13 | 14 | public void OnGetOrderSubmitted() 15 | { 16 | Message = "Your order submitted successfully."; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AspnetRunBasics.ContactModel 3 | @{ 4 | ViewData["Title"] = "Contact"; 5 | } 6 | 7 | 8 | 9 | E-COMMERCE CONTACT 10 | Contact Page build with Bootstrap 4 ! 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | Home 19 | Contact 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Contact us. 31 | 32 | 33 | 34 | 35 | Name 36 | 37 | 38 | 39 | Email address 40 | 41 | We'll never share your email with anyone else. 42 | 43 | 44 | Message 45 | 46 | 47 | 48 | Submit 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | Address 57 | 58 | 3 rue des Champs Elysées 59 | 75008 PARIS 60 | France 61 | Email : email@example.com 62 | Tel. +33 12 56 11 51 84 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Contact.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace AspnetRunBasics 9 | { 10 | public class ContactModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 | Error. 8 | An error occurred while processing your request. 9 | 10 | @if (Model.ShowRequestId) 11 | { 12 | 13 | Request ID: @Model.RequestId 14 | 15 | } 16 | 17 | Development Mode 18 | 19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 | 21 | 22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 | 27 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace AspnetRunBasics.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | public string RequestId { get; set; } 16 | 17 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 18 | 19 | private readonly ILogger _logger; 20 | 21 | public ErrorModel(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AspnetRunBasics.Models; 5 | using AspnetRunBasics.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace AspnetRunBasics.Pages 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | private readonly ICatalogService _catalogService; 14 | private readonly IBasketService _basketService; 15 | 16 | public IndexModel(ICatalogService catalogService, IBasketService basketService) 17 | { 18 | _catalogService = catalogService ?? throw new ArgumentNullException(nameof(catalogService)); 19 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 20 | } 21 | 22 | public IEnumerable ProductList { get; set; } = new List(); 23 | 24 | public async Task OnGetAsync() 25 | { 26 | ProductList = await _catalogService.GetCatalog(); 27 | return Page(); 28 | } 29 | 30 | public async Task OnPostAddToCartAsync(string productId) 31 | { 32 | var product = await _catalogService.GetCatalog(productId); 33 | 34 | var userName = "swn"; 35 | var basket = await _basketService.GetBasket(userName); 36 | 37 | basket.Items.Add(new BasketItemModel 38 | { 39 | ProductId = productId, 40 | ProductName = product.Name, 41 | Price = product.Price, 42 | Quantity = 1, 43 | Color = "Black" 44 | }); 45 | 46 | var basketUpdated = await _basketService.UpdateBasket(basket); 47 | return RedirectToPage("Cart"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Order.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AspnetRunBasics.OrderModel 3 | @{ 4 | ViewData["Title"] = "Order"; 5 | } 6 | 7 | 8 | 9 | 10 | 11 | 12 | Home 13 | Order 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | First Name 29 | Last Name 30 | Email 31 | Address 32 | TotalPrice 33 | 34 | 35 | 36 | 37 | @foreach (var order in Model.Orders) 38 | { 39 | 40 | 41 | @order.FirstName 42 | @order.LastName 43 | @order.EmailAddress 44 | @order.AddressLine 45 | @order.TotalPrice $ 46 | 47 | } 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | Continue Shopping 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Order.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using AspnetRunBasics.Models; 5 | using AspnetRunBasics.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | 9 | namespace AspnetRunBasics 10 | { 11 | public class OrderModel : PageModel 12 | { 13 | private readonly IOrderService _orderService; 14 | 15 | public OrderModel(IOrderService orderService) 16 | { 17 | _orderService = orderService ?? throw new ArgumentNullException(nameof(orderService)); 18 | } 19 | 20 | public IEnumerable Orders { get; set; } = new List(); 21 | 22 | public async Task OnGetAsync() 23 | { 24 | Orders = await _orderService.GetOrdersByUserName("swn"); 25 | 26 | return Page(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Privacy.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model PrivacyModel 3 | @{ 4 | ViewData["Title"] = "Privacy Policy"; 5 | } 6 | @ViewData["Title"] 7 | 8 | Use this page to detail your site's privacy policy. 9 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Privacy.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace AspnetRunBasics.Pages 10 | { 11 | public class PrivacyModel : PageModel 12 | { 13 | private readonly ILogger _logger; 14 | 15 | public PrivacyModel(ILogger logger) 16 | { 17 | _logger = logger; 18 | } 19 | 20 | public void OnGet() 21 | { 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Product.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AspnetRunBasics.Models; 6 | using AspnetRunBasics.Services; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | 10 | namespace AspnetRunBasics 11 | { 12 | public class ProductModel : PageModel 13 | { 14 | private readonly ICatalogService _catalogService; 15 | private readonly IBasketService _basketService; 16 | 17 | public ProductModel(ICatalogService catalogService, IBasketService basketService) 18 | { 19 | _catalogService = catalogService ?? throw new ArgumentNullException(nameof(catalogService)); 20 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 21 | } 22 | 23 | public IEnumerable CategoryList { get; set; } = new List(); 24 | public IEnumerable ProductList { get; set; } = new List(); 25 | 26 | 27 | [BindProperty(SupportsGet = true)] 28 | public string SelectedCategory { get; set; } 29 | 30 | public async Task OnGetAsync(string categoryName) 31 | { 32 | var productList = await _catalogService.GetCatalog(); 33 | CategoryList = productList.Select(p => p.Category).Distinct(); 34 | 35 | if (!string.IsNullOrWhiteSpace(categoryName)) 36 | { 37 | ProductList = productList.Where(p => p.Category == categoryName); 38 | SelectedCategory = categoryName; 39 | } 40 | else 41 | { 42 | ProductList = productList; 43 | } 44 | 45 | return Page(); 46 | } 47 | 48 | public async Task OnPostAddToCartAsync(string productId) 49 | { 50 | var product = await _catalogService.GetCatalog(productId); 51 | 52 | var userName = "swn"; 53 | var basket = await _basketService.GetBasket(userName); 54 | 55 | basket.Items.Add(new BasketItemModel 56 | { 57 | ProductId = productId, 58 | ProductName = product.Name, 59 | Price = product.Price, 60 | Quantity = 1, 61 | Color = "Black" 62 | }); 63 | 64 | var basketUpdated = await _basketService.UpdateBasket(basket); 65 | 66 | return RedirectToPage("Cart"); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/ProductDetail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AspnetRunBasics.Models; 4 | using AspnetRunBasics.Services; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace AspnetRunBasics 9 | { 10 | public class ProductDetailModel : PageModel 11 | { 12 | private readonly ICatalogService _catalogService; 13 | private readonly IBasketService _basketService; 14 | 15 | public ProductDetailModel(ICatalogService catalogService, IBasketService basketService) 16 | { 17 | _catalogService = catalogService ?? throw new ArgumentNullException(nameof(catalogService)); 18 | _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService)); 19 | } 20 | 21 | public CatalogModel Product { get; set; } 22 | 23 | [BindProperty] 24 | public string Color { get; set; } 25 | 26 | [BindProperty] 27 | public int Quantity { get; set; } 28 | 29 | public async Task OnGetAsync(string productId) 30 | { 31 | if (productId == null) 32 | { 33 | return NotFound(); 34 | } 35 | 36 | Product = await _catalogService.GetCatalog(productId); 37 | if (Product == null) 38 | { 39 | return NotFound(); 40 | } 41 | return Page(); 42 | } 43 | 44 | public async Task OnPostAddToCartAsync(string productId) 45 | { 46 | var product = await _catalogService.GetCatalog(productId); 47 | 48 | var userName = "swn"; 49 | var basket = await _basketService.GetBasket(userName); 50 | 51 | basket.Items.Add(new BasketItemModel 52 | { 53 | ProductId = productId, 54 | ProductName = product.Name, 55 | Price = product.Price, 56 | Quantity = Quantity, 57 | Color = Color 58 | }); 59 | 60 | var basketUpdated = await _basketService.UpdateBasket(basket); 61 | 62 | return RedirectToPage("Cart"); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /WebApps/AspnetRunBasics/Pages/Shared/_ProductItemPartial.cshtml: -------------------------------------------------------------------------------- 1 | @model CatalogModel 2 | 3 | 4 |
If you have any further questions, you can contact us 501-222-2222.
Contact Page build with Bootstrap 4 !
3 rue des Champs Elysées
75008 PARIS
France
Email : email@example.com
Tel. +33 12 56 11 51 84
13 | Request ID: @Model.RequestId 14 |
@Model.RequestId
19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |
22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |
Use this page to detail your site's privacy policy.