diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 805342af..00000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1 +0,0 @@
-custom: ["https://aspnetrun.azurewebsites.net/DownloadBook"]
diff --git a/README.md b/README.md
index aefb7f9d..21377881 100644
--- a/README.md
+++ b/README.md
@@ -1,28 +1,30 @@
-
-[](https://www.udemy.com/course/microservices-architecture-and-implementation-on-dotnet/?couponCode=JULY2021)
-
-**UDEMY COURSE WITH DISCOUNTED - Step by Step Development of this Repository -> https://www.udemy.com/course/microservices-architecture-and-implementation-on-dotnet/?couponCode=JULY2021**
+**UDEMY COURSE WITH DISCOUNTED - Step by Step Development of this Repository -> https://www.udemy.com/course/microservices-architecture-and-implementation-on-dotnet/?couponCode=MAYY25**
See the overall picture of **implementations on microservices with .net tools** on real-world **e-commerce microservices** project;
-
+
+
+There is a couple of microservices which implemented **e-commerce** modules over **Catalog, Basket, Discount** and **Ordering** microservices with **NoSQL (DocumentDb, Redis)** and **Relational databases (PostgreSQL, Sql Server)** with communicating over **RabbitMQ Event Driven Communication** and using **Yarp API Gateway**.
-There is a couple of microservices which implemented **e-commerce** modules over **Catalog, Basket, Discount** and **Ordering** microservices with **NoSQL (MongoDB, Redis)** and **Relational databases (PostgreSQL, Sql Server)** with communicating over **RabbitMQ Event Driven Communication** and using **Ocelot API Gateway**.
+### Check Explanation of this Repository on Medium
+* [.NET 8 Microservices: DDD, CQRS, Vertical/Clean Architecture and Event-Driven Communication](https://medium.com/@mehmetozkaya/net-8-microservices-ddd-cqrs-vertical-clean-architecture-2dd7ebaaf4bd)
## Whats Including In This Repository
We have implemented below **features over the run-aspnetcore-microservices repository**.
#### Catalog microservice which includes;
-* ASP.NET Core Web API application
-* REST API principles, CRUD operations
-* **MongoDB database** connection and containerization
-* Repository Pattern Implementation
-* Swagger Open API implementation
+* ASP.NET Core Minimal APIs and latest features of .NET8 and C# 12
+* **Vertical Slice Architecture** implementation with Feature folders and single .cs file includes different classes in one file
+* CQRS implementation using MediatR library
+* CQRS Validation Pipeline Behaviors with MediatR and FluentValidation
+* Use Marten library for .NET Transactional Document DB on PostgreSQL
+* Use Carter for Minimal API endpoint definition
+* Cross-cutting concerns Logging, Global Exception Handling and Health Checks
#### Basket microservice which includes;
-* ASP.NET Web API application
-* REST API principles, CRUD operations
-* **Redis database** connection and containerization
+* ASP.NET 8 Web API application, Following REST API principles, CRUD
+* Using **Redis** as a **Distributed Cache** over basketdb
+* Implements Proxy, Decorator and Cache-aside patterns
* Consume Discount **Grpc Service** for inter-service sync communication to calculate product final price
* Publish BasketCheckout Queue with using **MassTransit and RabbitMQ**
@@ -30,8 +32,8 @@ We have implemented below **features over the run-aspnetcore-microservices repos
* ASP.NET **Grpc Server** application
* Build a Highly Performant **inter-service gRPC Communication** with Basket Microservice
* Exposing Grpc Services with creating **Protobuf messages**
-* Using **Dapper for micro-orm implementation** to simplify data access and ensure high performance
-* **PostgreSQL database** connection and containerization
+* Entity Framework Core ORM — SQLite Data Provider and Migrations to simplify data access and ensure high performance
+* **SQLite database** connection and containerization
#### Microservices Communication
* Sync inter-service **gRPC Communication**
@@ -43,33 +45,19 @@ We have implemented below **features over the run-aspnetcore-microservices repos
#### Ordering Microservice
* Implementing **DDD, CQRS, and Clean Architecture** with using Best Practices
-* Developing **CQRS with using MediatR, FluentValidation and AutoMapper packages**
+* Developing **CQRS with using MediatR, FluentValidation and Mapster packages**
* Consuming **RabbitMQ** BasketCheckout event queue with using **MassTransit-RabbitMQ** Configuration
* **SqlServer database** connection and containerization
* Using **Entity Framework Core ORM** and auto migrate to SqlServer when application startup
-#### API Gateway Ocelot Microservice
-* Implement **API Gateways with Ocelot**
-* Sample microservices/containers to reroute through the API Gateways
-* Run multiple different **API Gateway/BFF** container types
-* The Gateway aggregation pattern in Shopping.Aggregator
+#### Yarp API Gateway Microservice
+* Develop API Gateways with **Yarp Reverse Proxy** applying Gateway Routing Pattern
+* Yarp Reverse Proxy Configuration; Route, Cluster, Path, Transform, Destinations
+* **Rate Limiting** with FixedWindowLimiter on Yarp Reverse Proxy Configuration
#### WebUI ShoppingApp Microservice
* ASP.NET Core Web Application with Bootstrap 4 and Razor template
-* Call **Ocelot APIs with HttpClientFactory** and **Polly**
-
-#### Microservices Cross-Cutting Implementations
-* Implementing **Centralized Distributed Logging with Elastic Stack (ELK); Elasticsearch, Logstash, Kibana and SeriLog** for Microservices
-* Use the **HealthChecks** feature in back-end ASP.NET microservices
-* Using **Watchdog** in separate service that can watch health and load across services, and report health about the microservices by querying with the HealthChecks
-
-#### Microservices Resilience Implementations
-* Making Microservices more **resilient Use IHttpClientFactory** to implement resilient HTTP requests
-* Implement **Retry and Circuit Breaker patterns** with exponential backoff with IHttpClientFactory and **Polly policies**
-
-#### Ancillary Containers
-* Use **Portainer** for Container lightweight management UI which allows you to easily manage your different Docker environments
-* **pgAdmin PostgreSQL Tools** feature rich Open Source administration and development platform for PostgreSQL
+* Call **Yarp APIs with Refit HttpClientFactory**
#### Docker Compose establishment with all microservices on docker;
* Containerization of microservices
@@ -79,8 +67,8 @@ We have implemented below **features over the run-aspnetcore-microservices repos
## Run The Project
You will need the following tools:
-* [Visual Studio 2019](https://visualstudio.microsoft.com/downloads/)
-* [.Net Core 5 or later](https://dotnet.microsoft.com/download/dotnet-core/5)
+* [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/)
+* [.Net Core 8 or later](https://dotnet.microsoft.com/download/dotnet-core/8)
* [Docker Desktop](https://www.docker.com/products/docker-desktop)
### Installing
@@ -89,36 +77,18 @@ Follow these steps to get your development environment set up: (Before Run Start
2. Once Docker for Windows is installed, go to the **Settings > Advanced option**, from the Docker icon in the system tray, to configure the minimum amount of memory and CPU like so:
* **Memory: 4 GB**
* CPU: 2
-3. At the root directory which include **docker-compose.yml** files, run below command:
+3. At the root directory of solution, select **docker-compose** and **Set a startup project**. **Run docker-compose without debugging on visual studio**.
+ Or you can go to root directory which include **docker-compose.yml** files, run below command:
```csharp
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
```
-3. Wait for docker compose all microservices. That’s it! (some microservices need extra time to work so please wait if not worked in first shut)
-
-4. You can **launch microservices** as below urls:
-* **Catalog API -> http://host.docker.internal:8000/swagger/index.html**
-* **Basket API -> http://host.docker.internal:8001/swagger/index.html**
-* **Discount API -> http://host.docker.internal:8002/swagger/index.html**
-* **Ordering API -> http://host.docker.internal:8004/swagger/index.html**
-* **Shopping.Aggregator -> http://host.docker.internal:8005/swagger/index.html**
-* **API Gateway -> http://host.docker.internal:8010/Catalog**
-* **Rabbit Management Dashboard -> http://host.docker.internal:15672** -- guest/guest
-* **Portainer -> http://host.docker.internal:9000** -- admin/admin1234
-* **pgAdmin PostgreSQL -> http://host.docker.internal:5050** -- admin@aspnetrun.com/admin1234
-* **Elasticsearch -> http://host.docker.internal:9200**
-* **Kibana -> http://host.docker.internal:5601**
+4. Wait for docker compose all microservices. That’s it! (some microservices need extra time to work so please wait if not worked in first shut)
-* **Web Status -> http://host.docker.internal:8007**
-* **Web UI -> http://host.docker.internal:8006**
-
-5. Launch http://host.docker.internal:8007 in your browser to view the Web Status. Make sure that every microservices are healthy.
-6. Launch http://host.docker.internal:8006 in your browser to view the Web UI. You can use Web project in order to **call microservices over API Gateway**. When you **checkout the basket** you can follow **queue record on RabbitMQ dashboard**.
+5. Launch **Shopping Web UI -> https://localhost:6065** in your browser to view index page. You can use Web project in order to **call microservices over Yarp API Gateway**. When you **checkout the basket** you can follow **queue record on RabbitMQ dashboard**.

->Note: If you are running this application in macOS then use `docker.for.mac.localhost` as DNS name in `.env` file and the above URLs instead of `host.docker.internal`.
-
## Authors
* **Mehmet Ozkaya** - *Initial work* - [mehmetozkaya](https://github.com/mehmetozkaya)
diff --git a/src/.dockerignore b/src/.dockerignore
index 3729ff0c..fe1152bd 100644
--- a/src/.dockerignore
+++ b/src/.dockerignore
@@ -22,4 +22,9 @@
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
-README.md
\ No newline at end of file
+README.md
+!**/.gitignore
+!.git/HEAD
+!.git/config
+!.git/packed-refs
+!.git/refs/heads/**
\ No newline at end of file
diff --git a/src/ApiGateways/OcelotApiGw/Dockerfile b/src/ApiGateways/OcelotApiGw/Dockerfile
deleted file mode 100644
index b753338b..00000000
--- a/src/ApiGateways/OcelotApiGw/Dockerfile
+++ /dev/null
@@ -1,22 +0,0 @@
-#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
-WORKDIR /app
-EXPOSE 80
-
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
-WORKDIR /src
-COPY ["ApiGateways/OcelotApiGw/OcelotApiGw.csproj", "ApiGateways/OcelotApiGw/"]
-COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"]
-RUN dotnet restore "ApiGateways/OcelotApiGw/OcelotApiGw.csproj"
-COPY . .
-WORKDIR "/src/ApiGateways/OcelotApiGw"
-RUN dotnet build "OcelotApiGw.csproj" -c Release -o /app/build
-
-FROM build AS publish
-RUN dotnet publish "OcelotApiGw.csproj" -c Release -o /app/publish
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "OcelotApiGw.dll"]
\ No newline at end of file
diff --git a/src/ApiGateways/OcelotApiGw/OcelotApiGw.csproj b/src/ApiGateways/OcelotApiGw/OcelotApiGw.csproj
deleted file mode 100644
index c6a4832b..00000000
--- a/src/ApiGateways/OcelotApiGw/OcelotApiGw.csproj
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
- net5.0
- ..\..\docker-compose.dcproj
- Linux
- ..\..
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ApiGateways/OcelotApiGw/Program.cs b/src/ApiGateways/OcelotApiGw/Program.cs
deleted file mode 100644
index 5381b777..00000000
--- a/src/ApiGateways/OcelotApiGw/Program.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Common.Logging;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Serilog;
-
-namespace OcelotApiGw
-{
- public class Program
- {
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .ConfigureAppConfiguration((hostingContext, config) =>
- {
- config.AddJsonFile($"ocelot.{hostingContext.HostingEnvironment.EnvironmentName}.json", true, true);
- })
- .UseSerilog(SeriLogger.Configure)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
-}
diff --git a/src/ApiGateways/OcelotApiGw/Startup.cs b/src/ApiGateways/OcelotApiGw/Startup.cs
deleted file mode 100644
index 962a7007..00000000
--- a/src/ApiGateways/OcelotApiGw/Startup.cs
+++ /dev/null
@@ -1,42 +0,0 @@
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Ocelot.Cache.CacheManager;
-using Ocelot.DependencyInjection;
-using Ocelot.Middleware;
-
-namespace OcelotApiGw
-{
- public class Startup
- {
- // This method gets called by the runtime. Use this method to add services to the container.
- // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddOcelot().AddCacheManager(settings => settings.WithDictionaryHandle());
- }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public async void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
-
- app.UseRouting();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapGet("/", async context =>
- {
- await context.Response.WriteAsync("Hello World!");
- });
- });
-
- await app.UseOcelot();
- }
- }
-}
diff --git a/src/ApiGateways/OcelotApiGw/appsettings.Development.json b/src/ApiGateways/OcelotApiGw/appsettings.Development.json
deleted file mode 100644
index 8983e0fc..00000000
--- a/src/ApiGateways/OcelotApiGw/appsettings.Development.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- }
-}
diff --git a/src/ApiGateways/OcelotApiGw/appsettings.json b/src/ApiGateways/OcelotApiGw/appsettings.json
deleted file mode 100644
index 95ff6d7e..00000000
--- a/src/ApiGateways/OcelotApiGw/appsettings.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "Serilog": {
- "MinimumLevel": {
- "Default": "Information",
- "Override": {
- "Microsoft": "Information",
- "System": "Warning"
- }
- }
- },
- "ElasticConfiguration": {
- "Uri": "http://localhost:9200"
- },
- "AllowedHosts": "*"
-}
diff --git a/src/ApiGateways/OcelotApiGw/ocelot.Development.json b/src/ApiGateways/OcelotApiGw/ocelot.Development.json
deleted file mode 100644
index 1cd53e88..00000000
--- a/src/ApiGateways/OcelotApiGw/ocelot.Development.json
+++ /dev/null
@@ -1,127 +0,0 @@
-{
- "Routes": [
- //Catalog API
- {
- "DownstreamPathTemplate": "/api/v1/Catalog",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "catalog.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Catalog",
- "UpstreamHttpMethod": [ "GET", "POST", "PUT" ],
- "FileCacheOptions": { "TtlSeconds": 30 }
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/{id}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "catalog.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/{id}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/GetProductByCategory/{category}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "catalog.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/GetProductByCategory/{category}",
- "UpstreamHttpMethod": [ "GET" ]
- },
- //Basket API
- {
- "DownstreamPathTemplate": "/api/v1/Basket/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "basket.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Basket/{userName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "basket.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Basket",
- "UpstreamHttpMethod": [ "POST" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket/Checkout",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "basket.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Basket/Checkout",
- "UpstreamHttpMethod": [ "POST" ],
- "RateLimitOptions": {
- "ClientWhitelist": [],
- "EnableRateLimiting": true,
- "Period": "3s",
- "PeriodTimespan": 1,
- "Limit": 1
- }
- },
- //Discount API
- {
- "DownstreamPathTemplate": "/api/v1/Discount/{productName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "discount.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Discount/{productName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Discount",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "discount.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Discount",
- "UpstreamHttpMethod": [ "PUT", "POST" ]
- },
- //Order API
- {
- "DownstreamPathTemplate": "/api/v1/Order/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "ordering.api",
- "Port": "80"
- }
- ],
- "UpstreamPathTemplate": "/Order/{userName}",
- "UpstreamHttpMethod": [ "GET" ]
- }
- ],
- "GlobalConfiguration": {
- "BaseUrl": "http://localhost:5010"
- }
-}
diff --git a/src/ApiGateways/OcelotApiGw/ocelot.json b/src/ApiGateways/OcelotApiGw/ocelot.json
deleted file mode 100644
index a0209b7d..00000000
--- a/src/ApiGateways/OcelotApiGw/ocelot.json
+++ /dev/null
@@ -1,127 +0,0 @@
-{
- "Routes": [
- //Catalog API
- {
- "DownstreamPathTemplate": "/api/v1/Catalog",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog",
- "UpstreamHttpMethod": [ "GET", "POST", "PUT" ],
- "FileCacheOptions": { "TtlSeconds": 30 }
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/{id}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/{id}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/GetProductByCategory/{category}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/GetProductByCategory/{category}",
- "UpstreamHttpMethod": [ "GET" ]
- },
- //Basket API
- {
- "DownstreamPathTemplate": "/api/v1/Basket/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket/{userName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket",
- "UpstreamHttpMethod": [ "POST" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket/Checkout",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket/Checkout",
- "UpstreamHttpMethod": [ "POST" ],
- "RateLimitOptions": {
- "ClientWhitelist": [],
- "EnableRateLimiting": true,
- "Period": "3s",
- "PeriodTimespan": 1,
- "Limit": 1
- }
- },
- //Discount API
- {
- "DownstreamPathTemplate": "/api/v1/Discount/{productName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8002"
- }
- ],
- "UpstreamPathTemplate": "/Discount/{productName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Discount",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8002"
- }
- ],
- "UpstreamPathTemplate": "/Discount",
- "UpstreamHttpMethod": [ "PUT", "POST" ]
- },
- //Order API
- {
- "DownstreamPathTemplate": "/api/v1/Order/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8004"
- }
- ],
- "UpstreamPathTemplate": "/Order/{userName}",
- "UpstreamHttpMethod": [ "GET" ]
- }
- ],
- "GlobalConfiguration": {
- "BaseUrl": "http://localhost:5010"
- }
-}
diff --git a/src/ApiGateways/OcelotApiGw/ocelot.local.json b/src/ApiGateways/OcelotApiGw/ocelot.local.json
deleted file mode 100644
index 6538291e..00000000
--- a/src/ApiGateways/OcelotApiGw/ocelot.local.json
+++ /dev/null
@@ -1,127 +0,0 @@
-{
- "Routes": [
- //Catalog API
- {
- "DownstreamPathTemplate": "/api/v1/Catalog",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog",
- "UpstreamHttpMethod": [ "GET", "POST", "PUT" ],
- "FileCacheOptions": { "TtlSeconds": 30 }
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/{id}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/{id}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Catalog/GetProductByCategory/{category}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8000"
- }
- ],
- "UpstreamPathTemplate": "/Catalog/GetProductByCategory/{category}",
- "UpstreamHttpMethod": [ "GET" ]
- },
- //Basket API
- {
- "DownstreamPathTemplate": "/api/v1/Basket/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket/{userName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket",
- "UpstreamHttpMethod": [ "POST" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Basket/Checkout",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8001"
- }
- ],
- "UpstreamPathTemplate": "/Basket/Checkout",
- "UpstreamHttpMethod": [ "POST" ],
- "RateLimitOptions": {
- "ClientWhitelist": [],
- "EnableRateLimiting": true,
- "Period": "3s",
- "PeriodTimespan": 1,
- "Limit": 1
- }
- },
- //Discount API
- {
- "DownstreamPathTemplate": "/api/v1/Discount/{productName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8002"
- }
- ],
- "UpstreamPathTemplate": "/Discount/{productName}",
- "UpstreamHttpMethod": [ "GET", "DELETE" ]
- },
- {
- "DownstreamPathTemplate": "/api/v1/Discount",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8002"
- }
- ],
- "UpstreamPathTemplate": "/Discount",
- "UpstreamHttpMethod": [ "PUT", "POST" ]
- },
- //Order API
- {
- "DownstreamPathTemplate": "/api/v1/Order/{userName}",
- "DownstreamScheme": "http",
- "DownstreamHostAndPorts": [
- {
- "Host": "localhost",
- "Port": "8004"
- }
- ],
- "UpstreamPathTemplate": "/Order/{userName}",
- "UpstreamHttpMethod": [ "GET" ]
- }
- ],
- "GlobalConfiguration": {
- "BaseUrl": "http://localhost:5010"
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Controllers/ShoppingController.cs b/src/ApiGateways/Shopping.Aggregator/Controllers/ShoppingController.cs
deleted file mode 100644
index 8e56aa4d..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Controllers/ShoppingController.cs
+++ /dev/null
@@ -1,56 +0,0 @@
-using Microsoft.AspNetCore.Mvc;
-using Shopping.Aggregator.Models;
-using Shopping.Aggregator.Services;
-using System;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Controllers
-{
- [ApiController]
- [Route("api/v1/[controller]")]
- public class ShoppingController : ControllerBase
- {
- private readonly ICatalogService _catalogService;
- private readonly IBasketService _basketService;
- private readonly IOrderService _orderService;
-
- public ShoppingController(ICatalogService catalogService, IBasketService basketService, IOrderService orderService)
- {
- _catalogService = catalogService ?? throw new ArgumentNullException(nameof(catalogService));
- _basketService = basketService ?? throw new ArgumentNullException(nameof(basketService));
- _orderService = orderService ?? throw new ArgumentNullException(nameof(orderService));
- }
-
- [HttpGet("{userName}", Name = "GetShopping")]
- [ProducesResponseType(typeof(ShoppingModel), (int)HttpStatusCode.OK)]
- public async Task> GetShopping(string userName)
- {
- var basket = await _basketService.GetBasket(userName);
-
- foreach (var item in basket.Items)
- {
- var product = await _catalogService.GetCatalog(item.ProductId);
-
- // set additional product fields
- item.ProductName = product.Name;
- item.Category = product.Category;
- item.Summary = product.Summary;
- item.Description = product.Description;
- item.ImageFile = product.ImageFile;
- }
-
- var orders = await _orderService.GetOrdersByUserName(userName);
-
- var shoppingModel = new ShoppingModel
- {
- UserName = userName,
- BasketWithProducts = basket,
- Orders = orders
- };
-
- return Ok(shoppingModel);
- }
-
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Dockerfile b/src/ApiGateways/Shopping.Aggregator/Dockerfile
deleted file mode 100644
index 7ffda6e7..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Dockerfile
+++ /dev/null
@@ -1,22 +0,0 @@
-#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
-WORKDIR /app
-EXPOSE 80
-
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
-WORKDIR /src
-COPY ["ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj", "ApiGateways/Shopping.Aggregator/"]
-COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"]
-RUN dotnet restore "ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj"
-COPY . .
-WORKDIR "/src/ApiGateways/Shopping.Aggregator"
-RUN dotnet build "Shopping.Aggregator.csproj" -c Release -o /app/build
-
-FROM build AS publish
-RUN dotnet publish "Shopping.Aggregator.csproj" -c Release -o /app/publish
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "Shopping.Aggregator.dll"]
\ No newline at end of file
diff --git a/src/ApiGateways/Shopping.Aggregator/Extensions/HttpClientExtensions.cs b/src/ApiGateways/Shopping.Aggregator/Extensions/HttpClientExtensions.cs
deleted file mode 100644
index 372f6628..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Extensions/HttpClientExtensions.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using System;
-using System.Net.Http;
-using System.Text.Json;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Extensions
-{
- public static class HttpClientExtensions
- {
- public static async Task ReadContentAs(this HttpResponseMessage response)
- {
- if (!response.IsSuccessStatusCode)
- throw new ApplicationException($"Something went wrong calling the API: {response.ReasonPhrase}");
-
- var dataAsString = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
-
- return JsonSerializer.Deserialize(dataAsString, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Models/BasketItemExtendedModel.cs b/src/ApiGateways/Shopping.Aggregator/Models/BasketItemExtendedModel.cs
deleted file mode 100644
index dc81e1ff..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Models/BasketItemExtendedModel.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Shopping.Aggregator.Models
-{
- public class BasketItemExtendedModel
- {
- public int Quantity { get; set; }
- public string Color { get; set; }
- public decimal Price { get; set; }
- public string ProductId { get; set; }
- public string ProductName { get; set; }
-
- //Product Related Additional Fields
- public string Category { get; set; }
- public string Summary { get; set; }
- public string Description { get; set; }
- public string ImageFile { get; set; }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Models/BasketModel.cs b/src/ApiGateways/Shopping.Aggregator/Models/BasketModel.cs
deleted file mode 100644
index ca86924c..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Models/BasketModel.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Collections.Generic;
-
-namespace Shopping.Aggregator.Models
-{
- public class BasketModel
- {
- public string UserName { get; set; }
- public List Items { get; set; } = new List();
- public decimal TotalPrice { get; set; }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Models/CatalogModel.cs b/src/ApiGateways/Shopping.Aggregator/Models/CatalogModel.cs
deleted file mode 100644
index a75aff5d..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Models/CatalogModel.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-namespace Shopping.Aggregator.Models
-{
- public class CatalogModel
- {
- public string Id { get; set; }
- public string Name { get; set; }
- public string Category { get; set; }
- public string Summary { get; set; }
- public string Description { get; set; }
- public string ImageFile { get; set; }
- public decimal Price { get; set; }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Models/OrderResponseModel.cs b/src/ApiGateways/Shopping.Aggregator/Models/OrderResponseModel.cs
deleted file mode 100644
index 590d42f0..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Models/OrderResponseModel.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Shopping.Aggregator.Models
-{
- public class OrderResponseModel
- {
- public string UserName { get; set; }
- public decimal TotalPrice { get; set; }
-
- // BillingAddress
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public string EmailAddress { get; set; }
- public string AddressLine { get; set; }
- public string Country { get; set; }
- public string State { get; set; }
- public string ZipCode { get; set; }
-
- // Payment
- public string CardName { get; set; }
- public string CardNumber { get; set; }
- public string Expiration { get; set; }
- public string CVV { get; set; }
- public int PaymentMethod { get; set; }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Models/ShoppingModel.cs b/src/ApiGateways/Shopping.Aggregator/Models/ShoppingModel.cs
deleted file mode 100644
index 8adbc63c..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Models/ShoppingModel.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Collections.Generic;
-
-namespace Shopping.Aggregator.Models
-{
- public class ShoppingModel
- {
- public string UserName { get; set; }
- public BasketModel BasketWithProducts { get; set; }
- public IEnumerable Orders { get; set; }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Program.cs b/src/ApiGateways/Shopping.Aggregator/Program.cs
deleted file mode 100644
index 9f232a9b..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Program.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Common.Logging;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using Serilog;
-
-namespace Shopping.Aggregator
-{
- public class Program
- {
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSerilog(SeriLogger.Configure)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/BasketService.cs b/src/ApiGateways/Shopping.Aggregator/Services/BasketService.cs
deleted file mode 100644
index cb9dcf93..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/BasketService.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using Shopping.Aggregator.Extensions;
-using Shopping.Aggregator.Models;
-using System;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public class BasketService : IBasketService
- {
- private readonly HttpClient _client;
-
- public BasketService(HttpClient client)
- {
- _client = client ?? throw new ArgumentNullException(nameof(client));
- }
-
- public async Task GetBasket(string userName)
- {
- var response = await _client.GetAsync($"/api/v1/Basket/{userName}");
- return await response.ReadContentAs();
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/CatalogService.cs b/src/ApiGateways/Shopping.Aggregator/Services/CatalogService.cs
deleted file mode 100644
index 991fb610..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/CatalogService.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using Shopping.Aggregator.Extensions;
-using Shopping.Aggregator.Models;
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public class CatalogService : ICatalogService
- {
- private readonly HttpClient _client;
-
- public CatalogService(HttpClient client)
- {
- _client = client ?? throw new ArgumentNullException(nameof(client));
- }
-
- public async Task> GetCatalog()
- {
- var response = await _client.GetAsync("/api/v1/Catalog");
- return await response.ReadContentAs>();
- }
-
- public async Task GetCatalog(string id)
- {
- var response = await _client.GetAsync($"/api/v1/Catalog/{id}");
- return await response.ReadContentAs();
- }
-
- public async Task> GetCatalogByCategory(string category)
- {
- var response = await _client.GetAsync($"/api/v1/Catalog/GetProductByCategory/{category}");
- return await response.ReadContentAs>();
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/IBasketService.cs b/src/ApiGateways/Shopping.Aggregator/Services/IBasketService.cs
deleted file mode 100644
index 3ff43435..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/IBasketService.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Shopping.Aggregator.Models;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public interface IBasketService
- {
- Task GetBasket(string userName);
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/ICatalogService.cs b/src/ApiGateways/Shopping.Aggregator/Services/ICatalogService.cs
deleted file mode 100644
index f0cc7be0..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/ICatalogService.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Shopping.Aggregator.Models;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public interface ICatalogService
- {
- Task> GetCatalog();
- Task> GetCatalogByCategory(string category);
- Task GetCatalog(string id);
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/IOrderService.cs b/src/ApiGateways/Shopping.Aggregator/Services/IOrderService.cs
deleted file mode 100644
index 427f1008..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/IOrderService.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using Shopping.Aggregator.Models;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public interface IOrderService
- {
- Task> GetOrdersByUserName(string userName);
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Services/OrderService.cs b/src/ApiGateways/Shopping.Aggregator/Services/OrderService.cs
deleted file mode 100644
index 5b41379c..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Services/OrderService.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Shopping.Aggregator.Extensions;
-using Shopping.Aggregator.Models;
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading.Tasks;
-
-namespace Shopping.Aggregator.Services
-{
- public class OrderService : IOrderService
- {
- private readonly HttpClient _client;
-
- public OrderService(HttpClient client)
- {
- _client = client ?? throw new ArgumentNullException(nameof(client));
- }
-
- public async Task> GetOrdersByUserName(string userName)
- {
- var response = await _client.GetAsync($"/api/v1/Order/{userName}");
- return await response.ReadContentAs>();
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/Shopping - Backup.Aggregator.csproj b/src/ApiGateways/Shopping.Aggregator/Shopping - Backup.Aggregator.csproj
deleted file mode 100644
index facb05c2..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Shopping - Backup.Aggregator.csproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
- net5.0
- ..\..\docker-compose.dcproj
-
-
-
-
-
-
-
diff --git a/src/ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj b/src/ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj
deleted file mode 100644
index 19b91443..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Shopping.Aggregator.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- net5.0
- ..\..\docker-compose.dcproj
- Linux
- ..\..
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/ApiGateways/Shopping.Aggregator/Startup.cs b/src/ApiGateways/Shopping.Aggregator/Startup.cs
deleted file mode 100644
index af088855..00000000
--- a/src/ApiGateways/Shopping.Aggregator/Startup.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using Common.Logging;
-using HealthChecks.UI.Client;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-using Microsoft.Extensions.Hosting;
-using Microsoft.OpenApi.Models;
-using Polly;
-using Polly.Extensions.Http;
-using Serilog;
-using Shopping.Aggregator.Services;
-using System;
-using System.Net.Http;
-
-namespace Shopping.Aggregator
-{
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddTransient();
-
- services.AddHttpClient(c =>
- c.BaseAddress = new Uri(Configuration["ApiSettings:CatalogUrl"]))
- .AddHttpMessageHandler()
- .AddPolicyHandler(GetRetryPolicy())
- .AddPolicyHandler(GetCircuitBreakerPolicy());
-
- services.AddHttpClient(c =>
- c.BaseAddress = new Uri(Configuration["ApiSettings:BasketUrl"]))
- .AddHttpMessageHandler()
- .AddPolicyHandler(GetRetryPolicy())
- .AddPolicyHandler(GetCircuitBreakerPolicy());
-
- services.AddHttpClient(c =>
- c.BaseAddress = new Uri(Configuration["ApiSettings:OrderingUrl"]))
- .AddHttpMessageHandler()
- .AddPolicyHandler(GetRetryPolicy())
- .AddPolicyHandler(GetCircuitBreakerPolicy());
-
- services.AddControllers();
- services.AddSwaggerGen(c =>
- {
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "Shopping.Aggregator", Version = "v1" });
- });
-
- services.AddHealthChecks()
- .AddUrlGroup(new Uri($"{Configuration["ApiSettings:CatalogUrl"]}/swagger/index.html"), "Catalog.API", HealthStatus.Degraded)
- .AddUrlGroup(new Uri($"{Configuration["ApiSettings:BasketUrl"]}/swagger/index.html"), "Basket.API", HealthStatus.Degraded)
- .AddUrlGroup(new Uri($"{Configuration["ApiSettings:OrderingUrl"]}/swagger/index.html"), "Ordering.API", HealthStatus.Degraded);
- }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- app.UseSwagger();
- app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Shopping.Aggregator v1"));
- }
-
- app.UseRouting();
-
- app.UseAuthorization();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
- {
- Predicate = _ => true,
- ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
- });
- });
- }
-
- private static IAsyncPolicy GetRetryPolicy()
- {
- // In this case will wait for
- // 2 ^ 1 = 2 seconds then
- // 2 ^ 2 = 4 seconds then
- // 2 ^ 3 = 8 seconds then
- // 2 ^ 4 = 16 seconds then
- // 2 ^ 5 = 32 seconds
-
- return HttpPolicyExtensions
- .HandleTransientHttpError()
- .WaitAndRetryAsync(
- retryCount: 5,
- sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
- onRetry: (exception, retryCount, context) =>
- {
- Log.Error($"Retry {retryCount} of {context.PolicyKey} at {context.OperationKey}, due to: {exception}.");
- });
- }
-
- private static IAsyncPolicy GetCircuitBreakerPolicy()
- {
- return HttpPolicyExtensions
- .HandleTransientHttpError()
- .CircuitBreakerAsync(
- handledEventsAllowedBeforeBreaking: 5,
- durationOfBreak: TimeSpan.FromSeconds(30)
- );
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/appsettings.Development.json b/src/ApiGateways/Shopping.Aggregator/appsettings.Development.json
deleted file mode 100644
index 8983e0fc..00000000
--- a/src/ApiGateways/Shopping.Aggregator/appsettings.Development.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
- }
- }
-}
diff --git a/src/ApiGateways/Shopping.Aggregator/appsettings.json b/src/ApiGateways/Shopping.Aggregator/appsettings.json
deleted file mode 100644
index e894dd1b..00000000
--- a/src/ApiGateways/Shopping.Aggregator/appsettings.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "ApiSettings": {
- "CatalogUrl": "http://localhost:8000",
- "BasketUrl": "http://localhost:8001",
- "OrderingUrl": "http://localhost:8004"
- },
- "Serilog": {
- "MinimumLevel": {
- "Default": "Information",
- "Override": {
- "Microsoft": "Information",
- "System": "Warning"
- }
- }
- },
- "ElasticConfiguration": {
- "Uri": "http://localhost:9200"
- },
- "AllowedHosts": "*"
-}
diff --git a/src/ApiGateways/YarpApiGateway/Dockerfile b/src/ApiGateways/YarpApiGateway/Dockerfile
new file mode 100644
index 00000000..8b168ba5
--- /dev/null
+++ b/src/ApiGateways/YarpApiGateway/Dockerfile
@@ -0,0 +1,25 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["ApiGateways/YarpApiGateway/YarpApiGateway.csproj", "ApiGateways/YarpApiGateway/"]
+RUN dotnet restore "./ApiGateways/YarpApiGateway/./YarpApiGateway.csproj"
+COPY . .
+WORKDIR "/src/ApiGateways/YarpApiGateway"
+RUN dotnet build "./YarpApiGateway.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./YarpApiGateway.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "YarpApiGateway.dll"]
\ No newline at end of file
diff --git a/src/ApiGateways/YarpApiGateway/Program.cs b/src/ApiGateways/YarpApiGateway/Program.cs
new file mode 100644
index 00000000..b0fcda0e
--- /dev/null
+++ b/src/ApiGateways/YarpApiGateway/Program.cs
@@ -0,0 +1,25 @@
+using Microsoft.AspNetCore.RateLimiting;
+
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+builder.Services.AddReverseProxy()
+ .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
+
+builder.Services.AddRateLimiter(rateLimiterOptions =>
+{
+ rateLimiterOptions.AddFixedWindowLimiter("fixed", options =>
+ {
+ options.Window = TimeSpan.FromSeconds(10);
+ options.PermitLimit = 5;
+ });
+});
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.UseRateLimiter();
+
+app.MapReverseProxy();
+
+app.Run();
diff --git a/src/WebApps/WebStatus/WebStatus.csproj b/src/ApiGateways/YarpApiGateway/YarpApiGateway.csproj
similarity index 55%
rename from src/WebApps/WebStatus/WebStatus.csproj
rename to src/ApiGateways/YarpApiGateway/YarpApiGateway.csproj
index cc543a44..276346a8 100644
--- a/src/WebApps/WebStatus/WebStatus.csproj
+++ b/src/ApiGateways/YarpApiGateway/YarpApiGateway.csproj
@@ -1,16 +1,18 @@
- net5.0
- ..\..\docker-compose.dcproj
+ net8.0
+ enable
+ enable
+ d254758e-5f2e-416e-8620-424bb728dc95
Linux
..\..
+ ..\..\docker-compose.dcproj
-
-
-
+
+
diff --git a/src/ApiGateways/YarpApiGateway/appsettings.Development.json b/src/ApiGateways/YarpApiGateway/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/src/ApiGateways/YarpApiGateway/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/src/ApiGateways/YarpApiGateway/appsettings.Local.json b/src/ApiGateways/YarpApiGateway/appsettings.Local.json
new file mode 100644
index 00000000..f82983de
--- /dev/null
+++ b/src/ApiGateways/YarpApiGateway/appsettings.Local.json
@@ -0,0 +1,58 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ReverseProxy": {
+ "Routes": {
+ "catalog-route": {
+ "ClusterId": "catalog-cluster",
+ "Match": {
+ "Path": "/catalog-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ },
+ "basket-route": {
+ "ClusterId": "basket-cluster",
+ "Match": {
+ "Path": "/basket-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ },
+ "ordering-route": {
+ "ClusterId": "ordering-cluster",
+ "RateLimiterPolicy": "fixed",
+ "Match": {
+ "Path": "/ordering-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ }
+ },
+ "Clusters": {
+ "catalog-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://localhost:6000/"
+ }
+ }
+ },
+ "basket-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://localhost:6001/"
+ }
+ }
+ },
+ "ordering-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://localhost:6003/"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/ApiGateways/YarpApiGateway/appsettings.json b/src/ApiGateways/YarpApiGateway/appsettings.json
new file mode 100644
index 00000000..e33cde08
--- /dev/null
+++ b/src/ApiGateways/YarpApiGateway/appsettings.json
@@ -0,0 +1,58 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ReverseProxy": {
+ "Routes": {
+ "catalog-route": {
+ "ClusterId": "catalog-cluster",
+ "Match": {
+ "Path": "/catalog-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ },
+ "basket-route": {
+ "ClusterId": "basket-cluster",
+ "Match": {
+ "Path": "/basket-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ },
+ "ordering-route": {
+ "ClusterId": "ordering-cluster",
+ "RateLimiterPolicy": "fixed",
+ "Match": {
+ "Path": "/ordering-service/{**catch-all}"
+ },
+ "Transforms": [ { "PathPattern": "{**catch-all}" } ]
+ }
+ },
+ "Clusters": {
+ "catalog-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://catalog.api:8080"
+ }
+ }
+ },
+ "basket-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://basket.api:8080"
+ }
+ }
+ },
+ "ordering-cluster": {
+ "Destinations": {
+ "destination1": {
+ "Address": "http://ordering.api:8080"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks.Messaging/BuildingBlocks.Messaging.csproj b/src/BuildingBlocks/BuildingBlocks.Messaging/BuildingBlocks.Messaging.csproj
new file mode 100644
index 00000000..456530fb
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks.Messaging/BuildingBlocks.Messaging.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/src/BuildingBlocks/BuildingBlocks.Messaging/Events/BasketCheckoutEvent.cs b/src/BuildingBlocks/BuildingBlocks.Messaging/Events/BasketCheckoutEvent.cs
new file mode 100644
index 00000000..d6e10c30
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks.Messaging/Events/BasketCheckoutEvent.cs
@@ -0,0 +1,23 @@
+namespace BuildingBlocks.Messaging.Events;
+public record BasketCheckoutEvent : IntegrationEvent
+{
+ public string UserName { get; set; } = default!;
+ public Guid CustomerId { get; set; } = default!;
+ public decimal TotalPrice { get; set; } = default!;
+
+ // Shipping and BillingAddress
+ public string FirstName { get; set; } = default!;
+ public string LastName { get; set; } = default!;
+ public string EmailAddress { get; set; } = default!;
+ public string AddressLine { get; set; } = default!;
+ public string Country { get; set; } = default!;
+ public string State { get; set; } = default!;
+ public string ZipCode { get; set; } = default!;
+
+ // Payment
+ public string CardName { get; set; } = default!;
+ public string CardNumber { get; set; } = default!;
+ public string Expiration { get; set; } = default!;
+ public string CVV { get; set; } = default!;
+ public int PaymentMethod { get; set; } = default!;
+}
diff --git a/src/BuildingBlocks/BuildingBlocks.Messaging/Events/IntegrationEvent.cs b/src/BuildingBlocks/BuildingBlocks.Messaging/Events/IntegrationEvent.cs
new file mode 100644
index 00000000..aa4d386f
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks.Messaging/Events/IntegrationEvent.cs
@@ -0,0 +1,7 @@
+namespace BuildingBlocks.Messaging.Events;
+public record IntegrationEvent
+{
+ public Guid Id => Guid.NewGuid();
+ public DateTime OccurredOn => DateTime.Now;
+ public string EventType => GetType().AssemblyQualifiedName;
+}
diff --git a/src/BuildingBlocks/BuildingBlocks.Messaging/MassTransit/Extentions.cs b/src/BuildingBlocks/BuildingBlocks.Messaging/MassTransit/Extentions.cs
new file mode 100644
index 00000000..1685398d
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks.Messaging/MassTransit/Extentions.cs
@@ -0,0 +1,32 @@
+using MassTransit;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using System.Reflection;
+
+namespace BuildingBlocks.Messaging.MassTransit;
+public static class Extentions
+{
+ public static IServiceCollection AddMessageBroker
+ (this IServiceCollection services, IConfiguration configuration, Assembly? assembly = null)
+ {
+ services.AddMassTransit(config =>
+ {
+ config.SetKebabCaseEndpointNameFormatter();
+
+ if (assembly != null)
+ config.AddConsumers(assembly);
+
+ config.UsingRabbitMq((context, configurator) =>
+ {
+ configurator.Host(new Uri(configuration["MessageBroker:Host"]!), host =>
+ {
+ host.Username(configuration["MessageBroker:UserName"]);
+ host.Password(configuration["MessageBroker:Password"]);
+ });
+ configurator.ConfigureEndpoints(context);
+ });
+ });
+
+ return services;
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Behaviors/LoggingBehavior.cs b/src/BuildingBlocks/BuildingBlocks/Behaviors/LoggingBehavior.cs
new file mode 100644
index 00000000..5650bce5
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Behaviors/LoggingBehavior.cs
@@ -0,0 +1,31 @@
+using MediatR;
+using Microsoft.Extensions.Logging;
+using System.Diagnostics;
+
+namespace BuildingBlocks.Behaviors;
+public class LoggingBehavior
+ (ILogger> logger)
+ : IPipelineBehavior
+ where TRequest : notnull, IRequest
+ where TResponse : notnull
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ logger.LogInformation("[START] Handle request={Request} - Response={Response} - RequestData={RequestData}",
+ typeof(TRequest).Name, typeof(TResponse).Name, request);
+
+ var timer = new Stopwatch();
+ timer.Start();
+
+ var response = await next();
+
+ timer.Stop();
+ var timeTaken = timer.Elapsed;
+ if (timeTaken.Seconds > 3) // if the request is greater than 3 seconds, then log the warnings
+ logger.LogWarning("[PERFORMANCE] The request {Request} took {TimeTaken} seconds.",
+ typeof(TRequest).Name, timeTaken.Seconds);
+
+ logger.LogInformation("[END] Handled {Request} with {Response}", typeof(TRequest).Name, typeof(TResponse).Name);
+ return response;
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Behaviors/ValidationBehavior.cs b/src/BuildingBlocks/BuildingBlocks/Behaviors/ValidationBehavior.cs
new file mode 100644
index 00000000..bc524acb
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Behaviors/ValidationBehavior.cs
@@ -0,0 +1,29 @@
+using BuildingBlocks.CQRS;
+using FluentValidation;
+using MediatR;
+
+namespace BuildingBlocks.Behaviors;
+public class ValidationBehavior
+ (IEnumerable> validators)
+ : IPipelineBehavior
+ where TRequest : ICommand
+{
+ public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
+ {
+ var context = new ValidationContext(request);
+
+ var validationResults =
+ await Task.WhenAll(validators.Select(v => v.ValidateAsync(context, cancellationToken)));
+
+ var failures =
+ validationResults
+ .Where(r => r.Errors.Any())
+ .SelectMany(r => r.Errors)
+ .ToList();
+
+ if (failures.Any())
+ throw new ValidationException(failures);
+
+ return await next();
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj b/src/BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj
new file mode 100644
index 00000000..ad74d7bf
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BuildingBlocks/BuildingBlocks/CQRS/ICommand.cs b/src/BuildingBlocks/BuildingBlocks/CQRS/ICommand.cs
new file mode 100644
index 00000000..507944aa
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/CQRS/ICommand.cs
@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace BuildingBlocks.CQRS;
+
+public interface ICommand : ICommand
+{
+}
+
+public interface ICommand : IRequest
+{
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/CQRS/ICommandHandler.cs b/src/BuildingBlocks/BuildingBlocks/CQRS/ICommandHandler.cs
new file mode 100644
index 00000000..df52da96
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/CQRS/ICommandHandler.cs
@@ -0,0 +1,16 @@
+using MediatR;
+
+namespace BuildingBlocks.CQRS;
+
+public interface ICommandHandler
+ : ICommandHandler
+ where TCommand : ICommand
+{
+}
+
+public interface ICommandHandler
+ : IRequestHandler
+ where TCommand : ICommand
+ where TResponse : notnull
+{
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/CQRS/IQuery.cs b/src/BuildingBlocks/BuildingBlocks/CQRS/IQuery.cs
new file mode 100644
index 00000000..834d2130
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/CQRS/IQuery.cs
@@ -0,0 +1,7 @@
+using MediatR;
+
+namespace BuildingBlocks.CQRS;
+public interface IQuery : IRequest
+ where TResponse : notnull
+{
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/CQRS/IQueryHandler.cs b/src/BuildingBlocks/BuildingBlocks/CQRS/IQueryHandler.cs
new file mode 100644
index 00000000..f7eeb42a
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/CQRS/IQueryHandler.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace BuildingBlocks.CQRS;
+public interface IQueryHandler
+ : IRequestHandler
+ where TQuery : IQuery
+ where TResponse : notnull
+{
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Exceptions/BadRequestException.cs b/src/BuildingBlocks/BuildingBlocks/Exceptions/BadRequestException.cs
new file mode 100644
index 00000000..0de450af
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Exceptions/BadRequestException.cs
@@ -0,0 +1,14 @@
+namespace BuildingBlocks.Exceptions;
+public class BadRequestException : Exception
+{
+ public BadRequestException(string message) : base(message)
+ {
+ }
+
+ public BadRequestException(string message, string details) : base(message)
+ {
+ Details = details;
+ }
+
+ public string? Details { get; }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Exceptions/Handler/CustomExceptionHandler.cs b/src/BuildingBlocks/BuildingBlocks/Exceptions/Handler/CustomExceptionHandler.cs
new file mode 100644
index 00000000..598d5fdd
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Exceptions/Handler/CustomExceptionHandler.cs
@@ -0,0 +1,70 @@
+using FluentValidation;
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+
+namespace BuildingBlocks.Exceptions.Handler;
+public class CustomExceptionHandler
+ (ILogger logger)
+ : IExceptionHandler
+{
+ public async ValueTask TryHandleAsync(HttpContext context, Exception exception, CancellationToken cancellationToken)
+ {
+ logger.LogError(
+ "Error Message: {exceptionMessage}, Time of occurrence {time}",
+ exception.Message, DateTime.UtcNow);
+
+ (string Detail, string Title, int StatusCode) details = exception switch
+ {
+ InternalServerException =>
+ (
+ exception.Message,
+ exception.GetType().Name,
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError
+ ),
+ ValidationException =>
+ (
+ exception.Message,
+ exception.GetType().Name,
+ context.Response.StatusCode = StatusCodes.Status400BadRequest
+ ),
+ BadRequestException =>
+ (
+ exception.Message,
+ exception.GetType().Name,
+ context.Response.StatusCode = StatusCodes.Status400BadRequest
+ ),
+ NotFoundException =>
+ (
+ exception.Message,
+ exception.GetType().Name,
+ context.Response.StatusCode = StatusCodes.Status404NotFound
+ ),
+ _ =>
+ (
+ exception.Message,
+ exception.GetType().Name,
+ context.Response.StatusCode = StatusCodes.Status500InternalServerError
+ )
+ };
+
+ var problemDetails = new ProblemDetails
+ {
+ Title = details.Title,
+ Detail = details.Detail,
+ Status = details.StatusCode,
+ Instance = context.Request.Path
+ };
+
+ problemDetails.Extensions.Add("traceId", context.TraceIdentifier);
+
+ if (exception is ValidationException validationException)
+ {
+ problemDetails.Extensions.Add("ValidationErrors", validationException.Errors);
+ }
+
+ await context.Response.WriteAsJsonAsync(problemDetails, cancellationToken: cancellationToken);
+ return true;
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Exceptions/InternalServerException.cs b/src/BuildingBlocks/BuildingBlocks/Exceptions/InternalServerException.cs
new file mode 100644
index 00000000..ce559e91
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Exceptions/InternalServerException.cs
@@ -0,0 +1,14 @@
+namespace BuildingBlocks.Exceptions;
+public class InternalServerException : Exception
+{
+ public InternalServerException(string message) : base(message)
+ {
+ }
+
+ public InternalServerException(string message, string details) : base(message)
+ {
+ Details = details;
+ }
+
+ public string? Details { get; }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Exceptions/NotFoundException.cs b/src/BuildingBlocks/BuildingBlocks/Exceptions/NotFoundException.cs
new file mode 100644
index 00000000..646a3b40
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Exceptions/NotFoundException.cs
@@ -0,0 +1,11 @@
+namespace BuildingBlocks.Exceptions;
+public class NotFoundException : Exception
+{
+ public NotFoundException(string message) : base(message)
+ {
+ }
+
+ public NotFoundException(string name, object key) : base($"Entity \"{name}\" ({key}) was not found.")
+ {
+ }
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Pagination/PaginatedResult.cs b/src/BuildingBlocks/BuildingBlocks/Pagination/PaginatedResult.cs
new file mode 100644
index 00000000..4ebec44b
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Pagination/PaginatedResult.cs
@@ -0,0 +1,10 @@
+namespace BuildingBlocks.Pagination;
+public class PaginatedResult
+ (int pageIndex, int pageSize, long count, IEnumerable data)
+ where TEntity : class
+{
+ public int PageIndex { get; } = pageIndex;
+ public int PageSize { get; } = pageSize;
+ public long Count { get; } = count;
+ public IEnumerable Data { get; } = data;
+}
diff --git a/src/BuildingBlocks/BuildingBlocks/Pagination/PaginationRequest.cs b/src/BuildingBlocks/BuildingBlocks/Pagination/PaginationRequest.cs
new file mode 100644
index 00000000..7a174a98
--- /dev/null
+++ b/src/BuildingBlocks/BuildingBlocks/Pagination/PaginationRequest.cs
@@ -0,0 +1,2 @@
+namespace BuildingBlocks.Pagination;
+public record PaginationRequest(int PageIndex = 0, int PageSize = 10);
diff --git a/src/BuildingBlocks/Common.Logging/Common.Logging.csproj b/src/BuildingBlocks/Common.Logging/Common.Logging.csproj
deleted file mode 100644
index 350909da..00000000
--- a/src/BuildingBlocks/Common.Logging/Common.Logging.csproj
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
- net5.0
-
-
-
-
-
-
-
-
-
diff --git a/src/BuildingBlocks/Common.Logging/LoggingDelegatingHandler.cs b/src/BuildingBlocks/Common.Logging/LoggingDelegatingHandler.cs
deleted file mode 100644
index 27fae0a7..00000000
--- a/src/BuildingBlocks/Common.Logging/LoggingDelegatingHandler.cs
+++ /dev/null
@@ -1,57 +0,0 @@
-using Microsoft.Extensions.Logging;
-using System.Net;
-using System.Net.Http;
-using System.Net.Sockets;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Common.Logging
-{
- public class LoggingDelegatingHandler : DelegatingHandler
- {
- private readonly ILogger logger;
-
- public LoggingDelegatingHandler(ILogger logger)
- {
- this.logger = logger;
- }
-
- protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- try
- {
- logger.LogInformation("Sending request to {Url}", request.RequestUri);
-
- var response = await base.SendAsync(request, cancellationToken);
-
- if (response.IsSuccessStatusCode)
- {
- logger.LogInformation("Received a success response from {Url}", response.RequestMessage.RequestUri);
- }
- else
- {
- logger.LogWarning("Received a non-success status code {StatusCode} from {Url}",
- (int)response.StatusCode, response.RequestMessage.RequestUri);
- }
-
- return response;
- }
- catch (HttpRequestException ex)
- when (ex.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionRefused)
- {
- var hostWithPort = request.RequestUri.IsDefaultPort
- ? request.RequestUri.DnsSafeHost
- : $"{request.RequestUri.DnsSafeHost}:{request.RequestUri.Port}";
-
- logger.LogCritical(ex, "Unable to connect to {Host}. Please check the " +
- "configuration to ensure the correct URL for the service " +
- "has been configured.", hostWithPort);
- }
-
- return new HttpResponseMessage(HttpStatusCode.BadGateway)
- {
- RequestMessage = request
- };
- }
- }
-}
diff --git a/src/BuildingBlocks/Common.Logging/SeriLogger.cs b/src/BuildingBlocks/Common.Logging/SeriLogger.cs
deleted file mode 100644
index 14bbdcf6..00000000
--- a/src/BuildingBlocks/Common.Logging/SeriLogger.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Hosting;
-using Serilog;
-using Serilog.Sinks.Elasticsearch;
-using System;
-
-namespace Common.Logging
-{
- public static class SeriLogger
- {
- public static Action Configure =>
- (context, configuration) =>
- {
- var elasticUri = context.Configuration.GetValue("ElasticConfiguration:Uri");
-
- configuration
- .Enrich.FromLogContext()
- .Enrich.WithMachineName()
- .WriteTo.Debug()
- .WriteTo.Console()
- .WriteTo.Elasticsearch(
- new ElasticsearchSinkOptions(new Uri(elasticUri))
- {
- IndexFormat = $"applogs-{context.HostingEnvironment.ApplicationName?.ToLower().Replace(".", "-")}-{context.HostingEnvironment.EnvironmentName?.ToLower().Replace(".", "-")}-{DateTime.UtcNow:yyyy-MM}",
- AutoRegisterTemplate = true,
- NumberOfShards = 2,
- NumberOfReplicas = 1
- })
- .Enrich.WithProperty("Environment", context.HostingEnvironment.EnvironmentName)
- .Enrich.WithProperty("Application", context.HostingEnvironment.ApplicationName)
- .ReadFrom.Configuration(context.Configuration);
- };
- }
-}
diff --git a/src/BuildingBlocks/EventBus.Messages/Common/EventBusConstants.cs b/src/BuildingBlocks/EventBus.Messages/Common/EventBusConstants.cs
deleted file mode 100644
index a627d2e5..00000000
--- a/src/BuildingBlocks/EventBus.Messages/Common/EventBusConstants.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace EventBus.Messages.Common
-{
- public static class EventBusConstants
- {
- public const string BasketCheckoutQueue = "basketcheckout-queue";
- }
-}
diff --git a/src/BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj b/src/BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj
deleted file mode 100644
index f208d303..00000000
--- a/src/BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- net5.0
-
-
-
diff --git a/src/BuildingBlocks/EventBus.Messages/Events/BasketCheckoutEvent.cs b/src/BuildingBlocks/EventBus.Messages/Events/BasketCheckoutEvent.cs
deleted file mode 100644
index 0282a1ad..00000000
--- a/src/BuildingBlocks/EventBus.Messages/Events/BasketCheckoutEvent.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace EventBus.Messages.Events
-{
- public class BasketCheckoutEvent : IntegrationBaseEvent
- {
- public string UserName { get; set; }
- public decimal TotalPrice { get; set; }
-
- // BillingAddress
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public string EmailAddress { get; set; }
- public string AddressLine { get; set; }
- public string Country { get; set; }
- public string State { get; set; }
- public string ZipCode { get; set; }
-
- // Payment
- public string CardName { get; set; }
- public string CardNumber { get; set; }
- public string Expiration { get; set; }
- public string CVV { get; set; }
- public int PaymentMethod { get; set; }
- }
-}
diff --git a/src/BuildingBlocks/EventBus.Messages/Events/IntegrationBaseEvent.cs b/src/BuildingBlocks/EventBus.Messages/Events/IntegrationBaseEvent.cs
deleted file mode 100644
index 6ca86e81..00000000
--- a/src/BuildingBlocks/EventBus.Messages/Events/IntegrationBaseEvent.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System;
-
-namespace EventBus.Messages.Events
-{
- public class IntegrationBaseEvent
- {
- public IntegrationBaseEvent()
- {
- Id = Guid.NewGuid();
- CreationDate = DateTime.UtcNow;
- }
-
- public IntegrationBaseEvent(Guid id, DateTime createDate)
- {
- Id = id;
- CreationDate = createDate;
- }
-
- public Guid Id { get; private set; }
-
- public DateTime CreationDate { get; private set; }
- }
-}
diff --git a/src/EShopMicroservices.postman_collection.json b/src/EShopMicroservices.postman_collection.json
new file mode 100644
index 00000000..4087e116
--- /dev/null
+++ b/src/EShopMicroservices.postman_collection.json
@@ -0,0 +1,730 @@
+{
+ "info": {
+ "_postman_id": "f00e126b-3521-4034-b400-b790fc45d65a",
+ "name": "EShopMicroservices",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
+ "_exporter_id": "2848550"
+ },
+ "item": [
+ {
+ "name": "Catalog",
+ "item": [
+ {
+ "name": "GET Product",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{catalog_url}}/products?pageNumber=1&pageSize=5",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products"
+ ],
+ "query": [
+ {
+ "key": "pageNumber",
+ "value": "1"
+ },
+ {
+ "key": "pageSize",
+ "value": "5"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET ProductById",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{catalog_url}}/products/018d2605-1fef-4cbf-b9b4-cbf999a85cc1",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products",
+ "018d2605-1fef-4cbf-b9b4-cbf999a85cc1"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET ProductByCategory",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{catalog_url}}/products/category/c2",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products",
+ "category",
+ "c2"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Product",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"Name\": \"New Product A\",\r\n \"Category\": [\"c1\", \"c2\"],\r\n \"Description\": \"Description Product A\",\r\n \"ImageFile\": \"ImageFile Product A\",\r\n \"Price\": 199\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{catalog_url}}/products",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "PUT Product",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"Id\": \"018d2605-1fef-4cbf-b9b4-cbf999a85cc1\",\r\n \"Name\": \"New Product A Updated\",\r\n \"Category\": [\"c1\", \"c2\", \"cUpdated\"],\r\n \"Description\": \"Description Product A Updated\",\r\n \"ImageFile\": \"ImageFile Product A Updated\",\r\n \"Price\": 299\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{catalog_url}}/products",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "DELETE Product",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "{{catalog_url}}/products/018d2605-1fef-4cbf-b9b4-cbf999a85cc1",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "products",
+ "018d2605-1fef-4cbf-b9b4-cbf999a85cc1"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Health",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{catalog_url}}/health",
+ "host": [
+ "{{catalog_url}}"
+ ],
+ "path": [
+ "health"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Basket",
+ "item": [
+ {
+ "name": "GET BasketByUsername",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{basket_url}}/basket/swn",
+ "host": [
+ "{{basket_url}}"
+ ],
+ "path": [
+ "basket",
+ "swn"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Store Basket",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"Cart\": \r\n {\r\n \"UserName\": \"swn\",\r\n \"Items\": [\r\n {\r\n \"Quantity\": 2,\r\n \"Color\": \"Red\",\r\n \"Price\": 500,\r\n \"ProductId\": \"5334c996-8457-4cf0-815c-ed2b77c4ff61\",\r\n \"ProductName\": \"IPhone X\"\r\n },\r\n {\r\n \"Quantity\": 1,\r\n \"Color\": \"Blue\",\r\n \"Price\": 500,\r\n \"ProductId\": \"c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914\",\r\n \"ProductName\": \"Samsung 10\"\r\n }\r\n ] \r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{basket_url}}/basket",
+ "host": [
+ "{{basket_url}}"
+ ],
+ "path": [
+ "basket"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "DELETE BasketByUsername",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "{{basket_url}}/basket/swn",
+ "host": [
+ "{{basket_url}}"
+ ],
+ "path": [
+ "basket",
+ "swn"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Checkout Basket",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"BasketCheckoutDto\": \r\n {\r\n\t\t \"userName\": \"swn\",\r\n\t\t \"CustomerId\": \"189dc8dc-990f-48e0-a37b-e6f2b60b9d7d\",\r\n\t\t \"totalPrice\": 0,\r\n\t\t \"firstName\": \"swn\",\r\n\t\t \"lastName\": \"swn\",\r\n\t\t \"emailAddress\": \"test@test.com\",\r\n\t\t \"addressLine\": \"34 Charles Street\",\r\n\t\t \"country\": \"USA\",\r\n\t\t \"state\": \"Michigan\",\r\n\t\t \"zipCode\": \"48198\",\r\n\t\t \"cardName\": \"swn\",\r\n\t\t \"cardNumber\": \"485-3184\",\r\n\t\t \"expiration\": \"11/30\",\r\n\t\t \"cvv\": \"333\",\r\n\t\t \"paymentMethod\": 1\r\n\t\t}\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{basket_url}}/basket/checkout",
+ "host": [
+ "{{basket_url}}"
+ ],
+ "path": [
+ "basket",
+ "checkout"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Health",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{basket_url}}/health",
+ "host": [
+ "{{basket_url}}"
+ ],
+ "path": [
+ "health"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Discount",
+ "item": []
+ },
+ {
+ "name": "Ordering",
+ "item": [
+ {
+ "name": "POST Order",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"Order\": \r\n {\r\n \t\"CustomerId\": \"58c49479-ec65-4de2-86e7-033c546291aa\",\r\n \"OrderName\": \"ORD_4\",\r\n \"ShippingAddress\": \r\n \t{\r\n \t\t\"FirstName\": \"Valentina\",\r\n\t \"LastName\": \"Legros\",\r\n\t \"EmailAddress\": \"valeg@xmail.com\",\r\n\t \"AddressLine\": \"Wiegand Pass Suite 825\",\r\n\t \"Country\": \"USA\",\r\n\t \"State\": \"Oklahoma\",\r\n\t \"ZipCode\": \"41248\"\r\n \t},\r\n \"BillingAddress\": \r\n \t{\r\n \t\t\"FirstName\": \"Valentina\",\r\n\t \"LastName\": \"Legros\",\r\n\t \"EmailAddress\": \"valeg@xmail.com\",\r\n\t \"AddressLine\": \"Wiegand Pass Suite 825\",\r\n\t \"Country\": \"USA\",\r\n\t \"State\": \"Oklahoma\",\r\n\t \"ZipCode\": \"41248\"\r\n \t},\r\n \"Payment\": \r\n \t{\r\n \t\t\"CardName\": \"CARD_1\",\r\n\t \"CardNumber\": \"875-444-3739\",\r\n\t \"Expiration\": \"12/29\",\r\n\t \"Cvv\": \"455\",\r\n\t \"PaymentMethod\": 1\r\n \t},\r\n \"Status\": 2,\r\n \"OrderItems\": [\r\n {\r\n\t \"ProductId\": \"5334c996-8457-4cf0-815c-ed2b77c4ff61\",\r\n\t //\"ProductName\": \"IPhone X\",\r\n\t \"Quantity\": 2,\r\n\t \"Price\": 500\r\n },\r\n {\r\n\t \"ProductId\": \"c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914\",\r\n\t //\"ProductName\": \"Samsung 10\",\r\n\t \"Quantity\": 1,\r\n\t \"Price\": 500\r\n }\r\n ] \r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{ordering_url}}/orders",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "PUT Order",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"Order\": \r\n {\r\n \"Id\": \"354c00f6-b04c-46ed-8d6d-f9f42d7bdf98\",\r\n \t\"CustomerId\": \"58c49479-ec65-4de2-86e7-033c546291aa\",\r\n \"OrderName\": \"ORD_5\",\r\n \"ShippingAddress\": \r\n \t{\r\n \t\t\"FirstName\": \"Mehmet\",\r\n\t \"LastName\": \"Legros\",\r\n\t \"EmailAddress\": \"valeg@xmail.com\",\r\n\t \"AddressLine\": \"Wiegand Pass Suite 825\",\r\n\t \"Country\": \"USA\",\r\n\t \"State\": \"New York\",\r\n\t \"ZipCode\": \"41248\"\r\n \t},\r\n \"BillingAddress\": \r\n \t{\r\n \t\t\"FirstName\": \"Mehmet\",\r\n\t \"LastName\": \"Legros\",\r\n\t \"EmailAddress\": \"valeg@xmail.com\",\r\n\t \"AddressLine\": \"Wiegand Pass Suite 825\",\r\n\t \"Country\": \"USA\",\r\n\t \"State\": \"Oklahoma\",\r\n\t \"ZipCode\": \"41248\"\r\n \t},\r\n \"Payment\": \r\n \t{\r\n \t\t\"CardName\": \"CARD_2\",\r\n\t \"CardNumber\": \"875-444-3739\",\r\n\t \"Expiration\": \"12/29\",\r\n\t \"CVV\": \"455\",\r\n\t \"PaymentMethod\": 1\r\n \t},\r\n \"Status\": 2,\r\n \"OrderItems\": [\r\n {\r\n\t \"ProductId\": \"5334c996-8457-4cf0-815c-ed2b77c4ff61\",\r\n\t //\"ProductName\": \"IPhone X\",\r\n\t \"Quantity\": 2,\r\n\t \"Price\": 500\r\n },\r\n {\r\n\t \"ProductId\": \"c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914\",\r\n\t //\"ProductName\": \"Samsung 10\",\r\n\t \"Quantity\": 1,\r\n\t \"Price\": 500\r\n }\r\n ] \r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{ordering_url}}/orders",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET Orders w/ Pagination",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{ordering_url}}/orders?PageIndex=0&PageSize=2",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders"
+ ],
+ "query": [
+ {
+ "key": "PageIndex",
+ "value": "0"
+ },
+ {
+ "key": "PageSize",
+ "value": "2"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET Orders by Name",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{ordering_url}}/orders/ORD_2",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders",
+ "ORD_2"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET Orders by Customer",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{ordering_url}}/orders/customer/58c49479-ec65-4de2-86e7-033c546291aa",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders",
+ "customer",
+ "58c49479-ec65-4de2-86e7-033c546291aa"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "DELETE Order",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "{{ordering_url}}/orders/354c00f6-b04c-46ed-8d6d-f9f42d7bdf98",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "orders",
+ "354c00f6-b04c-46ed-8d6d-f9f42d7bdf98"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Health",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{ordering_url}}/health",
+ "host": [
+ "{{ordering_url}}"
+ ],
+ "path": [
+ "health"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "YarpApiGateway",
+ "item": [
+ {
+ "name": "Catalog",
+ "item": [
+ {
+ "name": "GET Product",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/catalog-service/products?pageNumber=1&pageSize=5",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "catalog-service",
+ "products"
+ ],
+ "query": [
+ {
+ "key": "pageNumber",
+ "value": "1"
+ },
+ {
+ "key": "pageSize",
+ "value": "5"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET ProductById",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/catalog-service/products/5334c996-8457-4cf0-815c-ed2b77c4ff61",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "catalog-service",
+ "products",
+ "5334c996-8457-4cf0-815c-ed2b77c4ff61"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET ProductByCategory",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/catalog-service/products/category/c2",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "catalog-service",
+ "products",
+ "category",
+ "c2"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Product",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n \"Name\": \"New Product A\",\r\n \"Category\": [\"c1\", \"c2\"],\r\n \"Description\": \"Description Product A\",\r\n \"ImageFile\": \"ImageFile Product A\",\r\n \"Price\": 199\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{yarp_url}}/catalog-service/products",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "catalog-service",
+ "products"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "DELETE Product",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/catalog-service/products/018d2605-1fef-4cbf-b9b4-cbf999a85cc1",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "catalog-service",
+ "products",
+ "018d2605-1fef-4cbf-b9b4-cbf999a85cc1"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Basket",
+ "item": [
+ {
+ "name": "GET BasketByUsername",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/basket-service/basket/swn",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "basket-service",
+ "basket",
+ "swn"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Store Basket",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"Cart\": \r\n {\r\n \"UserName\": \"swn\",\r\n \"Items\": [\r\n {\r\n \"Quantity\": 2,\r\n \"Color\": \"Red\",\r\n \"Price\": 500,\r\n \"ProductId\": \"5334c996-8457-4cf0-815c-ed2b77c4ff61\",\r\n \"ProductName\": \"IPhone X\"\r\n },\r\n {\r\n \"Quantity\": 1,\r\n \"Color\": \"Blue\",\r\n \"Price\": 500,\r\n \"ProductId\": \"c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914\",\r\n \"ProductName\": \"Samsung 10\"\r\n }\r\n ] \r\n }\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{yarp_url}}/basket-service/basket",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "basket-service",
+ "basket"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "DELETE BasketByUsername",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/basket-service/basket/swn",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "basket-service",
+ "basket",
+ "swn"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "POST Checkout Basket",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\r\n\t\"BasketCheckoutDto\": \r\n {\r\n\t\t \"userName\": \"swn\",\r\n\t\t \"CustomerId\": \"189dc8dc-990f-48e0-a37b-e6f2b60b9d7d\",\r\n\t\t \"totalPrice\": 0,\r\n\t\t \"firstName\": \"swn\",\r\n\t\t \"lastName\": \"swn\",\r\n\t\t \"emailAddress\": \"test@test.com\",\r\n\t\t \"addressLine\": \"34 Charles Street\",\r\n\t\t \"country\": \"USA\",\r\n\t\t \"state\": \"Michigan\",\r\n\t\t \"zipCode\": \"48198\",\r\n\t\t \"cardName\": \"swn\",\r\n\t\t \"cardNumber\": \"485-3184\",\r\n\t\t \"expiration\": \"11/30\",\r\n\t\t \"cvv\": \"333\",\r\n\t\t \"paymentMethod\": 1\r\n\t\t}\r\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "{{yarp_url}}/basket-service/basket/checkout",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "basket-service",
+ "basket",
+ "checkout"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ },
+ {
+ "name": "Ordering",
+ "item": [
+ {
+ "name": "GET Orders w/ Pagination",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/ordering-service/orders?PageIndex=0&PageSize=2",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "ordering-service",
+ "orders"
+ ],
+ "query": [
+ {
+ "key": "PageIndex",
+ "value": "0"
+ },
+ {
+ "key": "PageSize",
+ "value": "2"
+ }
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET Orders by Name",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/ordering-service/orders/ORD_2",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "ordering-service",
+ "orders",
+ "ORD_2"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "GET Orders by Customer",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "{{yarp_url}}/ordering-service/orders/customer/58c49479-ec65-4de2-86e7-033c546291aa",
+ "host": [
+ "{{yarp_url}}"
+ ],
+ "path": [
+ "ordering-service",
+ "orders",
+ "customer",
+ "58c49479-ec65-4de2-86e7-033c546291aa"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/src/Services/Basket/Basket.API/Basket - Backup.API.csproj b/src/Services/Basket/Basket.API/Basket - Backup.API.csproj
deleted file mode 100644
index 3dfd4441..00000000
--- a/src/Services/Basket/Basket.API/Basket - Backup.API.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net5.0
- ..\..\..\docker-compose.dcproj
-
-
-
-
-
-
-
-
-
diff --git a/src/Services/Basket/Basket.API/Basket.API.csproj b/src/Services/Basket/Basket.API/Basket.API.csproj
index a071d5c1..f42f6765 100644
--- a/src/Services/Basket/Basket.API/Basket.API.csproj
+++ b/src/Services/Basket/Basket.API/Basket.API.csproj
@@ -1,29 +1,30 @@
- net5.0
- ..\..\..\docker-compose.dcproj
+ net8.0
+ enable
+ enable
+ 36d99f52-5397-4bcb-b3cf-8b3a5e74081d
Linux
..\..\..
+ ..\..\..\docker-compose.dcproj
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
diff --git a/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketEndpoints.cs b/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketEndpoints.cs
new file mode 100644
index 00000000..643515b6
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketEndpoints.cs
@@ -0,0 +1,26 @@
+namespace Basket.API.Basket.CheckoutBasket;
+
+public record CheckoutBasketRequest(BasketCheckoutDto BasketCheckoutDto);
+public record CheckoutBasketResponse(bool IsSuccess);
+
+public class CheckoutBasketEndpoints : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapPost("/basket/checkout", async (CheckoutBasketRequest request, ISender sender) =>
+ {
+ var command = request.Adapt();
+
+ var result = await sender.Send(command);
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("CheckoutBasket")
+ .Produces(StatusCodes.Status201Created)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Checkout Basket")
+ .WithDescription("Checkout Basket");
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketHandler.cs b/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketHandler.cs
new file mode 100644
index 00000000..d54703a4
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/CheckoutBasket/CheckoutBasketHandler.cs
@@ -0,0 +1,47 @@
+
+using BuildingBlocks.Messaging.Events;
+using MassTransit;
+
+namespace Basket.API.Basket.CheckoutBasket;
+
+public record CheckoutBasketCommand(BasketCheckoutDto BasketCheckoutDto)
+ : ICommand;
+public record CheckoutBasketResult(bool IsSuccess);
+
+public class CheckoutBasketCommandValidator
+ : AbstractValidator
+{
+ public CheckoutBasketCommandValidator()
+ {
+ RuleFor(x => x.BasketCheckoutDto).NotNull().WithMessage("BasketCheckoutDto can't be null");
+ RuleFor(x => x.BasketCheckoutDto.UserName).NotEmpty().WithMessage("UserName is required");
+ }
+}
+
+public class CheckoutBasketCommandHandler
+ (IBasketRepository repository, IPublishEndpoint publishEndpoint)
+ : ICommandHandler
+{
+ public async Task Handle(CheckoutBasketCommand command, CancellationToken cancellationToken)
+ {
+ // get existing basket with total price
+ // Set totalprice on basketcheckout event message
+ // send basket checkout event to rabbitmq using masstransit
+ // delete the basket
+
+ var basket = await repository.GetBasket(command.BasketCheckoutDto.UserName, cancellationToken);
+ if (basket == null)
+ {
+ return new CheckoutBasketResult(false);
+ }
+
+ var eventMessage = command.BasketCheckoutDto.Adapt();
+ eventMessage.TotalPrice = basket.TotalPrice;
+
+ await publishEndpoint.Publish(eventMessage, cancellationToken);
+
+ await repository.DeleteBasket(command.BasketCheckoutDto.UserName, cancellationToken);
+
+ return new CheckoutBasketResult(true);
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketEndpoints.cs b/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketEndpoints.cs
new file mode 100644
index 00000000..91747529
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketEndpoints.cs
@@ -0,0 +1,25 @@
+namespace Basket.API.Basket.DeleteBasket;
+
+//public record DeleteBasketRequest(string UserName);
+public record DeleteBasketResponse(bool IsSuccess);
+
+public class DeleteBasketEndpoints : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapDelete("/basket/{userName}", async (string userName, ISender sender) =>
+ {
+ var result = await sender.Send(new DeleteBasketCommand(userName));
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("DeleteProduct")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Delete Product")
+ .WithDescription("Delete Product");
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketHandler.cs b/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketHandler.cs
new file mode 100644
index 00000000..bc3bbd7f
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/DeleteBasket/DeleteBasketHandler.cs
@@ -0,0 +1,24 @@
+namespace Basket.API.Basket.DeleteBasket;
+
+public record DeleteBasketCommand(string UserName) : ICommand;
+public record DeleteBasketResult(bool IsSuccess);
+
+public class DeleteBasketCommandValidator : AbstractValidator
+{
+ public DeleteBasketCommandValidator()
+ {
+ RuleFor(x => x.UserName).NotEmpty().WithMessage("UserName is required");
+ }
+}
+
+public class DeleteBasketCommandHandler(IBasketRepository repository)
+ : ICommandHandler
+{
+ public async Task Handle(DeleteBasketCommand command, CancellationToken cancellationToken)
+ {
+ // TODO: delete basket from database and cache
+ await repository.DeleteBasket(command.UserName, cancellationToken);
+
+ return new DeleteBasketResult(true);
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketEndpoints.cs b/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketEndpoints.cs
new file mode 100644
index 00000000..e84aa472
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketEndpoints.cs
@@ -0,0 +1,24 @@
+namespace Basket.API.Basket.GetBasket;
+
+//public record GetBasketRequest(string UserName);
+public record GetBasketResponse(ShoppingCart Cart);
+
+public class GetBasketEndpoints : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapGet("/basket/{userName}", async (string userName, ISender sender) =>
+ {
+ var result = await sender.Send(new GetBasketQuery(userName));
+
+ var respose = result.Adapt();
+
+ return Results.Ok(respose);
+ })
+ .WithName("GetProductById")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Get Product By Id")
+ .WithDescription("Get Product By Id");
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketHandler.cs b/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketHandler.cs
new file mode 100644
index 00000000..ac227600
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/GetBasket/GetBasketHandler.cs
@@ -0,0 +1,15 @@
+namespace Basket.API.Basket.GetBasket;
+
+public record GetBasketQuery(string UserName) : IQuery;
+public record GetBasketResult(ShoppingCart Cart);
+
+public class GetBasketQueryHandler(IBasketRepository repository)
+ : IQueryHandler
+{
+ public async Task Handle(GetBasketQuery query, CancellationToken cancellationToken)
+ {
+ var basket = await repository.GetBasket(query.UserName);
+
+ return new GetBasketResult(basket);
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketEndpoints.cs b/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketEndpoints.cs
new file mode 100644
index 00000000..8c5389d9
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketEndpoints.cs
@@ -0,0 +1,26 @@
+namespace Basket.API.Basket.StoreBasket;
+
+public record StoreBasketRequest(ShoppingCart Cart);
+public record StoreBasketResponse(string UserName);
+
+public class StoreBasketEndpoints : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapPost("/basket", async (StoreBasketRequest request, ISender sender) =>
+ {
+ var command = request.Adapt();
+
+ var result = await sender.Send(command);
+
+ var response = result.Adapt();
+
+ return Results.Created($"/basket/{response.UserName}", response);
+ })
+ .WithName("CreateProduct")
+ .Produces(StatusCodes.Status201Created)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Create Product")
+ .WithDescription("Create Product");
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketHandler.cs b/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketHandler.cs
new file mode 100644
index 00000000..1b8b9df2
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Basket/StoreBasket/StoreBasketHandler.cs
@@ -0,0 +1,39 @@
+using Discount.Grpc;
+
+namespace Basket.API.Basket.StoreBasket;
+
+public record StoreBasketCommand(ShoppingCart Cart) : ICommand;
+public record StoreBasketResult(string UserName);
+
+public class StoreBasketCommandValidator : AbstractValidator
+{
+ public StoreBasketCommandValidator()
+ {
+ RuleFor(x => x.Cart).NotNull().WithMessage("Cart can not be null");
+ RuleFor(x => x.Cart.UserName).NotEmpty().WithMessage("UserName is required");
+ }
+}
+
+public class StoreBasketCommandHandler
+ (IBasketRepository repository, DiscountProtoService.DiscountProtoServiceClient discountProto)
+ : ICommandHandler
+{
+ public async Task Handle(StoreBasketCommand command, CancellationToken cancellationToken)
+ {
+ await DeductDiscount(command.Cart, cancellationToken);
+
+ await repository.StoreBasket(command.Cart, cancellationToken);
+
+ return new StoreBasketResult(command.Cart.UserName);
+ }
+
+ private async Task DeductDiscount(ShoppingCart cart, CancellationToken cancellationToken)
+ {
+ // Communicate with Discount.Grpc and calculate lastest prices of products into sc
+ foreach (var item in cart.Items)
+ {
+ var coupon = await discountProto.GetDiscountAsync(new GetDiscountRequest { ProductName = item.ProductName }, cancellationToken: cancellationToken);
+ item.Price -= coupon.Amount;
+ }
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Controllers/BasketController.cs b/src/Services/Basket/Basket.API/Controllers/BasketController.cs
deleted file mode 100644
index fbdb798d..00000000
--- a/src/Services/Basket/Basket.API/Controllers/BasketController.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using AutoMapper;
-using Basket.API.Entities;
-using Basket.API.GrpcServices;
-using Basket.API.Repositories.Interfaces;
-using EventBus.Messages.Events;
-using MassTransit;
-using Microsoft.AspNetCore.Mvc;
-using System;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace Basket.API.Controllers
-{
- [ApiController]
- [Route("api/v1/[controller]")]
- public class BasketController : ControllerBase
- {
- private readonly IBasketRepository _repository;
- private readonly DiscountGrpcService _discountGrpcService;
- private readonly IPublishEndpoint _publishEndpoint;
- private readonly IMapper _mapper;
-
- public BasketController(IBasketRepository repository, DiscountGrpcService discountGrpcService, IPublishEndpoint publishEndpoint, IMapper mapper)
- {
- _repository = repository ?? throw new ArgumentNullException(nameof(repository));
- _discountGrpcService = discountGrpcService ?? throw new ArgumentNullException(nameof(discountGrpcService));
- _publishEndpoint = publishEndpoint ?? throw new ArgumentNullException(nameof(publishEndpoint));
- _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
- }
-
- [HttpGet("{userName}", Name = "GetBasket")]
- [ProducesResponseType(typeof(ShoppingCart), (int)HttpStatusCode.OK)]
- public async Task> GetBasket(string userName)
- {
- var basket = await _repository.GetBasket(userName);
- return Ok(basket ?? new ShoppingCart(userName));
- }
-
- [HttpPost]
- [ProducesResponseType(typeof(ShoppingCart), (int)HttpStatusCode.OK)]
- public async Task> UpdateBasket([FromBody] ShoppingCart basket)
- {
- // Communicate with Discount.Grpc and calculate lastest prices of products into sc
- foreach (var item in basket.Items)
- {
- var coupon = await _discountGrpcService.GetDiscount(item.ProductName);
- item.Price -= coupon.Amount;
- }
-
- return Ok(await _repository.UpdateBasket(basket));
- }
-
- [HttpDelete("{userName}", Name = "DeleteBasket")]
- [ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)]
- public async Task DeleteBasket(string userName)
- {
- await _repository.DeleteBasket(userName);
- return Ok();
- }
-
- [Route("[action]")]
- [HttpPost]
- [ProducesResponseType((int)HttpStatusCode.Accepted)]
- [ProducesResponseType((int)HttpStatusCode.BadRequest)]
- public async Task Checkout([FromBody] BasketCheckout basketCheckout)
- {
- // get existing basket with total price
- // Set TotalPrice on basketCheckout eventMessage
- // send checkout event to rabbitmq
- // remove the basket
-
- // get existing basket with total price
- var basket = await _repository.GetBasket(basketCheckout.UserName);
- if (basket == null)
- {
- return BadRequest();
- }
-
- // send checkout event to rabbitmq
- var eventMessage = _mapper.Map(basketCheckout);
- eventMessage.TotalPrice = basket.TotalPrice;
- await _publishEndpoint.Publish(eventMessage);
-
- // remove the basket
- await _repository.DeleteBasket(basket.UserName);
-
- return Accepted();
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Data/BasketRepository.cs b/src/Services/Basket/Basket.API/Data/BasketRepository.cs
new file mode 100644
index 00000000..bcc81fc0
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Data/BasketRepository.cs
@@ -0,0 +1,26 @@
+namespace Basket.API.Data;
+
+public class BasketRepository(IDocumentSession session)
+ : IBasketRepository
+{
+ public async Task GetBasket(string userName, CancellationToken cancellationToken = default)
+ {
+ var basket = await session.LoadAsync(userName, cancellationToken);
+
+ return basket is null ? throw new BasketNotFoundException(userName) : basket;
+ }
+
+ public async Task StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
+ {
+ session.Store(basket);
+ await session.SaveChangesAsync(cancellationToken);
+ return basket;
+ }
+
+ public async Task DeleteBasket(string userName, CancellationToken cancellationToken = default)
+ {
+ session.Delete(userName);
+ await session.SaveChangesAsync(cancellationToken);
+ return true;
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Data/CachedBasketRepository.cs b/src/Services/Basket/Basket.API/Data/CachedBasketRepository.cs
new file mode 100644
index 00000000..ca57e811
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Data/CachedBasketRepository.cs
@@ -0,0 +1,38 @@
+using Microsoft.Extensions.Caching.Distributed;
+using System.Text.Json;
+
+namespace Basket.API.Data;
+
+public class CachedBasketRepository
+ (IBasketRepository repository, IDistributedCache cache)
+ : IBasketRepository
+{
+ public async Task GetBasket(string userName, CancellationToken cancellationToken = default)
+ {
+ var cachedBasket = await cache.GetStringAsync(userName, cancellationToken);
+ if (!string.IsNullOrEmpty(cachedBasket))
+ return JsonSerializer.Deserialize(cachedBasket)!;
+
+ var basket = await repository.GetBasket(userName, cancellationToken);
+ await cache.SetStringAsync(userName, JsonSerializer.Serialize(basket), cancellationToken);
+ return basket;
+ }
+
+ public async Task StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default)
+ {
+ await repository.StoreBasket(basket, cancellationToken);
+
+ await cache.SetStringAsync(basket.UserName, JsonSerializer.Serialize(basket), cancellationToken);
+
+ return basket;
+ }
+
+ public async Task DeleteBasket(string userName, CancellationToken cancellationToken = default)
+ {
+ await repository.DeleteBasket(userName, cancellationToken);
+
+ await cache.RemoveAsync(userName, cancellationToken);
+
+ return true;
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Data/IBasketRepository.cs b/src/Services/Basket/Basket.API/Data/IBasketRepository.cs
new file mode 100644
index 00000000..60deec7b
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Data/IBasketRepository.cs
@@ -0,0 +1,8 @@
+namespace Basket.API.Data;
+
+public interface IBasketRepository
+{
+ Task GetBasket(string userName, CancellationToken cancellationToken = default);
+ Task StoreBasket(ShoppingCart basket, CancellationToken cancellationToken = default);
+ Task DeleteBasket(string userName, CancellationToken cancellationToken = default);
+}
diff --git a/src/Services/Basket/Basket.API/Dockerfile b/src/Services/Basket/Basket.API/Dockerfile
index 29562421..41fd436d 100644
--- a/src/Services/Basket/Basket.API/Dockerfile
+++ b/src/Services/Basket/Basket.API/Dockerfile
@@ -1,21 +1,24 @@
-#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
-FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
WORKDIR /app
-EXPOSE 80
+EXPOSE 8080
+EXPOSE 8081
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Services/Basket/Basket.API/Basket.API.csproj", "Services/Basket/Basket.API/"]
-COPY ["BuildingBlocks/EventBus.Messages/EventBus.Messages.csproj", "BuildingBlocks/EventBus.Messages/"]
-COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"]
-RUN dotnet restore "Services/Basket/Basket.API/Basket.API.csproj"
+COPY ["BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj", "BuildingBlocks/BuildingBlocks/"]
+RUN dotnet restore "./Services/Basket/Basket.API/./Basket.API.csproj"
COPY . .
WORKDIR "/src/Services/Basket/Basket.API"
-RUN dotnet build "Basket.API.csproj" -c Release -o /app/build
+RUN dotnet build "./Basket.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
-RUN dotnet publish "Basket.API.csproj" -c Release -o /app/publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Basket.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
diff --git a/src/Services/Basket/Basket.API/Dockerfile.original b/src/Services/Basket/Basket.API/Dockerfile.original
new file mode 100644
index 00000000..9c8aa950
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Dockerfile.original
@@ -0,0 +1,25 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Services/Basket/Basket.API/Basket.API.csproj", "Services/Basket/Basket.API/"]
+RUN dotnet restore "./Services/Basket/Basket.API/./Basket.API.csproj"
+COPY . .
+WORKDIR "/src/Services/Basket/Basket.API"
+RUN dotnet build "./Basket.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Basket.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Basket.API.dll"]
\ No newline at end of file
diff --git a/src/Services/Basket/Basket.API/Dtos/BasketCheckoutDto.cs b/src/Services/Basket/Basket.API/Dtos/BasketCheckoutDto.cs
new file mode 100644
index 00000000..4cb52d6d
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Dtos/BasketCheckoutDto.cs
@@ -0,0 +1,24 @@
+namespace Basket.API.Dtos;
+
+public class BasketCheckoutDto
+{
+ public string UserName { get; set; } = default!;
+ public Guid CustomerId { get; set; } = default!;
+ public decimal TotalPrice { get; set; } = default!;
+
+ // Shipping and BillingAddress
+ public string FirstName { get; set; } = default!;
+ public string LastName { get; set; } = default!;
+ public string EmailAddress { get; set; } = default!;
+ public string AddressLine { get; set; } = default!;
+ public string Country { get; set; } = default!;
+ public string State { get; set; } = default!;
+ public string ZipCode { get; set; } = default!;
+
+ // Payment
+ public string CardName { get; set; } = default!;
+ public string CardNumber { get; set; } = default!;
+ public string Expiration { get; set; } = default!;
+ public string CVV { get; set; } = default!;
+ public int PaymentMethod { get; set; } = default!;
+}
diff --git a/src/Services/Basket/Basket.API/Entities/BasketCheckout.cs b/src/Services/Basket/Basket.API/Entities/BasketCheckout.cs
deleted file mode 100644
index a8c1af61..00000000
--- a/src/Services/Basket/Basket.API/Entities/BasketCheckout.cs
+++ /dev/null
@@ -1,29 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Basket.API.Entities
-{
- public class BasketCheckout
- {
- public string UserName { get; set; }
- public decimal TotalPrice { get; set; }
-
- // BillingAddress
- public string FirstName { get; set; }
- public string LastName { get; set; }
- public string EmailAddress { get; set; }
- public string AddressLine { get; set; }
- public string Country { get; set; }
- public string State { get; set; }
- public string ZipCode { get; set; }
-
- // Payment
- public string CardName { get; set; }
- public string CardNumber { get; set; }
- public string Expiration { get; set; }
- public string CVV { get; set; }
- public int PaymentMethod { get; set; }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Entities/ShoppingCart.cs b/src/Services/Basket/Basket.API/Entities/ShoppingCart.cs
deleted file mode 100644
index 2e03dc57..00000000
--- a/src/Services/Basket/Basket.API/Entities/ShoppingCart.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using System.Collections.Generic;
-
-namespace Basket.API.Entities
-{
- public class ShoppingCart
- {
- public string UserName { get; set; }
- public List Items { get; set; } = new List();
-
- public ShoppingCart()
- {
- }
-
- public ShoppingCart(string userName)
- {
- UserName = userName;
- }
-
- public decimal TotalPrice
- {
- get
- {
- decimal totalprice = 0;
- foreach (var item in Items)
- {
- totalprice += item.Price * item.Quantity;
- }
- return totalprice;
- }
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Entities/ShoppingCartItem.cs b/src/Services/Basket/Basket.API/Entities/ShoppingCartItem.cs
deleted file mode 100644
index 77cdd8e9..00000000
--- a/src/Services/Basket/Basket.API/Entities/ShoppingCartItem.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Basket.API.Entities
-{
- public class ShoppingCartItem
- {
- public int Quantity { get; set; }
- public string Color { get; set; }
- public decimal Price { get; set; }
- public string ProductId { get; set; }
- public string ProductName { get; set; }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Exceptions/BasketNotFoundException.cs b/src/Services/Basket/Basket.API/Exceptions/BasketNotFoundException.cs
new file mode 100644
index 00000000..309801e4
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Exceptions/BasketNotFoundException.cs
@@ -0,0 +1,9 @@
+namespace Basket.API.Exceptions;
+
+public class BasketNotFoundException : NotFoundException
+{
+ public BasketNotFoundException(string userName) : base("Basket", userName)
+ {
+
+ }
+}
diff --git a/src/Services/Basket/Basket.API/GlobalUsing.cs b/src/Services/Basket/Basket.API/GlobalUsing.cs
new file mode 100644
index 00000000..3e1b8e54
--- /dev/null
+++ b/src/Services/Basket/Basket.API/GlobalUsing.cs
@@ -0,0 +1,13 @@
+global using Basket.API.Models;
+global using BuildingBlocks.CQRS;
+global using Carter;
+global using MediatR;
+global using Mapster;
+global using FluentValidation;
+global using BuildingBlocks.Behaviors;
+global using BuildingBlocks.Exceptions;
+global using Basket.API.Exceptions;
+global using Marten;
+global using Basket.API.Data;
+global using BuildingBlocks.Exceptions.Handler;
+global using Basket.API.Dtos;
\ No newline at end of file
diff --git a/src/Services/Basket/Basket.API/GrpcServices/DiscountGrpcService.cs b/src/Services/Basket/Basket.API/GrpcServices/DiscountGrpcService.cs
deleted file mode 100644
index 584cceb1..00000000
--- a/src/Services/Basket/Basket.API/GrpcServices/DiscountGrpcService.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Discount.Grpc.Protos;
-using System;
-using System.Threading.Tasks;
-
-namespace Basket.API.GrpcServices
-{
- public class DiscountGrpcService
- {
- private readonly DiscountProtoService.DiscountProtoServiceClient _discountProtoService;
-
- public DiscountGrpcService(DiscountProtoService.DiscountProtoServiceClient discountProtoService)
- {
- _discountProtoService = discountProtoService ?? throw new ArgumentNullException(nameof(discountProtoService));
- }
-
- public async Task GetDiscount(string productName)
- {
- var discountRequest = new GetDiscountRequest { ProductName = productName };
-
- return await _discountProtoService.GetDiscountAsync(discountRequest);
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Mapper/BasketProfile.cs b/src/Services/Basket/Basket.API/Mapper/BasketProfile.cs
deleted file mode 100644
index 6d06248d..00000000
--- a/src/Services/Basket/Basket.API/Mapper/BasketProfile.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using AutoMapper;
-using Basket.API.Entities;
-using EventBus.Messages.Events;
-
-namespace Basket.API.Mapper
-{
- public class BasketProfile : Profile
- {
- public BasketProfile()
- {
- CreateMap().ReverseMap();
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Models/ShoppingCart.cs b/src/Services/Basket/Basket.API/Models/ShoppingCart.cs
new file mode 100644
index 00000000..870888a8
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Models/ShoppingCart.cs
@@ -0,0 +1,18 @@
+namespace Basket.API.Models;
+
+public class ShoppingCart
+{
+ public string UserName { get; set; } = default!;
+ public List Items { get; set; } = new();
+ public decimal TotalPrice => Items.Sum(x => x.Price * x.Quantity);
+
+ public ShoppingCart(string userName)
+ {
+ UserName = userName;
+ }
+
+ //Required for Mapping
+ public ShoppingCart()
+ {
+ }
+}
diff --git a/src/Services/Basket/Basket.API/Models/ShoppingCartItem.cs b/src/Services/Basket/Basket.API/Models/ShoppingCartItem.cs
new file mode 100644
index 00000000..9a14bfe7
--- /dev/null
+++ b/src/Services/Basket/Basket.API/Models/ShoppingCartItem.cs
@@ -0,0 +1,10 @@
+namespace Basket.API.Models;
+
+public class ShoppingCartItem
+{
+ public int Quantity { get; set; } = default!;
+ public string Color { get; set; } = default!;
+ public decimal Price { get; set; } = default!;
+ public Guid ProductId { get; set; } = default!;
+ public string ProductName { get; set; } = default!;
+}
diff --git a/src/Services/Basket/Basket.API/Program.cs b/src/Services/Basket/Basket.API/Program.cs
index e653c327..142af2c2 100644
--- a/src/Services/Basket/Basket.API/Program.cs
+++ b/src/Services/Basket/Basket.API/Program.cs
@@ -1,23 +1,73 @@
-using Common.Logging;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using Serilog;
+using Discount.Grpc;
+using HealthChecks.UI.Client;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using BuildingBlocks.Messaging.MassTransit;
-namespace Basket.API
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+
+//Application Services
+var assembly = typeof(Program).Assembly;
+builder.Services.AddCarter();
+builder.Services.AddMediatR(config =>
+{
+ config.RegisterServicesFromAssembly(assembly);
+ config.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ config.AddOpenBehavior(typeof(LoggingBehavior<,>));
+});
+
+//Data Services
+builder.Services.AddMarten(opts =>
+{
+ opts.Connection(builder.Configuration.GetConnectionString("Database")!);
+ opts.Schema.For().Identity(x => x.UserName);
+}).UseLightweightSessions();
+
+builder.Services.AddScoped();
+builder.Services.Decorate();
+
+builder.Services.AddStackExchangeRedisCache(options =>
+{
+ options.Configuration = builder.Configuration.GetConnectionString("Redis");
+ //options.InstanceName = "Basket";
+});
+
+//Grpc Services
+builder.Services.AddGrpcClient(options =>
{
- public class Program
+ options.Address = new Uri(builder.Configuration["GrpcSettings:DiscountUrl"]!);
+})
+.ConfigurePrimaryHttpMessageHandler(() =>
+{
+ var handler = new HttpClientHandler
{
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSerilog(SeriLogger.Configure)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
-}
+ ServerCertificateCustomValidationCallback =
+ HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
+ };
+
+ return handler;
+});
+
+//Async Communication Services
+builder.Services.AddMessageBroker(builder.Configuration);
+
+//Cross-Cutting Services
+builder.Services.AddExceptionHandler();
+
+builder.Services.AddHealthChecks()
+ .AddNpgSql(builder.Configuration.GetConnectionString("Database")!)
+ .AddRedis(builder.Configuration.GetConnectionString("Redis")!);
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.MapCarter();
+app.UseExceptionHandler(options => { });
+app.UseHealthChecks("/health",
+ new HealthCheckOptions
+ {
+ ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
+ });
+
+app.Run();
diff --git a/src/Services/Basket/Basket.API/Repositories/BasketRepository.cs b/src/Services/Basket/Basket.API/Repositories/BasketRepository.cs
deleted file mode 100644
index 13967fa5..00000000
--- a/src/Services/Basket/Basket.API/Repositories/BasketRepository.cs
+++ /dev/null
@@ -1,41 +0,0 @@
-using Basket.API.Entities;
-using Basket.API.Repositories.Interfaces;
-using Microsoft.Extensions.Caching.Distributed;
-using Newtonsoft.Json;
-using System;
-using System.Threading.Tasks;
-
-namespace Basket.API.Repositories
-{
- public class BasketRepository : IBasketRepository
- {
- private readonly IDistributedCache _redisCache;
-
- public BasketRepository(IDistributedCache cache)
- {
- _redisCache = cache ?? throw new ArgumentNullException(nameof(cache));
- }
-
- public async Task GetBasket(string userName)
- {
- var basket = await _redisCache.GetStringAsync(userName);
-
- if (String.IsNullOrEmpty(basket))
- return null;
-
- return JsonConvert.DeserializeObject(basket);
- }
-
- public async Task UpdateBasket(ShoppingCart basket)
- {
- await _redisCache.SetStringAsync(basket.UserName, JsonConvert.SerializeObject(basket));
-
- return await GetBasket(basket.UserName);
- }
-
- public async Task DeleteBasket(string userName)
- {
- await _redisCache.RemoveAsync(userName);
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/Repositories/Interfaces/IBasketRepository.cs b/src/Services/Basket/Basket.API/Repositories/Interfaces/IBasketRepository.cs
deleted file mode 100644
index d0d370e4..00000000
--- a/src/Services/Basket/Basket.API/Repositories/Interfaces/IBasketRepository.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using Basket.API.Entities;
-using System.Threading.Tasks;
-
-namespace Basket.API.Repositories.Interfaces
-{
- public interface IBasketRepository
- {
- Task GetBasket(string userName);
- Task UpdateBasket(ShoppingCart basket);
- Task DeleteBasket(string userName);
- }
-}
diff --git a/src/Services/Basket/Basket.API/Startup.cs b/src/Services/Basket/Basket.API/Startup.cs
deleted file mode 100644
index ef39352b..00000000
--- a/src/Services/Basket/Basket.API/Startup.cs
+++ /dev/null
@@ -1,95 +0,0 @@
-using Basket.API.GrpcServices;
-using Basket.API.Repositories;
-using Basket.API.Repositories.Interfaces;
-using Discount.Grpc.Protos;
-using HealthChecks.UI.Client;
-using MassTransit;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Microsoft.OpenApi.Models;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Basket.API
-{
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- // Redis Configuration
- services.AddStackExchangeRedisCache(options =>
- {
- options.Configuration = Configuration.GetValue("CacheSettings:ConnectionString");
- });
-
- // General Configuration
- services.AddScoped();
- services.AddAutoMapper(typeof(Startup));
-
- // Grpc Configuration
- services.AddGrpcClient
- (o => o.Address = new Uri(Configuration["GrpcSettings:DiscountUrl"]));
- services.AddScoped();
-
- // MassTransit-RabbitMQ Configuration
- services.AddMassTransit(config => {
- config.UsingRabbitMq((ctx, cfg) => {
- cfg.Host(Configuration["EventBusSettings:HostAddress"]);
- cfg.UseHealthCheck(ctx);
- });
- });
- services.AddMassTransitHostedService();
-
- services.AddControllers();
- services.AddSwaggerGen(c =>
- {
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "Basket.API", Version = "v1" });
- });
-
- services.AddHealthChecks()
- .AddRedis(Configuration["CacheSettings:ConnectionString"], "Redis Health", HealthStatus.Degraded);
- }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- app.UseSwagger();
- app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Basket.API v1"));
- }
-
- app.UseRouting();
-
- app.UseAuthorization();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
- {
- Predicate = _ => true,
- ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
- });
- });
- }
- }
-}
diff --git a/src/Services/Basket/Basket.API/appsettings.Development.json b/src/Services/Basket/Basket.API/appsettings.Development.json
index 8983e0fc..0c208ae9 100644
--- a/src/Services/Basket/Basket.API/appsettings.Development.json
+++ b/src/Services/Basket/Basket.API/appsettings.Development.json
@@ -2,8 +2,7 @@
"Logging": {
"LogLevel": {
"Default": "Information",
- "Microsoft": "Warning",
- "Microsoft.Hosting.Lifetime": "Information"
+ "Microsoft.AspNetCore": "Warning"
}
}
}
diff --git a/src/Services/Basket/Basket.API/appsettings.json b/src/Services/Basket/Basket.API/appsettings.json
index 4a9d2551..3eb884eb 100644
--- a/src/Services/Basket/Basket.API/appsettings.json
+++ b/src/Services/Basket/Basket.API/appsettings.json
@@ -1,24 +1,21 @@
{
- "CacheSettings": {
- "ConnectionString": "localhost:6379"
+ "ConnectionStrings": {
+ "Database": "Server=localhost;Port=5433;Database=BasketDb;User Id=postgres;Password=postgres;Include Error Detail=true",
+ "Redis": "localhost:6379"
},
"GrpcSettings": {
- "DiscountUrl": "http://localhost:5003"
+ "DiscountUrl": "https://localhost:5052"
},
- "EventBusSettings": {
- "HostAddress": "amqp://guest:guest@localhost:5672"
+ "MessageBroker": {
+ "Host": "amqp://localhost:5672",
+ "UserName": "guest",
+ "Password": "guest"
},
- "Serilog": {
- "MinimumLevel": {
+ "Logging": {
+ "LogLevel": {
"Default": "Information",
- "Override": {
- "Microsoft": "Information",
- "System": "Warning"
- }
+ "Microsoft.AspNetCore": "Warning"
}
},
- "ElasticConfiguration": {
- "Uri": "http://localhost:9200"
- },
"AllowedHosts": "*"
}
diff --git a/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj b/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj
deleted file mode 100644
index aaf45b31..00000000
--- a/src/Services/Basket/Basket.UnitTests/Basket.UnitTests.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- net5.0
-
- false
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
diff --git a/src/Services/Basket/Basket.UnitTests/UnitTest1.cs b/src/Services/Basket/Basket.UnitTests/UnitTest1.cs
deleted file mode 100644
index e7bac90f..00000000
--- a/src/Services/Basket/Basket.UnitTests/UnitTest1.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using Xunit;
-
-namespace Basket.UnitTests
-{
- public class UnitTest1
- {
- [Fact]
- public void Test1()
- {
-
- }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Catalog.API.csproj b/src/Services/Catalog/Catalog.API/Catalog.API.csproj
index e2f05f16..b3f7b688 100644
--- a/src/Services/Catalog/Catalog.API/Catalog.API.csproj
+++ b/src/Services/Catalog/Catalog.API/Catalog.API.csproj
@@ -1,22 +1,25 @@
- net5.0
- ..\..\..\docker-compose.dcproj
+ net8.0
+ enable
+ enable
+ 5bac83bc-291e-4956-a620-0cbbddb9de7a
Linux
..\..\..
+ ..\..\..\docker-compose.dcproj
-
-
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs b/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs
deleted file mode 100644
index 96578c0c..00000000
--- a/src/Services/Catalog/Catalog.API/Controllers/CatalogController.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using Catalog.API.Entities;
-using Catalog.API.Repositories.Interfaces;
-using DnsClient.Internal;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace Catalog.API.Controllers
-{
- [ApiController]
- [Route("api/v1/[controller]")]
- public class CatalogController : ControllerBase
- {
- private readonly IProductRepository _repository;
- private readonly ILogger _logger;
-
- public CatalogController(IProductRepository repository, ILogger logger)
- {
- _repository = repository ?? throw new ArgumentNullException(nameof(repository));
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- [HttpGet]
- [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)]
- public async Task>> GetProducts()
- {
- var products = await _repository.GetProducts();
- return Ok(products);
- }
-
- [HttpGet("{id:length(24)}", Name = "GetProduct")]
- [ProducesResponseType((int)HttpStatusCode.NotFound)]
- [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
- public async Task> GetProductById(string id)
- {
- var product = await _repository.GetProduct(id);
-
- if (product == null)
- {
- _logger.LogError($"Product with id: {id}, not found.");
- return NotFound();
- }
-
- return Ok(product);
- }
-
- [Route("[action]/{category}", Name = "GetProductByCategory")]
- [HttpGet]
- [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)]
- public async Task>> GetProductByCategory(string category)
- {
- var products = await _repository.GetProductByCategory(category);
- return Ok(products);
- }
-
- [Route("[action]/{name}", Name = "GetProductByName")]
- [HttpGet]
- [ProducesResponseType((int)HttpStatusCode.NotFound)]
- [ProducesResponseType(typeof(IEnumerable), (int)HttpStatusCode.OK)]
- public async Task>> GetProductByName(string name)
- {
- var items = await _repository.GetProductByName(name);
- if (items == null)
- {
- _logger.LogError($"Products with name: {name} not found.");
- return NotFound();
- }
- return Ok(items);
- }
-
- [HttpPost]
- [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
- public async Task> CreateProduct([FromBody] Product product)
- {
- await _repository.CreateProduct(product);
-
- return CreatedAtRoute("GetProduct", new { id = product.Id }, product);
- }
-
- [HttpPut]
- [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
- public async Task UpdateProduct([FromBody] Product product)
- {
- return Ok(await _repository.UpdateProduct(product));
- }
-
- [HttpDelete("{id:length(24)}", Name = "DeleteProduct")]
- [ProducesResponseType(typeof(Product), (int)HttpStatusCode.OK)]
- public async Task DeleteProductById(string id)
- {
- return Ok(await _repository.DeleteProduct(id));
- }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Data/CatalogContext.cs b/src/Services/Catalog/Catalog.API/Data/CatalogContext.cs
deleted file mode 100644
index 453ff733..00000000
--- a/src/Services/Catalog/Catalog.API/Data/CatalogContext.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using Catalog.API.Data.Interfaces;
-using Catalog.API.Entities;
-using Microsoft.Extensions.Configuration;
-using MongoDB.Driver;
-
-namespace Catalog.API.Data
-{
- public class CatalogContext : ICatalogContext
- {
- public CatalogContext(IConfiguration configuration)
- {
- var client = new MongoClient(configuration.GetValue("DatabaseSettings:ConnectionString"));
- var database = client.GetDatabase(configuration.GetValue("DatabaseSettings:DatabaseName"));
-
- Products = database.GetCollection(configuration.GetValue("DatabaseSettings:CollectionName"));
- CatalogContextSeed.SeedData(Products);
- }
-
- public IMongoCollection Products { get; }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Data/CatalogContextSeed.cs b/src/Services/Catalog/Catalog.API/Data/CatalogContextSeed.cs
deleted file mode 100644
index d61b31e9..00000000
--- a/src/Services/Catalog/Catalog.API/Data/CatalogContextSeed.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using Catalog.API.Entities;
-using MongoDB.Driver;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace Catalog.API.Data
-{
- public class CatalogContextSeed
- {
- public static void SeedData(IMongoCollection productCollection)
- {
- bool existProduct = productCollection.Find(p => true).Any();
- if (!existProduct)
- {
- productCollection.InsertManyAsync(GetPreconfiguredProducts());
- }
- }
-
- private static IEnumerable GetPreconfiguredProducts()
- {
- return new List()
- {
- new Product()
- {
- Id = "602d2149e773f2a3990b47f5",
- Name = "IPhone X",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-1.png",
- Price = 950.00M,
- Category = "Smart Phone"
- },
- new Product()
- {
- Id = "602d2149e773f2a3990b47f6",
- Name = "Samsung 10",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-2.png",
- Price = 840.00M,
- Category = "Smart Phone"
- },
- new Product()
- {
- Id = "602d2149e773f2a3990b47f7",
- Name = "Huawei Plus",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-3.png",
- Price = 650.00M,
- Category = "White Appliances"
- },
- new Product()
- {
- Id = "602d2149e773f2a3990b47f8",
- Name = "Xiaomi Mi 9",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-4.png",
- Price = 470.00M,
- Category = "White Appliances"
- },
- new Product()
- {
- Id = "602d2149e773f2a3990b47f9",
- Name = "HTC U11+ Plus",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-5.png",
- Price = 380.00M,
- Category = "Smart Phone"
- },
- new Product()
- {
- Id = "602d2149e773f2a3990b47fa",
- Name = "LG G7 ThinQ",
- Summary = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
- Description = "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ut, tenetur natus doloremque laborum quos iste ipsum rerum obcaecati impedit odit illo dolorum ab tempora nihil dicta earum fugiat. Temporibus, voluptatibus.",
- ImageFile = "product-6.png",
- Price = 240.00M,
- Category = "Home Kitchen"
- }
- };
- }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Data/CatalogInitialData.cs b/src/Services/Catalog/Catalog.API/Data/CatalogInitialData.cs
new file mode 100644
index 00000000..f8f07c7c
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Data/CatalogInitialData.cs
@@ -0,0 +1,86 @@
+using Marten.Schema;
+
+namespace Catalog.API.Data;
+
+public class CatalogInitialData : IInitialData
+{
+ public async Task Populate(IDocumentStore store, CancellationToken cancellation)
+ {
+ using var session = store.LightweightSession();
+
+ if (await session.Query().AnyAsync())
+ return;
+
+ // Marten UPSERT will cater for existing records
+ session.Store(GetPreconfiguredProducts());
+ await session.SaveChangesAsync();
+ }
+
+ private static IEnumerable GetPreconfiguredProducts() => new List()
+ {
+ new Product()
+ {
+ Id = new Guid("5334c996-8457-4cf0-815c-ed2b77c4ff61"),
+ Name = "IPhone X",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-1.png",
+ Price = 950.00M,
+ Category = new List { "Smart Phone" }
+ },
+ new Product()
+ {
+ Id = new Guid("c67d6323-e8b1-4bdf-9a75-b0d0d2e7e914"),
+ Name = "Samsung 10",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-2.png",
+ Price = 840.00M,
+ Category = new List { "Smart Phone" }
+ },
+ new Product()
+ {
+ Id = new Guid("4f136e9f-ff8c-4c1f-9a33-d12f689bdab8"),
+ Name = "Huawei Plus",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-3.png",
+ Price = 650.00M,
+ Category = new List { "White Appliances" }
+ },
+ new Product()
+ {
+ Id = new Guid("6ec1297b-ec0a-4aa1-be25-6726e3b51a27"),
+ Name = "Xiaomi Mi 9",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-4.png",
+ Price = 470.00M,
+ Category = new List { "White Appliances" }
+ },
+ new Product()
+ {
+ Id = new Guid("b786103d-c621-4f5a-b498-23452610f88c"),
+ Name = "HTC U11+ Plus",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-5.png",
+ Price = 380.00M,
+ Category = new List { "Smart Phone" }
+ },
+ new Product()
+ {
+ Id = new Guid("c4bbc4a2-4555-45d8-97cc-2a99b2167bff"),
+ Name = "LG G7 ThinQ",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-6.png",
+ Price = 240.00M,
+ Category = new List { "Home Kitchen" }
+ },
+ new Product()
+ {
+ Id = new Guid("93170c85-7795-489c-8e8f-7dcf3b4f4188"),
+ Name = "Panasonic Lumix",
+ Description = "This phone is the company's biggest change to its flagship smartphone in years. It includes a borderless.",
+ ImageFile = "product-6.png",
+ Price = 240.00M,
+ Category = new List { "Camera" }
+ }
+ };
+
+}
diff --git a/src/Services/Catalog/Catalog.API/Data/Interfaces/ICatalogContext.cs b/src/Services/Catalog/Catalog.API/Data/Interfaces/ICatalogContext.cs
deleted file mode 100644
index 0279ca63..00000000
--- a/src/Services/Catalog/Catalog.API/Data/Interfaces/ICatalogContext.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using Catalog.API.Entities;
-using MongoDB.Driver;
-
-namespace Catalog.API.Data.Interfaces
-{
- public interface ICatalogContext
- {
- IMongoCollection Products { get; }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Dockerfile b/src/Services/Catalog/Catalog.API/Dockerfile
index 8e964df2..999a7cf2 100644
--- a/src/Services/Catalog/Catalog.API/Dockerfile
+++ b/src/Services/Catalog/Catalog.API/Dockerfile
@@ -1,20 +1,24 @@
-#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
-FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
WORKDIR /app
-EXPOSE 80
+EXPOSE 8080
+EXPOSE 8081
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Services/Catalog/Catalog.API/Catalog.API.csproj", "Services/Catalog/Catalog.API/"]
-COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"]
-RUN dotnet restore "Services/Catalog/Catalog.API/Catalog.API.csproj"
+COPY ["BuildingBlocks/BuildingBlocks/BuildingBlocks.csproj", "BuildingBlocks/BuildingBlocks/"]
+RUN dotnet restore "./Services/Catalog/Catalog.API/./Catalog.API.csproj"
COPY . .
WORKDIR "/src/Services/Catalog/Catalog.API"
-RUN dotnet build "Catalog.API.csproj" -c Release -o /app/build
+RUN dotnet build "./Catalog.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
-RUN dotnet publish "Catalog.API.csproj" -c Release -o /app/publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Catalog.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
diff --git a/src/Services/Catalog/Catalog.API/Dockerfile.original b/src/Services/Catalog/Catalog.API/Dockerfile.original
new file mode 100644
index 00000000..01e33863
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Dockerfile.original
@@ -0,0 +1,25 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
+USER app
+WORKDIR /app
+EXPOSE 8080
+EXPOSE 8081
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["Services/Catalog/Catalog.API/Catalog.API.csproj", "Services/Catalog/Catalog.API/"]
+RUN dotnet restore "./Services/Catalog/Catalog.API/./Catalog.API.csproj"
+COPY . .
+WORKDIR "/src/Services/Catalog/Catalog.API"
+RUN dotnet build "./Catalog.API.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./Catalog.API.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "Catalog.API.dll"]
\ No newline at end of file
diff --git a/src/Services/Catalog/Catalog.API/Entities/Product.cs b/src/Services/Catalog/Catalog.API/Entities/Product.cs
deleted file mode 100644
index 3aace990..00000000
--- a/src/Services/Catalog/Catalog.API/Entities/Product.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using MongoDB.Bson;
-using MongoDB.Bson.Serialization.Attributes;
-
-namespace Catalog.API.Entities
-{
- public class Product
- {
- [BsonId]
- [BsonRepresentation(BsonType.ObjectId)]
- public string Id { get; set; }
-
- [BsonElement("Name")]
- public string Name { get; set; }
- public string Category { get; set; }
- public string Summary { get; set; }
- public string Description { get; set; }
- public string ImageFile { get; set; }
- public decimal Price { get; set; }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Exceptions/ProductNotFoundException.cs b/src/Services/Catalog/Catalog.API/Exceptions/ProductNotFoundException.cs
new file mode 100644
index 00000000..2136043a
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Exceptions/ProductNotFoundException.cs
@@ -0,0 +1,10 @@
+using BuildingBlocks.Exceptions;
+
+namespace Catalog.API.Exceptions;
+
+public class ProductNotFoundException : NotFoundException
+{
+ public ProductNotFoundException(Guid Id) : base("Product", Id)
+ {
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/GlobalUsing.cs b/src/Services/Catalog/Catalog.API/GlobalUsing.cs
new file mode 100644
index 00000000..66491dd6
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/GlobalUsing.cs
@@ -0,0 +1,12 @@
+global using Carter;
+global using Mapster;
+global using MediatR;
+global using Marten;
+global using BuildingBlocks.CQRS;
+global using Catalog.API.Models;
+global using Catalog.API.Exceptions;
+global using FluentValidation;
+global using BuildingBlocks.Behaviors;
+global using BuildingBlocks.Exceptions.Handler;
+global using Catalog.API.Data;
+global using Marten.Pagination;
\ No newline at end of file
diff --git a/src/Services/Catalog/Catalog.API/Models/Product.cs b/src/Services/Catalog/Catalog.API/Models/Product.cs
new file mode 100644
index 00000000..9e48f551
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Models/Product.cs
@@ -0,0 +1,11 @@
+namespace Catalog.API.Models;
+
+public class Product
+{
+ public Guid Id { get; set; }
+ public string Name { get; set; } = default!;
+ public List Category { get; set; } = new();
+ public string Description { get; set; } = default!;
+ public string ImageFile { get; set; } = default!;
+ public decimal Price { get; set; }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductEndpoint.cs
new file mode 100644
index 00000000..3a70daab
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductEndpoint.cs
@@ -0,0 +1,29 @@
+namespace Catalog.API.Products.CreateProduct;
+
+public record CreateProductRequest(string Name, List Category, string Description, string ImageFile, decimal Price);
+
+public record CreateProductResponse(Guid Id);
+
+public class CreateProductEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapPost("/products",
+ async (CreateProductRequest request, ISender sender) =>
+ {
+ var command = request.Adapt();
+
+ var result = await sender.Send(command);
+
+ var response = result.Adapt();
+
+ return Results.Created($"/products/{response.Id}", response);
+
+ })
+ .WithName("CreateProduct")
+ .Produces(StatusCodes.Status201Created)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Create Product")
+ .WithDescription("Create Product");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductHandler.cs b/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductHandler.cs
new file mode 100644
index 00000000..c18bf71f
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductHandler.cs
@@ -0,0 +1,44 @@
+namespace Catalog.API.Products.CreateProduct;
+
+public record CreateProductCommand(string Name, List Category, string Description, string ImageFile, decimal Price)
+ : ICommand;
+public record CreateProductResult(Guid Id);
+
+public class CreateProductCommandValidator : AbstractValidator
+{
+ public CreateProductCommandValidator()
+ {
+ RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required");
+ RuleFor(x => x.Category).NotEmpty().WithMessage("Category is required");
+ RuleFor(x => x.ImageFile).NotEmpty().WithMessage("ImageFile is required");
+ RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
+ }
+}
+
+internal class CreateProductCommandHandler
+ (IDocumentSession session)
+ : ICommandHandler
+{
+ public async Task Handle(CreateProductCommand command, CancellationToken cancellationToken)
+ {
+ //create Product entity from command object
+ //save to database
+ //return CreateProductResult result
+
+ var product = new Product
+ {
+ Name = command.Name,
+ Category = command.Category,
+ Description = command.Description,
+ ImageFile = command.ImageFile,
+ Price = command.Price
+ };
+
+ //save to database
+ session.Store(product);
+ await session.SaveChangesAsync(cancellationToken);
+
+ //return result
+ return new CreateProductResult(product.Id);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductEndpoint.cs
new file mode 100644
index 00000000..6112e4d1
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductEndpoint.cs
@@ -0,0 +1,26 @@
+
+namespace Catalog.API.Products.DeleteProduct;
+
+//public record DeleteProductRequest(Guid Id);
+public record DeleteProductResponse(bool IsSuccess);
+
+public class DeleteProductEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapDelete("/products/{id}", async (Guid id, ISender sender) =>
+ {
+ var result = await sender.Send(new DeleteProductCommand(id));
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("DeleteProduct")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Delete Product")
+ .WithDescription("Delete Product");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductHandler.cs b/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductHandler.cs
new file mode 100644
index 00000000..74cd2657
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/DeleteProduct/DeleteProductHandler.cs
@@ -0,0 +1,26 @@
+
+namespace Catalog.API.Products.DeleteProduct;
+
+public record DeleteProductCommand(Guid Id) : ICommand;
+public record DeleteProductResult(bool IsSuccess);
+
+public class DeleteProductCommandValidator : AbstractValidator
+{
+ public DeleteProductCommandValidator()
+ {
+ RuleFor(x => x.Id).NotEmpty().WithMessage("Product ID is required");
+ }
+}
+
+internal class DeleteProductCommandHandler
+ (IDocumentSession session)
+ : ICommandHandler
+{
+ public async Task Handle(DeleteProductCommand command, CancellationToken cancellationToken)
+ {
+ session.Delete(command.Id);
+ await session.SaveChangesAsync(cancellationToken);
+
+ return new DeleteProductResult(true);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryEndpoint.cs
new file mode 100644
index 00000000..4c8b034e
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryEndpoint.cs
@@ -0,0 +1,26 @@
+
+namespace Catalog.API.Products.GetProductByCategory;
+
+//public record GetProductByCategoryRequest();
+public record GetProductByCategoryResponse(IEnumerable Products);
+
+public class GetProductByCategoryEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapGet("/products/category/{category}",
+ async (string category, ISender sender) =>
+ {
+ var result = await sender.Send(new GetProductByCategoryQuery(category));
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("GetProductByCategory")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Get Product By Category")
+ .WithDescription("Get Product By Category");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryHandler.cs b/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryHandler.cs
new file mode 100644
index 00000000..a4ba777a
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProductByCategory/GetProductByCategoryHandler.cs
@@ -0,0 +1,19 @@
+
+namespace Catalog.API.Products.GetProductByCategory;
+
+public record GetProductByCategoryQuery(string Category) : IQuery;
+public record GetProductByCategoryResult(IEnumerable Products);
+
+internal class GetProductByCategoryQueryHandler
+ (IDocumentSession session)
+ : IQueryHandler
+{
+ public async Task Handle(GetProductByCategoryQuery query, CancellationToken cancellationToken)
+ {
+ var products = await session.Query()
+ .Where(p => p.Category.Contains(query.Category))
+ .ToListAsync(cancellationToken);
+
+ return new GetProductByCategoryResult(products);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdEndpoint.cs
new file mode 100644
index 00000000..5849ca13
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdEndpoint.cs
@@ -0,0 +1,25 @@
+
+namespace Catalog.API.Products.GetProductById;
+
+//public record GetProductByIdRequest();
+public record GetProductByIdResponse(Product Product);
+
+public class GetProductByIdEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapGet("/products/{id}", async (Guid id, ISender sender) =>
+ {
+ var result = await sender.Send(new GetProductByIdQuery(id));
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("GetProductById")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Get Product By Id")
+ .WithDescription("Get Product By Id");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdHandler.cs b/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdHandler.cs
new file mode 100644
index 00000000..c80347ac
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProductById/GetProductByIdHandler.cs
@@ -0,0 +1,21 @@
+namespace Catalog.API.Products.GetProductById;
+
+public record GetProductByIdQuery(Guid Id) : IQuery;
+public record GetProductByIdResult(Product Product);
+
+internal class GetProductByIdQueryHandler
+ (IDocumentSession session)
+ : IQueryHandler
+{
+ public async Task Handle(GetProductByIdQuery query, CancellationToken cancellationToken)
+ {
+ var product = await session.LoadAsync(query.Id, cancellationToken);
+
+ if (product is null)
+ {
+ throw new ProductNotFoundException(query.Id);
+ }
+
+ return new GetProductByIdResult(product);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsEndpoint.cs
new file mode 100644
index 00000000..b1c8d2b9
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsEndpoint.cs
@@ -0,0 +1,26 @@
+namespace Catalog.API.Products.GetProducts;
+
+public record GetProductsRequest(int? PageNumber = 1, int? PageSize = 10);
+public record GetProductsResponse(IEnumerable Products);
+
+public class GetProductsEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapGet("/products", async ([AsParameters] GetProductsRequest request, ISender sender) =>
+ {
+ var query = request.Adapt();
+
+ var result = await sender.Send(query);
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("GetProducts")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .WithSummary("Get Products")
+ .WithDescription("Get Products");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsHandler.cs b/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsHandler.cs
new file mode 100644
index 00000000..92618cf1
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/GetProducts/GetProductsHandler.cs
@@ -0,0 +1,17 @@
+namespace Catalog.API.Products.GetProducts;
+
+public record GetProductsQuery(int? PageNumber = 1, int? PageSize = 10) : IQuery;
+public record GetProductsResult(IEnumerable Products);
+
+internal class GetProductsQueryHandler
+ (IDocumentSession session)
+ : IQueryHandler
+{
+ public async Task Handle(GetProductsQuery query, CancellationToken cancellationToken)
+ {
+ var products = await session.Query()
+ .ToPagedListAsync(query.PageNumber ?? 1, query.PageSize ?? 10, cancellationToken);
+
+ return new GetProductsResult(products);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductEndpoint.cs b/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductEndpoint.cs
new file mode 100644
index 00000000..b01507ed
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductEndpoint.cs
@@ -0,0 +1,29 @@
+
+namespace Catalog.API.Products.UpdateProduct;
+
+public record UpdateProductRequest(Guid Id, string Name, List Category, string Description, string ImageFile, decimal Price);
+public record UpdateProductResponse(bool IsSuccess);
+
+public class UpdateProductEndpoint : ICarterModule
+{
+ public void AddRoutes(IEndpointRouteBuilder app)
+ {
+ app.MapPut("/products",
+ async (UpdateProductRequest request, ISender sender) =>
+ {
+ var command = request.Adapt();
+
+ var result = await sender.Send(command);
+
+ var response = result.Adapt();
+
+ return Results.Ok(response);
+ })
+ .WithName("UpdateProduct")
+ .Produces(StatusCodes.Status200OK)
+ .ProducesProblem(StatusCodes.Status400BadRequest)
+ .ProducesProblem(StatusCodes.Status404NotFound)
+ .WithSummary("Update Product")
+ .WithDescription("Update Product");
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductHandler.cs b/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductHandler.cs
new file mode 100644
index 00000000..fe7b2fa4
--- /dev/null
+++ b/src/Services/Catalog/Catalog.API/Products/UpdateProduct/UpdateProductHandler.cs
@@ -0,0 +1,47 @@
+
+namespace Catalog.API.Products.UpdateProduct;
+
+public record UpdateProductCommand(Guid Id, string Name, List Category, string Description, string ImageFile, decimal Price)
+ : ICommand;
+public record UpdateProductResult(bool IsSuccess);
+
+public class UpdateProductCommandValidator : AbstractValidator
+{
+ public UpdateProductCommandValidator()
+ {
+ RuleFor(command => command.Id).NotEmpty().WithMessage("Product ID is required");
+
+ RuleFor(command => command.Name)
+ .NotEmpty().WithMessage("Name is required")
+ .Length(2, 150).WithMessage("Name must be between 2 and 150 characters");
+
+ RuleFor(command => command.Price)
+ .GreaterThan(0).WithMessage("Price must be greater than 0");
+ }
+}
+
+internal class UpdateProductCommandHandler
+ (IDocumentSession session)
+ : ICommandHandler
+{
+ public async Task Handle(UpdateProductCommand command, CancellationToken cancellationToken)
+ {
+ var product = await session.LoadAsync(command.Id, cancellationToken);
+
+ if (product is null)
+ {
+ throw new ProductNotFoundException(command.Id);
+ }
+
+ product.Name = command.Name;
+ product.Category = command.Category;
+ product.Description = command.Description;
+ product.ImageFile = command.ImageFile;
+ product.Price = command.Price;
+
+ session.Update(product);
+ await session.SaveChangesAsync(cancellationToken);
+
+ return new UpdateProductResult(true);
+ }
+}
diff --git a/src/Services/Catalog/Catalog.API/Program.cs b/src/Services/Catalog/Catalog.API/Program.cs
index bab380d1..5fe300a8 100644
--- a/src/Services/Catalog/Catalog.API/Program.cs
+++ b/src/Services/Catalog/Catalog.API/Program.cs
@@ -1,23 +1,44 @@
-using Common.Logging;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using Serilog;
+using HealthChecks.UI.Client;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-namespace Catalog.API
+var builder = WebApplication.CreateBuilder(args);
+
+// Add services to the container.
+var assembly = typeof(Program).Assembly;
+builder.Services.AddMediatR(config =>
+{
+ config.RegisterServicesFromAssembly(assembly);
+ config.AddOpenBehavior(typeof(ValidationBehavior<,>));
+ config.AddOpenBehavior(typeof(LoggingBehavior<,>));
+});
+builder.Services.AddValidatorsFromAssembly(assembly);
+
+builder.Services.AddCarter();
+
+builder.Services.AddMarten(opts =>
{
- public class Program
+ opts.Connection(builder.Configuration.GetConnectionString("Database")!);
+}).UseLightweightSessions();
+
+if (builder.Environment.IsDevelopment())
+ builder.Services.InitializeMartenWith();
+
+builder.Services.AddExceptionHandler();
+
+builder.Services.AddHealthChecks()
+ .AddNpgSql(builder.Configuration.GetConnectionString("Database")!);
+
+var app = builder.Build();
+
+// Configure the HTTP request pipeline.
+app.MapCarter();
+
+app.UseExceptionHandler(options => { });
+
+app.UseHealthChecks("/health",
+ new HealthCheckOptions
{
- public static void Main(string[] args)
- {
- CreateHostBuilder(args).Build().Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSerilog(SeriLogger.Configure)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
-}
+ ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
+ });
+
+app.Run();
diff --git a/src/Services/Catalog/Catalog.API/Repositories/Interfaces/IProductRepository.cs b/src/Services/Catalog/Catalog.API/Repositories/Interfaces/IProductRepository.cs
deleted file mode 100644
index a7bc613e..00000000
--- a/src/Services/Catalog/Catalog.API/Repositories/Interfaces/IProductRepository.cs
+++ /dev/null
@@ -1,20 +0,0 @@
-using Catalog.API.Entities;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Catalog.API.Repositories.Interfaces
-{
- public interface IProductRepository
- {
- Task> GetProducts();
- Task GetProduct(string id);
- Task> GetProductByName(string name);
- Task> GetProductByCategory(string categoryName);
-
- Task CreateProduct(Product product);
- Task UpdateProduct(Product product);
- Task DeleteProduct(string id);
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Repositories/ProductRepository.cs b/src/Services/Catalog/Catalog.API/Repositories/ProductRepository.cs
deleted file mode 100644
index 59734d7f..00000000
--- a/src/Services/Catalog/Catalog.API/Repositories/ProductRepository.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using Catalog.API.Data.Interfaces;
-using Catalog.API.Entities;
-using Catalog.API.Repositories.Interfaces;
-using MongoDB.Driver;
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-
-namespace Catalog.API.Repositories
-{
- public class ProductRepository : IProductRepository
- {
- private readonly ICatalogContext _context;
-
- public ProductRepository(ICatalogContext context)
- {
- _context = context ?? throw new ArgumentNullException(nameof(context));
- }
-
- public async Task> GetProducts()
- {
- return await _context
- .Products
- .Find(p => true)
- .ToListAsync();
- }
-
- public async Task GetProduct(string id)
- {
- return await _context
- .Products
- .Find(p => p.Id == id)
- .FirstOrDefaultAsync();
- }
-
- public async Task> GetProductByName(string name)
- {
- FilterDefinition filter = Builders.Filter.ElemMatch(p => p.Name, name);
-
- return await _context
- .Products
- .Find(filter)
- .ToListAsync();
- }
-
- public async Task> GetProductByCategory(string categoryName)
- {
- FilterDefinition filter = Builders.Filter.Eq(p => p.Category, categoryName);
-
- return await _context
- .Products
- .Find(filter)
- .ToListAsync();
- }
-
-
- public async Task CreateProduct(Product product)
- {
- await _context.Products.InsertOneAsync(product);
- }
-
- public async Task UpdateProduct(Product product)
- {
- var updateResult = await _context
- .Products
- .ReplaceOneAsync(filter: g => g.Id == product.Id, replacement: product);
-
- return updateResult.IsAcknowledged
- && updateResult.ModifiedCount > 0;
- }
-
- public async Task DeleteProduct(string id)
- {
- FilterDefinition filter = Builders.Filter.Eq(p => p.Id, id);
-
- DeleteResult deleteResult = await _context
- .Products
- .DeleteOneAsync(filter);
-
- return deleteResult.IsAcknowledged
- && deleteResult.DeletedCount > 0;
- }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/Startup.cs b/src/Services/Catalog/Catalog.API/Startup.cs
deleted file mode 100644
index 236d75dd..00000000
--- a/src/Services/Catalog/Catalog.API/Startup.cs
+++ /dev/null
@@ -1,67 +0,0 @@
-using Catalog.API.Data;
-using Catalog.API.Data.Interfaces;
-using Catalog.API.Repositories;
-using Catalog.API.Repositories.Interfaces;
-using HealthChecks.UI.Client;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Diagnostics.HealthChecks;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Diagnostics.HealthChecks;
-using Microsoft.Extensions.Hosting;
-using Microsoft.OpenApi.Models;
-
-namespace Catalog.API
-{
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public IConfiguration Configuration { get; }
-
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddScoped();
- services.AddScoped();
-
- services.AddControllers();
- services.AddSwaggerGen(c =>
- {
- c.SwaggerDoc("v1", new OpenApiInfo { Title = "Catalog.API", Version = "v1" });
- });
-
- services.AddHealthChecks()
- .AddMongoDb(Configuration["DatabaseSettings:ConnectionString"], "MongoDb Health", HealthStatus.Degraded);
- }
-
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- app.UseSwagger();
- app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Catalog.API v1"));
- }
-
- app.UseRouting();
-
- app.UseAuthorization();
-
- app.UseEndpoints(endpoints =>
- {
- endpoints.MapControllers();
- endpoints.MapHealthChecks("/hc", new HealthCheckOptions()
- {
- Predicate = _ => true,
- ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
- });
- });
- }
- }
-}
diff --git a/src/Services/Catalog/Catalog.API/appsettings.Development.json b/src/Services/Catalog/Catalog.API/appsettings.Development.json
index 4bef95ec..0c208ae9 100644
--- a/src/Services/Catalog/Catalog.API/appsettings.Development.json
+++ b/src/Services/Catalog/Catalog.API/appsettings.Development.json
@@ -1,19 +1,8 @@
{
- "DatabaseSettings": {
- "ConnectionString": "mongodb://localhost:27017",
- "DatabaseName": "CatalogDb",
- "CollectionName": "Products"
- },
- "Serilog": {
- "MinimumLevel": {
+ "Logging": {
+ "LogLevel": {
"Default": "Information",
- "Override": {
- "Microsoft": "Information",
- "System": "Warning"
- }
+ "Microsoft.AspNetCore": "Warning"
}
- },
- "ElasticConfiguration": {
- "Uri": "http://localhost:9200"
}
}
diff --git a/src/Services/Catalog/Catalog.API/appsettings.json b/src/Services/Catalog/Catalog.API/appsettings.json
index 821c62c6..9292137b 100644
--- a/src/Services/Catalog/Catalog.API/appsettings.json
+++ b/src/Services/Catalog/Catalog.API/appsettings.json
@@ -1,20 +1,12 @@
{
- "DatabaseSettings": {
- "ConnectionString": "mongodb://localhost:27017",
- "DatabaseName": "CatalogDb",
- "CollectionName": "Products"
+ "ConnectionStrings": {
+ "Database": "Server=localhost;Port=5432;Database=CatalogDb;User Id=postgres;Password=postgres;Include Error Detail=true"
},
- "Serilog": {
- "MinimumLevel": {
+ "Logging": {
+ "LogLevel": {
"Default": "Information",
- "Override": {
- "Microsoft": "Information",
- "System": "Warning"
- }
+ "Microsoft.AspNetCore": "Warning"
}
},
- "ElasticConfiguration": {
- "Uri": "http://localhost:9200"
- },
"AllowedHosts": "*"
}
diff --git a/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj b/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj
deleted file mode 100644
index aaf45b31..00000000
--- a/src/Services/Catalog/Catalog.UnitTests/Catalog.UnitTests.csproj
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
- net5.0
-
- false
-
-
-
-
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
-
-
diff --git a/src/Services/Catalog/Catalog.UnitTests/UnitTest1.cs b/src/Services/Catalog/Catalog.UnitTests/UnitTest1.cs
deleted file mode 100644
index cb84bcb0..00000000
--- a/src/Services/Catalog/Catalog.UnitTests/UnitTest1.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using System;
-using Xunit;
-
-namespace Catalog.UnitTests
-{
- public class UnitTest1
- {
- [Fact]
- public void Test1()
- {
-
- }
- }
-}
diff --git a/src/Services/Discount/Discount.API/Controllers/DiscountController.cs b/src/Services/Discount/Discount.API/Controllers/DiscountController.cs
deleted file mode 100644
index 3b2c96ed..00000000
--- a/src/Services/Discount/Discount.API/Controllers/DiscountController.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using Discount.API.Entities;
-using Discount.API.Repositories.Interfaces;
-using Microsoft.AspNetCore.Mvc;
-using System;
-using System.Net;
-using System.Threading.Tasks;
-
-namespace Discount.API.Controllers
-{
- [ApiController]
- [Route("api/v1/[controller]")]
- public class DiscountController : ControllerBase
- {
- private readonly IDiscountRepository _repository;
-
- public DiscountController(IDiscountRepository repository)
- {
- _repository = repository ?? throw new ArgumentNullException(nameof(repository));
- }
-
- [HttpGet("{productName}", Name = "GetDiscount")]
- [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)]
- public async Task> GetDiscount(string productName)
- {
- var discount = await _repository.GetDiscount(productName);
- return Ok(discount);
- }
-
- [HttpPost]
- [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)]
- public async Task> CreateDiscount([FromBody] Coupon coupon)
- {
- await _repository.CreateDiscount(coupon);
- return CreatedAtRoute("GetDiscount", new { productName = coupon.ProductName }, coupon);
- }
-
- [HttpPut]
- [ProducesResponseType(typeof(Coupon), (int)HttpStatusCode.OK)]
- public async Task> UpdateBasket([FromBody] Coupon coupon)
- {
- return Ok(await _repository.UpdateDiscount(coupon));
- }
-
- [HttpDelete("{productName}", Name = "DeleteDiscount")]
- [ProducesResponseType(typeof(void), (int)HttpStatusCode.OK)]
- public async Task> DeleteDiscount(string productName)
- {
- return Ok(await _repository.DeleteDiscount(productName));
- }
- }
-}
diff --git a/src/Services/Discount/Discount.API/Discount - Backup.API.csproj b/src/Services/Discount/Discount.API/Discount - Backup.API.csproj
deleted file mode 100644
index 5b758070..00000000
--- a/src/Services/Discount/Discount.API/Discount - Backup.API.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net5.0
- ..\..\..\docker-compose.dcproj
-
-
-
-
-
-
-
-
-
diff --git a/src/Services/Discount/Discount.API/Discount.API.csproj b/src/Services/Discount/Discount.API/Discount.API.csproj
deleted file mode 100644
index 7c6acd16..00000000
--- a/src/Services/Discount/Discount.API/Discount.API.csproj
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
- net5.0
- ..\..\..\docker-compose.dcproj
- Linux
- ..\..\..
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/Services/Discount/Discount.API/Dockerfile b/src/Services/Discount/Discount.API/Dockerfile
deleted file mode 100644
index 1c43427b..00000000
--- a/src/Services/Discount/Discount.API/Dockerfile
+++ /dev/null
@@ -1,22 +0,0 @@
-#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
-
-FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base
-WORKDIR /app
-EXPOSE 80
-
-FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build
-WORKDIR /src
-COPY ["Services/Discount/Discount.API/Discount.API.csproj", "Services/Discount/Discount.API/"]
-COPY ["BuildingBlocks/Common.Logging/Common.Logging.csproj", "BuildingBlocks/Common.Logging/"]
-RUN dotnet restore "Services/Discount/Discount.API/Discount.API.csproj"
-COPY . .
-WORKDIR "/src/Services/Discount/Discount.API"
-RUN dotnet build "Discount.API.csproj" -c Release -o /app/build
-
-FROM build AS publish
-RUN dotnet publish "Discount.API.csproj" -c Release -o /app/publish
-
-FROM base AS final
-WORKDIR /app
-COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "Discount.API.dll"]
\ No newline at end of file
diff --git a/src/Services/Discount/Discount.API/Entities/Coupon.cs b/src/Services/Discount/Discount.API/Entities/Coupon.cs
deleted file mode 100644
index 93f8252e..00000000
--- a/src/Services/Discount/Discount.API/Entities/Coupon.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace Discount.API.Entities
-{
- public class Coupon
- {
- public int Id { get; set; }
- public string ProductName { get; set; }
- public string Description { get; set; }
- public int Amount { get; set; }
- }
-}
diff --git a/src/Services/Discount/Discount.API/Extensions/HostExtensions.cs b/src/Services/Discount/Discount.API/Extensions/HostExtensions.cs
deleted file mode 100644
index 9589a2fc..00000000
--- a/src/Services/Discount/Discount.API/Extensions/HostExtensions.cs
+++ /dev/null
@@ -1,77 +0,0 @@
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-using Microsoft.Extensions.Logging;
-using Npgsql;
-using Polly;
-using System;
-
-namespace Discount.API.Extensions
-{
- public static class HostExtensions
- {
- public static IHost MigrateDatabase(this IHost host)
- {
- using (var scope = host.Services.CreateScope())
- {
- var services = scope.ServiceProvider;
- var configuration = services.GetRequiredService();
- var logger = services.GetRequiredService>();
-
- try
- {
- logger.LogInformation("Migrating postresql database.");
-
- var retry = Policy.Handle()
- .WaitAndRetry(
- retryCount: 5,
- sleepDurationProvider: retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)), // 2,4,8,16,32 sc
- onRetry: (exception, retryCount, context) =>
- {
- logger.LogError($"Retry {retryCount} of {context.PolicyKey} at {context.OperationKey}, due to: {exception}.");
- });
-
- //if the postgresql server container is not created on run docker compose this
- //migration can't fail for network related exception. The retry options for database operations
- //apply to transient exceptions
- retry.Execute(() => ExecuteMigrations(configuration));
-
- logger.LogInformation("Migrated postresql database.");
- }
- catch (NpgsqlException ex)
- {
- logger.LogError(ex, "An error occurred while migrating the postresql database");
- }
- }
-
- return host;
- }
-
- private static void ExecuteMigrations(IConfiguration configuration)
- {
- using var connection = new NpgsqlConnection(configuration.GetValue("DatabaseSettings:ConnectionString"));
- connection.Open();
-
- using var command = new NpgsqlCommand
- {
- Connection = connection
- };
-
- command.CommandText = "DROP TABLE IF EXISTS Coupon";
- command.ExecuteNonQuery();
-
- command.CommandText = @"CREATE TABLE Coupon(Id SERIAL PRIMARY KEY,
- ProductName VARCHAR(24) NOT NULL,
- Description TEXT,
- Amount INT)";
- command.ExecuteNonQuery();
-
-
- command.CommandText = "INSERT INTO Coupon(ProductName, Description, Amount) VALUES('IPhone X', 'IPhone Discount', 150);";
- command.ExecuteNonQuery();
-
- command.CommandText = "INSERT INTO Coupon(ProductName, Description, Amount) VALUES('Samsung 10', 'Samsung Discount', 100);";
- command.ExecuteNonQuery();
- }
- }
-}
diff --git a/src/Services/Discount/Discount.API/Program.cs b/src/Services/Discount/Discount.API/Program.cs
deleted file mode 100644
index 2d717a7a..00000000
--- a/src/Services/Discount/Discount.API/Program.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Common.Logging;
-using Discount.API.Extensions;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.Hosting;
-using Serilog;
-
-namespace Discount.API
-{
- public class Program
- {
- public static void Main(string[] args)
- {
- var host = CreateHostBuilder(args).Build();
- host.MigrateDatabase();
- host.Run();
- }
-
- public static IHostBuilder CreateHostBuilder(string[] args) =>
- Host.CreateDefaultBuilder(args)
- .UseSerilog(SeriLogger.Configure)
- .ConfigureWebHostDefaults(webBuilder =>
- {
- webBuilder.UseStartup();
- });
- }
-}
diff --git a/src/Services/Discount/Discount.API/Repositories/DiscountRepository.cs b/src/Services/Discount/Discount.API/Repositories/DiscountRepository.cs
deleted file mode 100644
index 7f9a64fc..00000000
--- a/src/Services/Discount/Discount.API/Repositories/DiscountRepository.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-using Dapper;
-using Discount.API.Entities;
-using Discount.API.Repositories.Interfaces;
-using Microsoft.Extensions.Configuration;
-using Npgsql;
-using System;
-using System.Threading.Tasks;
-
-namespace Discount.API.Repositories
-{
- public class DiscountRepository : IDiscountRepository
- {
- private readonly IConfiguration _configuration;
-
- public DiscountRepository(IConfiguration configuration)
- {
- _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
- }
-
- public async Task GetDiscount(string productName)
- {
- using var connection = new NpgsqlConnection(_configuration.GetValue