diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..47565c6
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,17 @@
+**/bin/
+**/obj/
+**/.vs/
+**/.vscode/
+**/*.user
+**/*.suo
+**/*.pdb
+**/*.db
+**/*.log
+**/node_modules/
+Dockerfile*
+docker-compose*
+.env
+.git
+.gitignore
+README.md
+/vsdbg
\ No newline at end of file
diff --git a/.env b/.env
new file mode 100644
index 0000000..88aaa35
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+MSSQL_SA_PASSWORD=YourStrong!Passw0rd
+MONGO_INITDB_ROOT_USERNAME=admin
+MONGO_INITDB_ROOT_PASSWORD=secret
+RABBITMQ_DEFAULT_USER=guest
+RABBITMQ_DEFAULT_PASS=guest
\ No newline at end of file
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
new file mode 100644
index 0000000..c8c5052
--- /dev/null
+++ b/.github/workflows/ci-pipeline.yml
@@ -0,0 +1,48 @@
+name: Parallel Docker Builds with Cache
+
+on:
+ push:
+ branches: [develop, master]
+ pull_request:
+ branches: [develop, master]
+
+jobs:
+ build-docker:
+ name: "Build docker images"
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - dockerfile: OrderTrackingApp.Api/Dockerfile
+ image-name: ordertrackingapp-api
+ - dockerfile: OrderTrackingApp.Consumer/Dockerfile
+ image-name: ordertrackingapp-consumer
+ - dockerfile: OrderTrackingApp.Blazor.Server/Dockerfile
+ image-name: ordertrackingapp-blazor-server
+ - dockerfile: OrderTrackingApp.MigrationRunner/Dockerfile
+ image-name: ordertrackingapp-migration-runner
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Cache Docker layers
+ uses: actions/cache@v3
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ matrix.image-name }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-
+
+ - name: Build Docker image with cache
+ run: |
+ docker buildx build \
+ --cache-from=type=local,src=/tmp/.buildx-cache \
+ --cache-to=type=local,dest=/tmp/.buildx-cache \
+ -f ${{ matrix.dockerfile }} \
+ -t ${{ matrix.image-name }} \
+ --load .
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..992dcd4
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,12 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+
+ {
+ "name": ".NET Core Attach",
+ "type": "coreclr",
+ "request": "attach",
+ "processId": "${command:pickProcess}"
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..b1c79f6
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "dotnet",
+ "task": "build",
+ "group": "build",
+ "problemMatcher": [],
+ "label": "dotnet: build"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..c1bc24b
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,36 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/OrderTrackingApp.Api/Controllers/OrdersController.cs b/OrderTrackingApp.Api/Controllers/OrdersController.cs
new file mode 100644
index 0000000..978a006
--- /dev/null
+++ b/OrderTrackingApp.Api/Controllers/OrdersController.cs
@@ -0,0 +1,44 @@
+using AutoMapper;
+using MediatR;
+using Microsoft.AspNetCore.Mvc;
+using OrderTrackingApp.Api.Examples;
+using OrderTrackingApp.Api.Models;
+using OrderTrackingApp.Application.Orders.Commands;
+using OrderTrackingApp.Application.Orders.Queries;
+using Swashbuckle.AspNetCore.Filters;
+
+namespace OrderTrackingApp.Api.Controllers
+{
+ [Route("api/[controller]")]
+ [ApiController]
+ public class OrdersController(IMediator mediator, IMapper mapper) : ControllerBase
+ {
+ [HttpPost]
+
+ [SwaggerRequestExample(typeof(CreateOrderRequest), typeof(CreateOrderRequestExample))]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ public async Task CreateOrder([FromBody] CreateOrderRequest request)
+ {
+ var command = mapper.Map(request);
+ var orderId = await mediator.Send(command);
+
+ return CreatedAtAction(nameof(CreateOrder), new { id = orderId }, new { OrderId = orderId });
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetOrderById(Guid id)
+ {
+ var order = await mediator.Send(new GetOrderByIdQuery(id));
+ return Ok(order);
+ }
+
+ [HttpGet]
+ public async Task GetOrders([FromQuery] GetOrdersRequest request)
+ {
+ var query = mapper.Map(request);
+
+ var orders = await mediator.Send(query);
+ return Ok(orders);
+ }
+ }
+}
diff --git a/OrderTrackingApp.Api/Dockerfile b/OrderTrackingApp.Api/Dockerfile
new file mode 100644
index 0000000..85667b6
--- /dev/null
+++ b/OrderTrackingApp.Api/Dockerfile
@@ -0,0 +1,40 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+WORKDIR /app
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# Copy root package props first
+COPY ["Directory.Packages.props", "."]
+
+COPY ["OrderTrackingApp.Api/OrderTrackingApp.Api.csproj", "OrderTrackingApp.Api/"]
+COPY ["OrderTrackingApp.Application/OrderTrackingApp.Application.csproj", "OrderTrackingApp.Application/"]
+COPY ["OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj", "OrderTrackingApp.Application.Contracts/"]
+COPY ["OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj", "OrderTrackingApp.Domain/"]
+COPY ["OrderTrackingApp.Infrastructure/OrderTrackingApp.Infrastructure.csproj", "OrderTrackingApp.Infrastructure/"]
+COPY ["OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj", "OrderTrackingApp.Persistence/"]
+COPY ["OrderTrackingApp.ReadPersistence/OrderTrackingApp.ReadPersistence.csproj", "OrderTrackingApp.ReadPersistence/"]
+
+# Copy your root certificate into the container
+COPY nscacert.crt /usr/local/share/ca-certificates/nscacert.crt
+
+# Update the certificate store
+RUN update-ca-certificates
+
+RUN dotnet restore "OrderTrackingApp.Api/OrderTrackingApp.Api.csproj"
+
+COPY . .
+
+RUN dotnet build "OrderTrackingApp.Api/OrderTrackingApp.Api.csproj" -c Release -o /app/build --no-restore
+
+FROM build AS publish
+RUN dotnet publish "OrderTrackingApp.Api/OrderTrackingApp.Api.csproj" -c Release -o /app/publish --no-restore
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+ENV ASPNETCORE_URLS=http://0.0.0.0:8080
+
+ENTRYPOINT ["dotnet", "OrderTrackingApp.Api.dll"]
diff --git a/OrderTrackingApp.Api/Examples/CreateOrderRequestExample.cs b/OrderTrackingApp.Api/Examples/CreateOrderRequestExample.cs
new file mode 100644
index 0000000..bcbc646
--- /dev/null
+++ b/OrderTrackingApp.Api/Examples/CreateOrderRequestExample.cs
@@ -0,0 +1,28 @@
+using OrderTrackingApp.Api.Models;
+using Swashbuckle.AspNetCore.Filters;
+
+namespace OrderTrackingApp.Api.Examples;
+
+public class CreateOrderRequestExample : IExamplesProvider
+{
+ public CreateOrderRequest GetExamples()
+ {
+ return new CreateOrderRequest
+ {
+ CustomerId = Guid.NewGuid(),
+ Items = new List
+ {
+ new CreateOrderItemRequest
+ {
+ ProductId = Guid.Parse("b111a4dd-f45c-4d40-a6a3-3002f62f823f"),
+ Quantity = 2
+ },
+ new CreateOrderItemRequest
+ {
+ ProductId = Guid.Parse("2a13551b-059c-4035-b275-8a8cb82bd272"),
+ Quantity = 3
+ }
+ }
+ };
+ }
+}
diff --git a/OrderTrackingApp.Api/Extensions/ApiServiceRegistrations.cs b/OrderTrackingApp.Api/Extensions/ApiServiceRegistrations.cs
new file mode 100644
index 0000000..b2198cb
--- /dev/null
+++ b/OrderTrackingApp.Api/Extensions/ApiServiceRegistrations.cs
@@ -0,0 +1,34 @@
+using FluentValidation;
+using OrderTrackingApp.Api.Examples;
+using OrderTrackingApp.Api.MappingProfiles;
+using OrderTrackingApp.Api.Validators;
+using Swashbuckle.AspNetCore.Filters;
+using System.Text.Json;
+
+namespace OrderTrackingApp.Api.Extensions
+{
+ public static class ApiServiceRegistrations
+ {
+ public static IServiceCollection AddApiServices(this IServiceCollection services)
+ {
+ services.AddEndpointsApiExplorer();
+
+ services.AddSwaggerGen(options => {
+ options.ExampleFilters();
+ });
+
+ services.AddSwaggerExamplesFromAssemblyOf();
+
+ services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ });
+
+ services.AddValidatorsFromAssemblyContaining();
+ services.AddAutoMapper(config => { }, typeof(OrderProfile).Assembly);
+
+ return services;
+ }
+ }
+}
diff --git a/OrderTrackingApp.Api/MappingProfiles/OrderProfile.cs b/OrderTrackingApp.Api/MappingProfiles/OrderProfile.cs
new file mode 100644
index 0000000..6e97398
--- /dev/null
+++ b/OrderTrackingApp.Api/MappingProfiles/OrderProfile.cs
@@ -0,0 +1,18 @@
+using AutoMapper;
+using OrderTrackingApp.Api.Models;
+using OrderTrackingApp.Application.Orders.Commands;
+using OrderTrackingApp.Application.Orders.Queries;
+
+namespace OrderTrackingApp.Api.MappingProfiles
+{
+ public class OrderProfile : Profile
+ {
+ public OrderProfile()
+ {
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ }
+ }
+}
diff --git a/OrderTrackingApp.Api/Models/CreateOrderRequest.cs b/OrderTrackingApp.Api/Models/CreateOrderRequest.cs
new file mode 100644
index 0000000..2b699f0
--- /dev/null
+++ b/OrderTrackingApp.Api/Models/CreateOrderRequest.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace OrderTrackingApp.Api.Models
+{
+ public class CreateOrderRequest
+ {
+ [Required]
+ public Guid CustomerId { get; set; }
+
+ [Required]
+ public List Items { get; set; } = [];
+ }
+
+ public class CreateOrderItemRequest
+ {
+ [Required]
+ public Guid ProductId { get; set; }
+
+ [Range(1, int.MaxValue)]
+ public int Quantity { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Api/Models/GetOrdersRequest.cs b/OrderTrackingApp.Api/Models/GetOrdersRequest.cs
new file mode 100644
index 0000000..39f30a2
--- /dev/null
+++ b/OrderTrackingApp.Api/Models/GetOrdersRequest.cs
@@ -0,0 +1,15 @@
+namespace OrderTrackingApp.Api.Models
+{
+ public class GetOrdersRequest
+ {
+ public int Page { get; set; } = 1;
+
+ public int PageSize { get; set; } = 50;
+
+ public string? Status { get; set; }
+
+ public DateTime? FromDate { get; set; }
+
+ public DateTime? ToDate { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Api/OrderTrackingApp.Api.csproj b/OrderTrackingApp.Api/OrderTrackingApp.Api.csproj
new file mode 100644
index 0000000..b4123b8
--- /dev/null
+++ b/OrderTrackingApp.Api/OrderTrackingApp.Api.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+ enable
+ fd420179-d5b0-44df-b29b-018fbfbdb9f1
+ Linux
+ ..\OrderTrackingApp
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Api/Program.cs b/OrderTrackingApp.Api/Program.cs
new file mode 100644
index 0000000..571889f
--- /dev/null
+++ b/OrderTrackingApp.Api/Program.cs
@@ -0,0 +1,33 @@
+using OrderTrackingApp.Infrastructure.Extensions;
+using OrderTrackingApp.Persistence.Extensions;
+using OrderTrackingApp.ReadPersistence.Extensions;
+using OrderTrackingApp.Application.Extensions;
+using OrderTrackingApp.Api.Extensions;
+using Scalar.AspNetCore;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddPersistenceServices(builder.Configuration);
+builder.Services.AddReadPersistence();
+builder.Services.AddInfrastructureServices();
+builder.Services.AddApplicationServices();
+builder.Services.AddApiServices();
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger(options =>
+ {
+ options.RouteTemplate = "/openapi/{documentName}.json";
+ });
+ app.MapScalarApiReference();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/OrderTrackingApp.Api/Properties/launchSettings.json b/OrderTrackingApp.Api/Properties/launchSettings.json
new file mode 100644
index 0000000..4c1dc4f
--- /dev/null
+++ b/OrderTrackingApp.Api/Properties/launchSettings.json
@@ -0,0 +1,42 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:12188",
+ "sslPort": 44349
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "launchUrl": "scalar",
+ "applicationUrl": "http://localhost:5077",
+ "hotReloadEnabled": false,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7113;http://localhost:5077",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/OrderTrackingApp.Api/Validators/CreateOrderRequestValidator.cs b/OrderTrackingApp.Api/Validators/CreateOrderRequestValidator.cs
new file mode 100644
index 0000000..e5fe048
--- /dev/null
+++ b/OrderTrackingApp.Api/Validators/CreateOrderRequestValidator.cs
@@ -0,0 +1,20 @@
+using FluentValidation;
+using OrderTrackingApp.Api.Models;
+
+namespace OrderTrackingApp.Api.Validators
+{
+ public class CreateOrderRequestValidator : AbstractValidator
+ {
+ public CreateOrderRequestValidator()
+ {
+ RuleFor(x => x.CustomerId).NotEmpty();
+ RuleFor(x => x.Items).NotEmpty();
+
+ RuleForEach(x => x.Items).ChildRules(items =>
+ {
+ items.RuleFor(i => i.ProductId).NotEmpty();
+ items.RuleFor(i => i.Quantity).GreaterThan(0);
+ });
+ }
+ }
+}
diff --git a/OrderTrackingApp.Client/wwwroot/appsettings.Development.json b/OrderTrackingApp.Api/appsettings.Development.json
similarity index 100%
rename from OrderTrackingApp.Client/wwwroot/appsettings.Development.json
rename to OrderTrackingApp.Api/appsettings.Development.json
diff --git a/OrderTrackingApp.Api/appsettings.json b/OrderTrackingApp.Api/appsettings.json
new file mode 100644
index 0000000..7013322
--- /dev/null
+++ b/OrderTrackingApp.Api/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "SqlConnection": "Server=localhost,1433;Database=OrderDb;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True",
+ "MongoDb": "mongodb://admin:secret@localhost:27017",
+ "RabbitMq": "amqp://guest:guest@localhost:5672/"
+ }
+}
diff --git a/OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj b/OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj
new file mode 100644
index 0000000..55356d8
--- /dev/null
+++ b/OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Application.Contracts/Orders/IOrderReadRepository.cs b/OrderTrackingApp.Application.Contracts/Orders/IOrderReadRepository.cs
new file mode 100644
index 0000000..cb387b9
--- /dev/null
+++ b/OrderTrackingApp.Application.Contracts/Orders/IOrderReadRepository.cs
@@ -0,0 +1,16 @@
+
+namespace OrderTrackingApp.Application.Contracts.Orders
+{
+ public interface IOrderReadRepository
+ {
+ Task GetByIdAsync(Guid id);
+
+ Task> GetAllAsync();
+
+ Task InsertAsync(OrderDto order);
+
+ Task> GetPaginatedOrdersAsync(int page, int pageSize, string? status,
+ DateTime? fromDate, DateTime? toDate,
+ CancellationToken cancellationToken);
+ }
+}
diff --git a/OrderTrackingApp.Application.Contracts/Orders/IOrderWriteRepository.cs b/OrderTrackingApp.Application.Contracts/Orders/IOrderWriteRepository.cs
new file mode 100644
index 0000000..0bb50bc
--- /dev/null
+++ b/OrderTrackingApp.Application.Contracts/Orders/IOrderWriteRepository.cs
@@ -0,0 +1,13 @@
+namespace OrderTrackingApp.Application.Contracts.Orders
+{
+ public interface IOrderWriteRepository
+ {
+ Task AddAsync(OrderDto order);
+
+ Task UpdateAsync(OrderDto order);
+
+ Task DeleteAsync(Guid id);
+
+ Task GetByIdAsync(Guid id);
+ }
+}
diff --git a/OrderTrackingApp.Application.Contracts/Orders/OrderDto.cs b/OrderTrackingApp.Application.Contracts/Orders/OrderDto.cs
new file mode 100644
index 0000000..33ba483
--- /dev/null
+++ b/OrderTrackingApp.Application.Contracts/Orders/OrderDto.cs
@@ -0,0 +1,28 @@
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.Application.Contracts.Orders
+{
+ public class OrderDto
+ {
+ public Guid Id { get; set; }
+
+ public string OrderNumber { get; set; } = string.Empty;
+
+ public OrderStatus Status { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+
+ public Guid CustomerId { get; set; }
+
+ public List Items { get; set; } = [];
+ }
+
+ public class OrderItemDto
+ {
+ public Guid ProductId { get; set; }
+
+ public int Quantity { get; set; }
+
+ public decimal UnitPrice { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Application.Contracts/PaginatedResult.cs b/OrderTrackingApp.Application.Contracts/PaginatedResult.cs
new file mode 100644
index 0000000..773201d
--- /dev/null
+++ b/OrderTrackingApp.Application.Contracts/PaginatedResult.cs
@@ -0,0 +1,13 @@
+namespace OrderTrackingApp.Application.Contracts
+{
+ public class PaginatedResult(List items, long totalCount, int page, int pageSize)
+ {
+ public List Items { get; } = items;
+
+ public long TotalCount { get; } = totalCount;
+
+ public int Page { get; } = page;
+
+ public int PageSize { get; } = pageSize;
+ }
+}
diff --git a/OrderTrackingApp.Application/Extensions/ApplicationServiceRegistrations.cs b/OrderTrackingApp.Application/Extensions/ApplicationServiceRegistrations.cs
new file mode 100644
index 0000000..ca42207
--- /dev/null
+++ b/OrderTrackingApp.Application/Extensions/ApplicationServiceRegistrations.cs
@@ -0,0 +1,16 @@
+using Microsoft.Extensions.DependencyInjection;
+using OrderTrackingApp.Application.MappingProfiles;
+using OrderTrackingApp.Application.Orders.Commands;
+
+namespace OrderTrackingApp.Application.Extensions
+{
+ public static class ApplicationServiceRegistrations
+ {
+ public static IServiceCollection AddApplicationServices(this IServiceCollection services)
+ {
+ services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommandHandler).Assembly));
+ services.AddAutoMapper(config => { }, typeof(OrderProfile).Assembly);
+ return services;
+ }
+ }
+}
diff --git a/OrderTrackingApp.Application/MappingProfiles/OrderProfile.cs b/OrderTrackingApp.Application/MappingProfiles/OrderProfile.cs
new file mode 100644
index 0000000..611290f
--- /dev/null
+++ b/OrderTrackingApp.Application/MappingProfiles/OrderProfile.cs
@@ -0,0 +1,15 @@
+using AutoMapper;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.Application.MappingProfiles
+{
+ public class OrderProfile : Profile
+ {
+ public OrderProfile()
+ {
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/OrderTrackingApp.Application/OrderTrackingApp.Application.csproj b/OrderTrackingApp.Application/OrderTrackingApp.Application.csproj
new file mode 100644
index 0000000..3e668a6
--- /dev/null
+++ b/OrderTrackingApp.Application/OrderTrackingApp.Application.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommand.cs b/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommand.cs
new file mode 100644
index 0000000..491fdc3
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommand.cs
@@ -0,0 +1,18 @@
+using MediatR;
+
+namespace OrderTrackingApp.Application.Orders.Commands
+{
+ public class CreateOrderCommand : IRequest
+ {
+ public Guid CustomerId { get; set; }
+
+ public List Items { get; set; } = [];
+ }
+
+ public class CreateOrderItemDto
+ {
+ public Guid ProductId { get; set; }
+
+ public int Quantity { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommandHandler.cs b/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommandHandler.cs
new file mode 100644
index 0000000..3c161b5
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Commands/CreateOrderCommandHandler.cs
@@ -0,0 +1,59 @@
+using MediatR;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Entities;
+using OrderTrackingApp.Domain.Events;
+using OrderTrackingApp.Domain.Interfaces;
+
+namespace OrderTrackingApp.Application.Orders.Commands
+{
+ public class CreateOrderCommandHandler(IOrderWriteRepository orderRepository, IProductRepository productRepository, IMediator mediator)
+ : IRequestHandler
+ {
+ public async Task Handle(CreateOrderCommand request, CancellationToken cancellationToken)
+ {
+ var orderItems = new List();
+ var updatedProducts = new List();
+
+ var productIds = request.Items.Select(item => item.ProductId).ToList();
+ var products = await productRepository.GetByIdsAsync(productIds);
+ var productDict = products.ToDictionary(p => p.Id, p => p);
+
+ foreach (var item in request.Items)
+ {
+ if (!productDict.TryGetValue(item.ProductId, out Product? product))
+ {
+ throw new KeyNotFoundException($"Product with ID {item.ProductId} not found.");
+ }
+
+ if(product.StockQuantity < item.Quantity)
+ {
+ throw new InvalidOperationException($"Insufficient stock for product {product.Name}. Available: {product.StockQuantity}, Requested: {item.Quantity}.");
+ }
+
+ orderItems.Add(new OrderItemDto { ProductId = item.ProductId, Quantity = item.Quantity, UnitPrice = product.Price });
+
+ product.StockQuantity -= item.Quantity;
+
+ updatedProducts.Add(product);
+ }
+
+ await productRepository.UpdateProducts(updatedProducts);
+
+ var order = new OrderDto
+ {
+ Id = Guid.NewGuid(),
+ OrderNumber = $"ORD-{DateTime.UtcNow.Ticks}",
+ CustomerId = request.CustomerId,
+ CreatedAt = DateTime.UtcNow,
+ Status = OrderStatus.Pending,
+ Items = orderItems
+ };
+
+ await orderRepository.AddAsync(order);
+
+ await mediator.Publish(new OrderCreatedEvent(order.Id), cancellationToken);
+
+ return order.Id;
+ }
+ }
+}
diff --git a/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQuery.cs b/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQuery.cs
new file mode 100644
index 0000000..d1c39ca
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQuery.cs
@@ -0,0 +1,7 @@
+using MediatR;
+using OrderTrackingApp.Application.Contracts.Orders;
+
+namespace OrderTrackingApp.Application.Orders.Queries
+{
+ public record GetOrderByIdQuery(Guid OrderId) : IRequest;
+}
diff --git a/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQueryHandler.cs b/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQueryHandler.cs
new file mode 100644
index 0000000..5be44d3
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Queries/GetOrderByIdQueryHandler.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using OrderTrackingApp.Application.Contracts.Orders;
+
+namespace OrderTrackingApp.Application.Orders.Queries
+{
+ public class GetOrderByIdQueryHandler(IOrderReadRepository orderReadRepository) : IRequestHandler
+ {
+ public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
+ {
+ var order = await orderReadRepository.GetByIdAsync(request.OrderId);
+ return order ?? throw new Exception($"Order not found for ID {request.OrderId}");
+ }
+ }
+}
diff --git a/OrderTrackingApp.Application/Orders/Queries/GetOrdersQuery.cs b/OrderTrackingApp.Application/Orders/Queries/GetOrdersQuery.cs
new file mode 100644
index 0000000..729822a
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Queries/GetOrdersQuery.cs
@@ -0,0 +1,19 @@
+using MediatR;
+using OrderTrackingApp.Application.Contracts;
+using OrderTrackingApp.Application.Contracts.Orders;
+
+namespace OrderTrackingApp.Application.Orders.Queries
+{
+ public record GetOrdersQuery : IRequest>
+ {
+ public int Page { get; set; } = 1;
+
+ public int PageSize { get; set; } = 50;
+
+ public string? Status { get; set; }
+
+ public DateTime? FromDate { get; set; }
+
+ public DateTime? ToDate { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Application/Orders/Queries/GetOrdersQueryHandler.cs b/OrderTrackingApp.Application/Orders/Queries/GetOrdersQueryHandler.cs
new file mode 100644
index 0000000..7756a4e
--- /dev/null
+++ b/OrderTrackingApp.Application/Orders/Queries/GetOrdersQueryHandler.cs
@@ -0,0 +1,20 @@
+using MediatR;
+using OrderTrackingApp.Application.Contracts;
+using OrderTrackingApp.Application.Contracts.Orders;
+
+namespace OrderTrackingApp.Application.Orders.Queries
+{
+ public class GetOrdersQueryHandler(IOrderReadRepository orderReadRepository) : IRequestHandler>
+ {
+ public async Task> Handle(GetOrdersQuery request, CancellationToken cancellationToken)
+ {
+ return await orderReadRepository.GetPaginatedOrdersAsync(
+ page: request.Page,
+ pageSize: request.PageSize,
+ status: request.Status,
+ fromDate: request.FromDate,
+ toDate: request.ToDate,
+ cancellationToken: cancellationToken);
+ }
+ }
+}
diff --git a/OrderTrackingApp.Client/OrderTrackingApp.Blazor.Client.csproj b/OrderTrackingApp.Blazor.Client/OrderTrackingApp.Blazor.Client.csproj
similarity index 83%
rename from OrderTrackingApp.Client/OrderTrackingApp.Blazor.Client.csproj
rename to OrderTrackingApp.Blazor.Client/OrderTrackingApp.Blazor.Client.csproj
index 81d27a4..aaeb1dd 100644
--- a/OrderTrackingApp.Client/OrderTrackingApp.Blazor.Client.csproj
+++ b/OrderTrackingApp.Blazor.Client/OrderTrackingApp.Blazor.Client.csproj
@@ -1,7 +1,7 @@
- net8.0
+ net10.0
enable
enable
true
@@ -9,7 +9,7 @@
-
+
diff --git a/OrderTrackingApp.Client/Pages/Counter.razor b/OrderTrackingApp.Blazor.Client/Pages/Counter.razor
similarity index 100%
rename from OrderTrackingApp.Client/Pages/Counter.razor
rename to OrderTrackingApp.Blazor.Client/Pages/Counter.razor
diff --git a/OrderTrackingApp.Client/Program.cs b/OrderTrackingApp.Blazor.Client/Program.cs
similarity index 100%
rename from OrderTrackingApp.Client/Program.cs
rename to OrderTrackingApp.Blazor.Client/Program.cs
diff --git a/OrderTrackingApp.Client/_Imports.razor b/OrderTrackingApp.Blazor.Client/_Imports.razor
similarity index 89%
rename from OrderTrackingApp.Client/_Imports.razor
rename to OrderTrackingApp.Blazor.Client/_Imports.razor
index 425c382..0b4c4a9 100644
--- a/OrderTrackingApp.Client/_Imports.razor
+++ b/OrderTrackingApp.Blazor.Client/_Imports.razor
@@ -6,4 +6,4 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
-@using OrderTrackingApp.Client
+@using OrderTrackingApp.Blazor.Client
diff --git a/OrderTrackingApp/appsettings.Development.json b/OrderTrackingApp.Blazor.Client/wwwroot/appsettings.Development.json
similarity index 100%
rename from OrderTrackingApp/appsettings.Development.json
rename to OrderTrackingApp.Blazor.Client/wwwroot/appsettings.Development.json
diff --git a/OrderTrackingApp.Client/wwwroot/appsettings.json b/OrderTrackingApp.Blazor.Client/wwwroot/appsettings.json
similarity index 100%
rename from OrderTrackingApp.Client/wwwroot/appsettings.json
rename to OrderTrackingApp.Blazor.Client/wwwroot/appsettings.json
diff --git a/OrderTrackingApp/Components/App.razor b/OrderTrackingApp.Blazor.Server/Components/App.razor
similarity index 85%
rename from OrderTrackingApp/Components/App.razor
rename to OrderTrackingApp.Blazor.Server/Components/App.razor
index 207b995..a009aa6 100644
--- a/OrderTrackingApp/Components/App.razor
+++ b/OrderTrackingApp.Blazor.Server/Components/App.razor
@@ -7,7 +7,7 @@
-
+
diff --git a/OrderTrackingApp/Components/Layout/MainLayout.razor b/OrderTrackingApp.Blazor.Server/Components/Layout/MainLayout.razor
similarity index 100%
rename from OrderTrackingApp/Components/Layout/MainLayout.razor
rename to OrderTrackingApp.Blazor.Server/Components/Layout/MainLayout.razor
diff --git a/OrderTrackingApp/Components/Layout/MainLayout.razor.css b/OrderTrackingApp.Blazor.Server/Components/Layout/MainLayout.razor.css
similarity index 100%
rename from OrderTrackingApp/Components/Layout/MainLayout.razor.css
rename to OrderTrackingApp.Blazor.Server/Components/Layout/MainLayout.razor.css
diff --git a/OrderTrackingApp/Components/Layout/NavMenu.razor b/OrderTrackingApp.Blazor.Server/Components/Layout/NavMenu.razor
similarity index 94%
rename from OrderTrackingApp/Components/Layout/NavMenu.razor
rename to OrderTrackingApp.Blazor.Server/Components/Layout/NavMenu.razor
index d3c64c9..2580c74 100644
--- a/OrderTrackingApp/Components/Layout/NavMenu.razor
+++ b/OrderTrackingApp.Blazor.Server/Components/Layout/NavMenu.razor
@@ -1,6 +1,6 @@
diff --git a/OrderTrackingApp/Components/Layout/NavMenu.razor.css b/OrderTrackingApp.Blazor.Server/Components/Layout/NavMenu.razor.css
similarity index 100%
rename from OrderTrackingApp/Components/Layout/NavMenu.razor.css
rename to OrderTrackingApp.Blazor.Server/Components/Layout/NavMenu.razor.css
diff --git a/OrderTrackingApp/Components/Pages/Error.razor b/OrderTrackingApp.Blazor.Server/Components/Pages/Error.razor
similarity index 100%
rename from OrderTrackingApp/Components/Pages/Error.razor
rename to OrderTrackingApp.Blazor.Server/Components/Pages/Error.razor
diff --git a/OrderTrackingApp/Components/Pages/Home.razor b/OrderTrackingApp.Blazor.Server/Components/Pages/Home.razor
similarity index 100%
rename from OrderTrackingApp/Components/Pages/Home.razor
rename to OrderTrackingApp.Blazor.Server/Components/Pages/Home.razor
diff --git a/OrderTrackingApp/Components/Pages/Weather.razor b/OrderTrackingApp.Blazor.Server/Components/Pages/Weather.razor
similarity index 100%
rename from OrderTrackingApp/Components/Pages/Weather.razor
rename to OrderTrackingApp.Blazor.Server/Components/Pages/Weather.razor
diff --git a/OrderTrackingApp/Components/Routes.razor b/OrderTrackingApp.Blazor.Server/Components/Routes.razor
similarity index 100%
rename from OrderTrackingApp/Components/Routes.razor
rename to OrderTrackingApp.Blazor.Server/Components/Routes.razor
diff --git a/OrderTrackingApp/Components/_Imports.razor b/OrderTrackingApp.Blazor.Server/Components/_Imports.razor
similarity index 78%
rename from OrderTrackingApp/Components/_Imports.razor
rename to OrderTrackingApp.Blazor.Server/Components/_Imports.razor
index 6b191ef..564b32f 100644
--- a/OrderTrackingApp/Components/_Imports.razor
+++ b/OrderTrackingApp.Blazor.Server/Components/_Imports.razor
@@ -6,6 +6,5 @@
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
-@using OrderTrackingApp
-@using OrderTrackingApp.Client
-@using OrderTrackingApp.Components
+@using OrderTrackingApp.Blazor.Server
+@using OrderTrackingApp.Blazor.Server.Components
diff --git a/OrderTrackingApp.Blazor.Server/Dockerfile b/OrderTrackingApp.Blazor.Server/Dockerfile
new file mode 100644
index 0000000..52f2bdb
--- /dev/null
+++ b/OrderTrackingApp.Blazor.Server/Dockerfile
@@ -0,0 +1,32 @@
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+COPY ["OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj", "OrderTrackingApp.Blazor.Server/"]
+COPY ["OrderTrackingApp.Blazor.Client/OrderTrackingApp.Blazor.Client.csproj", "OrderTrackingApp.Blazor.Client/"]
+
+# Copy root package props first
+COPY ["Directory.Packages.props", "."]
+
+# Copy your root certificate into the container
+COPY nscacert.crt /usr/local/share/ca-certificates/nscacert.crt
+
+# Update the certificate store
+RUN update-ca-certificates
+
+RUN dotnet restore "OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj"
+
+COPY . .
+
+RUN dotnet build "OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj" -c Release -o /app/build --no-restore
+
+FROM build AS publish
+RUN dotnet publish "OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj" -c Release -o /app/publish --no-restore
+
+FROM mcr.microsoft.com/dotnet/aspnet:10.0
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+EXPOSE 8080
+
+# Start the app
+ENTRYPOINT ["dotnet", "OrderTrackingApp.Blazor.Server.dll"]
\ No newline at end of file
diff --git a/OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj b/OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj
new file mode 100644
index 0000000..96d03b4
--- /dev/null
+++ b/OrderTrackingApp.Blazor.Server/OrderTrackingApp.Blazor.Server.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+
+
diff --git a/OrderTrackingApp/Program.cs b/OrderTrackingApp.Blazor.Server/Program.cs
similarity index 83%
rename from OrderTrackingApp/Program.cs
rename to OrderTrackingApp.Blazor.Server/Program.cs
index 075c206..d784bf9 100644
--- a/OrderTrackingApp/Program.cs
+++ b/OrderTrackingApp.Blazor.Server/Program.cs
@@ -1,5 +1,4 @@
-using OrderTrackingApp.Client.Pages;
-using OrderTrackingApp.Components;
+using OrderTrackingApp.Blazor.Server.Components;
var builder = WebApplication.CreateBuilder(args);
@@ -30,6 +29,6 @@
app.MapRazorComponents()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
- .AddAdditionalAssemblies(typeof(OrderTrackingApp.Client._Imports).Assembly);
+ .AddAdditionalAssemblies(typeof(OrderTrackingApp.Blazor.Client._Imports).Assembly);
app.Run();
diff --git a/OrderTrackingApp/Properties/launchSettings.json b/OrderTrackingApp.Blazor.Server/Properties/launchSettings.json
similarity index 85%
rename from OrderTrackingApp/Properties/launchSettings.json
rename to OrderTrackingApp.Blazor.Server/Properties/launchSettings.json
index f4d5c4c..46d14d4 100644
--- a/OrderTrackingApp/Properties/launchSettings.json
+++ b/OrderTrackingApp.Blazor.Server/Properties/launchSettings.json
@@ -4,8 +4,8 @@
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
- "applicationUrl": "http://localhost:12383",
- "sslPort": 44332
+ "applicationUrl": "http://localhost:48834",
+ "sslPort": 44398
}
},
"profiles": {
@@ -14,7 +14,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "applicationUrl": "http://localhost:5166",
+ "applicationUrl": "http://localhost:5229",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -24,7 +24,7 @@
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "applicationUrl": "https://localhost:7299;http://localhost:5166",
+ "applicationUrl": "https://localhost:7262;http://localhost:5229",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/OrderTrackingApp.Blazor.Server/appsettings.Development.json b/OrderTrackingApp.Blazor.Server/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/OrderTrackingApp.Blazor.Server/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/OrderTrackingApp/appsettings.json b/OrderTrackingApp.Blazor.Server/appsettings.json
similarity index 100%
rename from OrderTrackingApp/appsettings.json
rename to OrderTrackingApp.Blazor.Server/appsettings.json
diff --git a/OrderTrackingApp/wwwroot/app.css b/OrderTrackingApp.Blazor.Server/wwwroot/app.css
similarity index 100%
rename from OrderTrackingApp/wwwroot/app.css
rename to OrderTrackingApp.Blazor.Server/wwwroot/app.css
diff --git a/OrderTrackingApp/wwwroot/bootstrap/bootstrap.min.css b/OrderTrackingApp.Blazor.Server/wwwroot/bootstrap/bootstrap.min.css
similarity index 100%
rename from OrderTrackingApp/wwwroot/bootstrap/bootstrap.min.css
rename to OrderTrackingApp.Blazor.Server/wwwroot/bootstrap/bootstrap.min.css
diff --git a/OrderTrackingApp/wwwroot/bootstrap/bootstrap.min.css.map b/OrderTrackingApp.Blazor.Server/wwwroot/bootstrap/bootstrap.min.css.map
similarity index 100%
rename from OrderTrackingApp/wwwroot/bootstrap/bootstrap.min.css.map
rename to OrderTrackingApp.Blazor.Server/wwwroot/bootstrap/bootstrap.min.css.map
diff --git a/OrderTrackingApp/wwwroot/favicon.png b/OrderTrackingApp.Blazor.Server/wwwroot/favicon.png
similarity index 100%
rename from OrderTrackingApp/wwwroot/favicon.png
rename to OrderTrackingApp.Blazor.Server/wwwroot/favicon.png
diff --git a/OrderTrackingApp.Consumer/Dockerfile b/OrderTrackingApp.Consumer/Dockerfile
new file mode 100644
index 0000000..9b2b11d
--- /dev/null
+++ b/OrderTrackingApp.Consumer/Dockerfile
@@ -0,0 +1,36 @@
+FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# Copy root package props first
+COPY ["Directory.Packages.props", "."]
+
+COPY ["OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj", "OrderTrackingApp.Consumer/"]
+COPY ["OrderTrackingApp.Application/OrderTrackingApp.Application.csproj", "OrderTrackingApp.Application/"]
+COPY ["OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj", "OrderTrackingApp.Application.Contracts/"]
+COPY ["OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj", "OrderTrackingApp.Domain/"]
+COPY ["OrderTrackingApp.Infrastructure/OrderTrackingApp.Infrastructure.csproj", "OrderTrackingApp.Infrastructure/"]
+COPY ["OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj", "OrderTrackingApp.Persistence/"]
+COPY ["OrderTrackingApp.ReadPersistence/OrderTrackingApp.ReadPersistence.csproj", "OrderTrackingApp.ReadPersistence/"]
+
+# Copy your root certificate into the container
+COPY nscacert.crt /usr/local/share/ca-certificates/nscacert.crt
+
+# Update the certificate store
+RUN update-ca-certificates
+
+RUN dotnet restore "OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj"
+
+COPY . .
+
+RUN dotnet build "OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj" -c Release -o /app/build --no-restore
+
+FROM build AS publish
+RUN dotnet publish "OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj" -c Release -o /app/publish --no-restore
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+ENTRYPOINT ["dotnet", "OrderTrackingApp.Consumer.dll"]
diff --git a/OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj b/OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj
new file mode 100644
index 0000000..bf42411
--- /dev/null
+++ b/OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net10.0
+ enable
+ enable
+ dotnet-OrderTrackingApp.Consumer-4c972be3-521f-42ce-87d8-815ba1ccddd7
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Consumer/Program.cs b/OrderTrackingApp.Consumer/Program.cs
new file mode 100644
index 0000000..2e4ec64
--- /dev/null
+++ b/OrderTrackingApp.Consumer/Program.cs
@@ -0,0 +1,14 @@
+using OrderTrackingApp.Consumer;
+using OrderTrackingApp.Persistence.Extensions;
+using OrderTrackingApp.ReadPersistence.Extensions;
+using OrderTrackingApp.Application.Extensions;
+
+var builder = Host.CreateApplicationBuilder(args);
+builder.Services.AddHostedService();
+
+builder.Services.AddPersistenceServices(builder.Configuration);
+builder.Services.AddReadPersistence();
+builder.Services.AddApplicationServices();
+
+var host = builder.Build();
+host.Run();
diff --git a/OrderTrackingApp.Consumer/Properties/launchSettings.json b/OrderTrackingApp.Consumer/Properties/launchSettings.json
new file mode 100644
index 0000000..49b44b4
--- /dev/null
+++ b/OrderTrackingApp.Consumer/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "OrderTrackingApp.Consumer": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "hotReloadEnabled": false,
+ "environmentVariables": {
+ "DOTNET_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/OrderTrackingApp.Consumer/Worker.cs b/OrderTrackingApp.Consumer/Worker.cs
new file mode 100644
index 0000000..3fc0b34
--- /dev/null
+++ b/OrderTrackingApp.Consumer/Worker.cs
@@ -0,0 +1,76 @@
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Events;
+using RabbitMQ.Client;
+using RabbitMQ.Client.Events;
+using System.Text;
+using System.Text.Json;
+
+namespace OrderTrackingApp.Consumer;
+
+public class Worker(ILogger logger, IServiceProvider serviceProvider) : BackgroundService
+{
+ private IConnection? _connection;
+ private IChannel? _channel;
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await InitializeRabbitMqListener();
+
+ var consumer = new AsyncEventingBasicConsumer(_channel!);
+
+ consumer.ReceivedAsync += async (model, ea) =>
+ {
+ try
+ {
+ using var scope = serviceProvider.CreateScope();
+ var orderWriteRepository = scope.ServiceProvider.GetRequiredService();
+ var orderReadRepository = scope.ServiceProvider.GetRequiredService();
+
+ var body = ea.Body.ToArray();
+ var message = Encoding.UTF8.GetString(body);
+
+ logger.LogInformation("Received message: {message}", message);
+
+ var orderEvent = JsonSerializer.Deserialize(message);
+
+ if (orderEvent != null)
+ {
+ var order = await orderWriteRepository.GetByIdAsync(orderEvent.OrderId)
+ ?? throw new Exception($"Order with ID {orderEvent.OrderId} not found.");
+
+ await orderReadRepository.InsertAsync(order);
+
+ logger.LogInformation("Synced OrderId {orderId} to MongoDB", order.Id);
+
+ await _channel!.BasicAckAsync(deliveryTag: ea.DeliveryTag, multiple: false);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Failed to process RabbitMQ message.");
+
+ await _channel!.BasicNackAsync(deliveryTag: ea.DeliveryTag, multiple: false, requeue: true);
+ }
+ };
+
+ await _channel!.BasicConsumeAsync(queue: "orders",
+ autoAck: false,
+ consumer: consumer,
+ stoppingToken);
+ }
+
+ private async Task InitializeRabbitMqListener()
+ {
+ var factory = new ConnectionFactory() { HostName = "localhost" };
+ _connection = await factory.CreateConnectionAsync();
+ _channel = await _connection.CreateChannelAsync();
+
+ await _channel.BasicQosAsync(0, 1, false);
+
+ await _channel.QueueDeclareAsync(queue: "orders",
+ durable: true,
+ exclusive: false,
+ autoDelete: false,
+ arguments: null);
+ }
+}
diff --git a/OrderTrackingApp.Consumer/appsettings.Development.json b/OrderTrackingApp.Consumer/appsettings.Development.json
new file mode 100644
index 0000000..b2dcdb6
--- /dev/null
+++ b/OrderTrackingApp.Consumer/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ }
+}
diff --git a/OrderTrackingApp.Consumer/appsettings.json b/OrderTrackingApp.Consumer/appsettings.json
new file mode 100644
index 0000000..ba98022
--- /dev/null
+++ b/OrderTrackingApp.Consumer/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.Hosting.Lifetime": "Information"
+ }
+ },
+ "ConnectionStrings": {
+ "SqlConnection": "Server=localhost,1433;Database=OrderDb;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True",
+ "MongoDb": "mongodb://admin:secret@localhost:27017",
+ "RabbitMq": "amqp://guest:guest@localhost:5672/"
+ }
+}
diff --git a/OrderTrackingApp.Domain/Entities/Order.cs b/OrderTrackingApp.Domain/Entities/Order.cs
new file mode 100644
index 0000000..045d6c2
--- /dev/null
+++ b/OrderTrackingApp.Domain/Entities/Order.cs
@@ -0,0 +1,17 @@
+namespace OrderTrackingApp.Domain.Entities
+{
+ public class Order
+ {
+ public Guid Id { get; set; }
+
+ public string OrderNumber { get; set; } = string.Empty;
+
+ public Guid CustomerId { get; set; }
+
+ public DateTime OrderDate { get; set; }
+
+ public OrderStatus Status { get; set; }
+
+ public List Items { get; set; } = [];
+ }
+}
diff --git a/OrderTrackingApp.Domain/Entities/OrderItem.cs b/OrderTrackingApp.Domain/Entities/OrderItem.cs
new file mode 100644
index 0000000..a73e340
--- /dev/null
+++ b/OrderTrackingApp.Domain/Entities/OrderItem.cs
@@ -0,0 +1,14 @@
+namespace OrderTrackingApp.Domain.Entities
+{
+ public class OrderItem
+ {
+ public Guid ProductId { get; set; }
+
+ public int Quantity { get; set; }
+
+ public decimal UnitPrice { get; set; }
+
+ public decimal TotalPrice => Quantity * UnitPrice;
+ }
+
+}
diff --git a/OrderTrackingApp.Domain/Entities/OrderStatus.cs b/OrderTrackingApp.Domain/Entities/OrderStatus.cs
new file mode 100644
index 0000000..e404730
--- /dev/null
+++ b/OrderTrackingApp.Domain/Entities/OrderStatus.cs
@@ -0,0 +1,11 @@
+namespace OrderTrackingApp.Domain.Entities
+{
+ public enum OrderStatus
+ {
+ Pending = 0,
+ Processing = 1,
+ Shipped = 2,
+ Delivered = 3,
+ Cancelled = 4
+ }
+}
diff --git a/OrderTrackingApp.Domain/Entities/Product.cs b/OrderTrackingApp.Domain/Entities/Product.cs
new file mode 100644
index 0000000..2ca56ef
--- /dev/null
+++ b/OrderTrackingApp.Domain/Entities/Product.cs
@@ -0,0 +1,17 @@
+namespace OrderTrackingApp.Domain.Entities
+{
+ public class Product
+ {
+ public Guid Id { get; set; }
+
+ public string Name { get; set; } = string.Empty;
+
+ public string Sku { get; set; } = string.Empty;
+
+ public string Description { get; set; } = string.Empty;
+
+ public decimal Price { get; set; }
+
+ public int StockQuantity { get; set; }
+ }
+}
diff --git a/OrderTrackingApp.Domain/Events/OrderCreatedEvent.cs b/OrderTrackingApp.Domain/Events/OrderCreatedEvent.cs
new file mode 100644
index 0000000..8e3f471
--- /dev/null
+++ b/OrderTrackingApp.Domain/Events/OrderCreatedEvent.cs
@@ -0,0 +1,9 @@
+using MediatR;
+
+namespace OrderTrackingApp.Domain.Events
+{
+ public class OrderCreatedEvent(Guid orderId) : INotification
+ {
+ public Guid OrderId { get; } = orderId;
+ }
+}
diff --git a/OrderTrackingApp.Domain/Interfaces/IProductRepository.cs b/OrderTrackingApp.Domain/Interfaces/IProductRepository.cs
new file mode 100644
index 0000000..60b1367
--- /dev/null
+++ b/OrderTrackingApp.Domain/Interfaces/IProductRepository.cs
@@ -0,0 +1,17 @@
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.Domain.Interfaces
+{
+ public interface IProductRepository
+ {
+ Task GetByIdAsync(Guid id);
+
+ Task> GetByIdsAsync(IEnumerable ids);
+
+ Task AddAsync(Product product);
+
+ Task UpdateAsync(Product product);
+
+ Task UpdateProducts(List updatedProducts);
+ }
+}
diff --git a/OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj b/OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj
new file mode 100644
index 0000000..12bdd8c
--- /dev/null
+++ b/OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Infrastructure/EventHandlers/OrderCreatedEventHandler.cs b/OrderTrackingApp.Infrastructure/EventHandlers/OrderCreatedEventHandler.cs
new file mode 100644
index 0000000..d035896
--- /dev/null
+++ b/OrderTrackingApp.Infrastructure/EventHandlers/OrderCreatedEventHandler.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using OrderTrackingApp.Domain.Events;
+using OrderTrackingApp.Infrastructure.Messaging;
+
+namespace OrderTrackingApp.Infrastructure.EventHandlers
+{
+ public class OrderCreatedEventHandler(IMessagePublisher messagePublisher) : INotificationHandler
+ {
+ public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
+ {
+ await messagePublisher.PublishAsync("orders", notification);
+ }
+ }
+}
diff --git a/OrderTrackingApp.Infrastructure/Extensions/InfrastructureServiceRegistrations.cs b/OrderTrackingApp.Infrastructure/Extensions/InfrastructureServiceRegistrations.cs
new file mode 100644
index 0000000..e587344
--- /dev/null
+++ b/OrderTrackingApp.Infrastructure/Extensions/InfrastructureServiceRegistrations.cs
@@ -0,0 +1,17 @@
+using Microsoft.Extensions.DependencyInjection;
+using OrderTrackingApp.Infrastructure.EventHandlers;
+using OrderTrackingApp.Infrastructure.Messaging;
+
+namespace OrderTrackingApp.Infrastructure.Extensions
+{
+ public static class InfrastructureServiceRegistrations
+ {
+ public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(OrderCreatedEventHandler).Assembly));
+
+ return services;
+ }
+ }
+}
diff --git a/OrderTrackingApp.Infrastructure/Messaging/IMessagePublisher.cs b/OrderTrackingApp.Infrastructure/Messaging/IMessagePublisher.cs
new file mode 100644
index 0000000..d2559e7
--- /dev/null
+++ b/OrderTrackingApp.Infrastructure/Messaging/IMessagePublisher.cs
@@ -0,0 +1,7 @@
+namespace OrderTrackingApp.Infrastructure.Messaging
+{
+ public interface IMessagePublisher
+ {
+ Task PublishAsync(string queue, T message);
+ }
+}
diff --git a/OrderTrackingApp.Infrastructure/Messaging/RabbitMqPublisher.cs b/OrderTrackingApp.Infrastructure/Messaging/RabbitMqPublisher.cs
new file mode 100644
index 0000000..cfd908f
--- /dev/null
+++ b/OrderTrackingApp.Infrastructure/Messaging/RabbitMqPublisher.cs
@@ -0,0 +1,38 @@
+using Microsoft.Extensions.Configuration;
+using RabbitMQ.Client;
+using System.Text;
+using System.Text.Json;
+
+namespace OrderTrackingApp.Infrastructure.Messaging
+{
+ internal class RabbitMqPublisher : IMessagePublisher
+ {
+ private readonly IConnection _connection;
+
+ public RabbitMqPublisher(IConfiguration configuration)
+ {
+ var connectionString = configuration.GetConnectionString("RabbitMq");
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ throw new Exception("RabbitMQ connection string is not configured.");
+ }
+
+ var factory = new ConnectionFactory
+ {
+ Uri = new Uri(connectionString)
+ };
+
+ _connection = factory.CreateConnectionAsync().GetAwaiter().GetResult();
+ }
+
+ public async Task PublishAsync(string topic, T message)
+ {
+ using var channel = await _connection.CreateChannelAsync();
+ await channel.QueueDeclareAsync(queue: topic, durable: true, exclusive: false, autoDelete: false);
+
+ var body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(message));
+ await channel.BasicPublishAsync(exchange: "", routingKey: topic, body: body);
+ }
+
+ }
+}
diff --git a/OrderTrackingApp.Infrastructure/OrderTrackingApp.Infrastructure.csproj b/OrderTrackingApp.Infrastructure/OrderTrackingApp.Infrastructure.csproj
new file mode 100644
index 0000000..2007db0
--- /dev/null
+++ b/OrderTrackingApp.Infrastructure/OrderTrackingApp.Infrastructure.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.MigrationRunner/Dockerfile b/OrderTrackingApp.MigrationRunner/Dockerfile
new file mode 100644
index 0000000..bbfbb43
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/Dockerfile
@@ -0,0 +1,35 @@
+FROM mcr.microsoft.com/dotnet/runtime:10.0 AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
+WORKDIR /src
+
+# Copy root package props first
+COPY ["Directory.Packages.props", "."]
+
+COPY ["OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj", "OrderTrackingApp.MigrationRunner/"]
+COPY ["OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj", "OrderTrackingApp.Persistence/"]
+COPY ["OrderTrackingApp.Application/OrderTrackingApp.Application.csproj", "OrderTrackingApp.Application/"]
+COPY ["OrderTrackingApp.Application.Contracts/OrderTrackingApp.Application.Contracts.csproj", "OrderTrackingApp.Application.Contracts/"]
+COPY ["OrderTrackingApp.Domain/OrderTrackingApp.Domain.csproj", "OrderTrackingApp.Domain/"]
+
+# Copy your root certificate into the container
+COPY nscacert.crt /usr/local/share/ca-certificates/nscacert.crt
+
+# Update the certificate store
+RUN update-ca-certificates
+
+RUN dotnet restore "OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj"
+
+COPY . .
+
+RUN dotnet build "OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj" -c Release -o /app/build --no-restore
+
+FROM build AS publish
+RUN dotnet publish "OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+
+ENTRYPOINT ["dotnet", "OrderTrackingApp.MigrationRunner.dll"]
\ No newline at end of file
diff --git a/OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj b/OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj
new file mode 100644
index 0000000..7ea555c
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/OrderTrackingApp.MigrationRunner.csproj
@@ -0,0 +1,30 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+ Linux
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.MigrationRunner/ProductSeeder.cs b/OrderTrackingApp.MigrationRunner/ProductSeeder.cs
new file mode 100644
index 0000000..0940046
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/ProductSeeder.cs
@@ -0,0 +1,42 @@
+using Microsoft.EntityFrameworkCore;
+using OrderTrackingApp.Domain.Entities;
+using OrderTrackingApp.Persistence;
+
+namespace OrderTrackingApp.MigrationRunner
+{
+ internal static class ProductSeeder
+ {
+ public static async Task SeedAsync(AppDbContext context, CancellationToken cancellationToken = default)
+ {
+ if (await context.Products.AnyAsync(cancellationToken))
+ {
+ return; // Already seeded
+ }
+
+ var products = new List
+ {
+ new()
+ {
+ Id = Guid.Parse("b111a4dd-f45c-4d40-a6a3-3002f62f823f"),
+ Name = "Wireless Mouse",
+ Sku = "MSE-001",
+ Description = "Ergonomic wireless mouse",
+ Price = 29.99m,
+ StockQuantity = 1000
+ },
+ new()
+ {
+ Id = Guid.Parse("2a13551b-059c-4035-b275-8a8cb82bd272"),
+ Name = "Mechanical Keyboard",
+ Sku = "KEY-101",
+ Description = "RGB mechanical keyboard",
+ Price = 79.99m,
+ StockQuantity = 500
+ }
+ };
+
+ context.Products.AddRange(products);
+ await context.SaveChangesAsync(cancellationToken);
+ }
+ }
+}
diff --git a/OrderTrackingApp.MigrationRunner/Program.cs b/OrderTrackingApp.MigrationRunner/Program.cs
new file mode 100644
index 0000000..4aa3674
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/Program.cs
@@ -0,0 +1,21 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using OrderTrackingApp.MigrationRunner;
+using OrderTrackingApp.Persistence;
+
+var host = Host.CreateDefaultBuilder(args)
+ .ConfigureServices((context, services) =>
+ {
+ var connStr = context.Configuration["ConnectionStrings:SqlConnection"];
+ services.AddDbContext(options =>
+ options.UseSqlServer(connStr));
+ })
+ .Build();
+
+using var scope = host.Services.CreateScope();
+var db = scope.ServiceProvider.GetRequiredService();
+db.Database.Migrate();
+
+await ProductSeeder.SeedAsync(db);
+
diff --git a/OrderTrackingApp.MigrationRunner/Properties/launchSettings.json b/OrderTrackingApp.MigrationRunner/Properties/launchSettings.json
new file mode 100644
index 0000000..139a592
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "profiles": {
+ "OrderTrackingApp.MigrationRunner": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "Container (Dockerfile)": {
+ "commandName": "Docker",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/OrderTrackingApp.MigrationRunner/appsettings.json b/OrderTrackingApp.MigrationRunner/appsettings.json
new file mode 100644
index 0000000..ab66b29
--- /dev/null
+++ b/OrderTrackingApp.MigrationRunner/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "ConnectionStrings": {
+ "SqlConnection": "Server=localhost,1433;Database=OrderDb;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True"
+ }
+}
diff --git a/OrderTrackingApp.Persistence.Tests/OrderTrackingApp.Persistence.Tests.csproj b/OrderTrackingApp.Persistence.Tests/OrderTrackingApp.Persistence.Tests.csproj
new file mode 100644
index 0000000..38307a0
--- /dev/null
+++ b/OrderTrackingApp.Persistence.Tests/OrderTrackingApp.Persistence.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Persistence/AppDbContext.cs b/OrderTrackingApp.Persistence/AppDbContext.cs
new file mode 100644
index 0000000..bb81642
--- /dev/null
+++ b/OrderTrackingApp.Persistence/AppDbContext.cs
@@ -0,0 +1,68 @@
+using Microsoft.EntityFrameworkCore;
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.Persistence
+{
+ public class AppDbContext(DbContextOptions options) : DbContext(options)
+ {
+ public DbSet Orders => Set();
+
+ public DbSet Products => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ // Order
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(x => x.Id);
+
+ entity.Property(x => x.OrderNumber)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ entity.Property(x => x.Status)
+ .HasConversion(); // Enum -> string
+
+ entity.Property(x => x.OrderDate)
+ .IsRequired();
+
+ // OrderItem - owned collection
+ entity.OwnsMany(x => x.Items, items =>
+ {
+ items.WithOwner().HasForeignKey("OrderId");
+ items.HasKey("OrderId", "ProductId"); // Composite Key
+
+ items.Property(x => x.ProductId).IsRequired();
+ items.Property(x => x.Quantity).IsRequired();
+ items.Property(x => x.UnitPrice).IsRequired().HasColumnType("decimal(18,2)");
+ items.Ignore(x => x.TotalPrice); // Computed property
+ items.ToTable("OrderItems");
+ });
+ });
+
+ // Product
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(x => x.Id);
+
+ entity.Property(x => x.Name)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ entity.Property(x => x.Sku)
+ .IsRequired()
+ .HasMaxLength(50);
+
+ entity.Property(x => x.Description)
+ .HasMaxLength(500);
+
+ entity.Property(x => x.Price)
+ .IsRequired()
+ .HasColumnType("decimal(18,2)");
+
+ entity.Property(x => x.StockQuantity)
+ .IsRequired();
+ });
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/DesignTimeDbContextFactory.cs b/OrderTrackingApp.Persistence/DesignTimeDbContextFactory.cs
new file mode 100644
index 0000000..b316aa2
--- /dev/null
+++ b/OrderTrackingApp.Persistence/DesignTimeDbContextFactory.cs
@@ -0,0 +1,16 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Design;
+
+namespace OrderTrackingApp.Persistence
+{
+ public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory
+ {
+ public AppDbContext CreateDbContext(string[] args)
+ {
+ var optionsBuilder = new DbContextOptionsBuilder();
+ optionsBuilder.UseSqlServer("Server=localhost,1433;Database=OrderDb;User=sa;Password=YourStrong!Pass123;");
+
+ return new AppDbContext(optionsBuilder.Options);
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/Extensions/PersistenceServiceRegistrations.cs b/OrderTrackingApp.Persistence/Extensions/PersistenceServiceRegistrations.cs
new file mode 100644
index 0000000..200ab96
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Extensions/PersistenceServiceRegistrations.cs
@@ -0,0 +1,23 @@
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Interfaces;
+using OrderTrackingApp.Persistence.Repositories;
+
+namespace OrderTrackingApp.Persistence.Extensions
+{
+ public static class PersistenceServiceRegistration
+ {
+ public static IServiceCollection AddPersistenceServices(this IServiceCollection services, IConfiguration config)
+ {
+ services.AddDbContext(options =>
+ options.UseSqlServer(config.GetConnectionString("SqlConnection")));
+
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.Designer.cs b/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.Designer.cs
new file mode 100644
index 0000000..c52b9a4
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.Designer.cs
@@ -0,0 +1,116 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OrderTrackingApp.Persistence;
+
+#nullable disable
+
+namespace OrderTrackingApp.Persistence.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20250716055404_InitSchema")]
+ partial class InitSchema
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.18")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Order", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CustomerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("OrderDate")
+ .HasColumnType("datetime2");
+
+ b.Property("OrderNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Orders");
+ });
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Product", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Price")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("StockQuantity")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Products");
+ });
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Order", b =>
+ {
+ b.OwnsMany("OrderTrackingApp.Domain.Entities.OrderItem", "Items", b1 =>
+ {
+ b1.Property("OrderId")
+ .HasColumnType("uniqueidentifier");
+
+ b1.Property("ProductId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b1.Property("Quantity")
+ .HasColumnType("int");
+
+ b1.Property("UnitPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b1.HasKey("OrderId", "ProductId");
+
+ b1.ToTable("OrderItems", (string)null);
+
+ b1.WithOwner()
+ .HasForeignKey("OrderId");
+ });
+
+ b.Navigation("Items");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.cs b/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.cs
new file mode 100644
index 0000000..cc9a3cf
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Migrations/20250716055404_InitSchema.cs
@@ -0,0 +1,78 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace OrderTrackingApp.Persistence.Migrations
+{
+ ///
+ public partial class InitSchema : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Orders",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false),
+ OrderNumber = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
+ CustomerId = table.Column(type: "uniqueidentifier", nullable: false),
+ OrderDate = table.Column(type: "datetime2", nullable: false),
+ Status = table.Column(type: "nvarchar(max)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Orders", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Products",
+ columns: table => new
+ {
+ Id = table.Column(type: "uniqueidentifier", nullable: false),
+ Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
+ Sku = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false),
+ Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false),
+ Price = table.Column(type: "decimal(18,2)", nullable: false),
+ StockQuantity = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Products", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "OrderItems",
+ columns: table => new
+ {
+ ProductId = table.Column(type: "uniqueidentifier", nullable: false),
+ OrderId = table.Column(type: "uniqueidentifier", nullable: false),
+ Quantity = table.Column(type: "int", nullable: false),
+ UnitPrice = table.Column(type: "decimal(18,2)", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_OrderItems", x => new { x.OrderId, x.ProductId });
+ table.ForeignKey(
+ name: "FK_OrderItems_Orders_OrderId",
+ column: x => x.OrderId,
+ principalTable: "Orders",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "OrderItems");
+
+ migrationBuilder.DropTable(
+ name: "Products");
+
+ migrationBuilder.DropTable(
+ name: "Orders");
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/Migrations/AppDbContextModelSnapshot.cs b/OrderTrackingApp.Persistence/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 0000000..834383a
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,113 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using OrderTrackingApp.Persistence;
+
+#nullable disable
+
+namespace OrderTrackingApp.Persistence.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.18")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Order", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("CustomerId")
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("OrderDate")
+ .HasColumnType("datetime2");
+
+ b.Property("OrderNumber")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Orders");
+ });
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Product", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Price")
+ .HasColumnType("decimal(18,2)");
+
+ b.Property("Sku")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)");
+
+ b.Property("StockQuantity")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Products");
+ });
+
+ modelBuilder.Entity("OrderTrackingApp.Domain.Entities.Order", b =>
+ {
+ b.OwnsMany("OrderTrackingApp.Domain.Entities.OrderItem", "Items", b1 =>
+ {
+ b1.Property("OrderId")
+ .HasColumnType("uniqueidentifier");
+
+ b1.Property("ProductId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uniqueidentifier");
+
+ b1.Property("Quantity")
+ .HasColumnType("int");
+
+ b1.Property("UnitPrice")
+ .HasColumnType("decimal(18,2)");
+
+ b1.HasKey("OrderId", "ProductId");
+
+ b1.ToTable("OrderItems", (string)null);
+
+ b1.WithOwner()
+ .HasForeignKey("OrderId");
+ });
+
+ b.Navigation("Items");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj b/OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj
new file mode 100644
index 0000000..3be187e
--- /dev/null
+++ b/OrderTrackingApp.Persistence/OrderTrackingApp.Persistence.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.Persistence/Repositories/OrderWriteRepository.cs b/OrderTrackingApp.Persistence/Repositories/OrderWriteRepository.cs
new file mode 100644
index 0000000..da9d827
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Repositories/OrderWriteRepository.cs
@@ -0,0 +1,46 @@
+using AutoMapper;
+using Microsoft.EntityFrameworkCore;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.Persistence.Repositories
+{
+ public class OrderWriteRepository(AppDbContext dbContext, IMapper mapper) : IOrderWriteRepository
+ {
+ public async Task AddAsync(OrderDto orderDto)
+ {
+ var order = mapper.Map(orderDto);
+
+ dbContext.Orders.Add(order);
+ await dbContext.SaveChangesAsync();
+ }
+
+ public async Task DeleteAsync(Guid id)
+ {
+ var order = await dbContext.Orders.FirstOrDefaultAsync(x => x.Id == id);
+
+ if(order != null)
+ {
+ dbContext.Orders.Remove(order);
+ await dbContext.SaveChangesAsync();
+ }
+ }
+
+ public async Task UpdateAsync(OrderDto orderDto)
+ {
+ var order = mapper.Map(orderDto);
+
+ dbContext.Orders.Update(order);
+ await dbContext.SaveChangesAsync();
+ }
+
+ public async Task GetByIdAsync(Guid id)
+ {
+ var order = await dbContext.Orders
+ .Include(o => o.Items)
+ .FirstOrDefaultAsync(x => x.Id == id);
+
+ return mapper.Map(order);
+ }
+ }
+}
diff --git a/OrderTrackingApp.Persistence/Repositories/ProductRepository.cs b/OrderTrackingApp.Persistence/Repositories/ProductRepository.cs
new file mode 100644
index 0000000..3ad607d
--- /dev/null
+++ b/OrderTrackingApp.Persistence/Repositories/ProductRepository.cs
@@ -0,0 +1,36 @@
+using Microsoft.EntityFrameworkCore;
+using OrderTrackingApp.Domain.Entities;
+using OrderTrackingApp.Domain.Interfaces;
+
+namespace OrderTrackingApp.Persistence.Repositories
+{
+ public class ProductRepository(AppDbContext appDbContext) : IProductRepository
+ {
+ public Task AddAsync(Product product)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task GetByIdAsync(Guid id)
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task> GetByIdsAsync(IEnumerable ids)
+ {
+ return appDbContext.Products.Where(p => ids.Any(id => id == p.Id)).ToListAsync();
+ }
+
+ public Task UpdateAsync(Product product)
+ {
+ throw new NotImplementedException();
+ }
+
+ public async Task UpdateProducts(List updatedProducts)
+ {
+ appDbContext.Products.UpdateRange(updatedProducts);
+
+ await appDbContext.SaveChangesAsync();
+ }
+ }
+}
diff --git a/OrderTrackingApp.ReadPersistence/Extensions/ReadPersistenceServiceRegistration.cs b/OrderTrackingApp.ReadPersistence/Extensions/ReadPersistenceServiceRegistration.cs
new file mode 100644
index 0000000..85c86ee
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/Extensions/ReadPersistenceServiceRegistration.cs
@@ -0,0 +1,18 @@
+using Microsoft.Extensions.DependencyInjection;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.ReadPersistence.Repositories;
+
+namespace OrderTrackingApp.ReadPersistence.Extensions
+{
+ public static class ReadPersistenceServiceRegistration
+ {
+ public static IServiceCollection AddReadPersistence(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddScoped();
+
+ services.AddAutoMapper(config => { }, typeof(MappingProfiles.OrderProfile).Assembly);
+ return services;
+ }
+ }
+}
diff --git a/OrderTrackingApp.ReadPersistence/MappingProfiles/OrderProfile.cs b/OrderTrackingApp.ReadPersistence/MappingProfiles/OrderProfile.cs
new file mode 100644
index 0000000..aa1c95b
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/MappingProfiles/OrderProfile.cs
@@ -0,0 +1,15 @@
+using AutoMapper;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.ReadPersistence.Models;
+
+namespace OrderTrackingApp.ReadPersistence.MappingProfiles
+{
+ public class OrderProfile : Profile
+ {
+ public OrderProfile()
+ {
+ CreateMap().ReverseMap();
+ CreateMap().ReverseMap();
+ }
+ }
+}
diff --git a/OrderTrackingApp.ReadPersistence/Models/OrderItemReadModel.cs b/OrderTrackingApp.ReadPersistence/Models/OrderItemReadModel.cs
new file mode 100644
index 0000000..2b70674
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/Models/OrderItemReadModel.cs
@@ -0,0 +1,11 @@
+namespace OrderTrackingApp.ReadPersistence.Models
+{
+ public class OrderItemReadModel
+ {
+ public Guid ProductId { get; set; }
+
+ public int Quantity { get; set; }
+
+ public decimal UnitPrice { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/OrderTrackingApp.ReadPersistence/Models/OrderReadModel.cs b/OrderTrackingApp.ReadPersistence/Models/OrderReadModel.cs
new file mode 100644
index 0000000..b5bfaa3
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/Models/OrderReadModel.cs
@@ -0,0 +1,17 @@
+using OrderTrackingApp.Domain.Entities;
+
+namespace OrderTrackingApp.ReadPersistence.Models
+{
+ public class OrderReadModel
+ {
+ public Guid Id { get; set; }
+
+ public Guid CustomerId { get; set; }
+
+ public OrderStatus Status { get; set; }
+
+ public DateTime CreatedAt { get; set; }
+
+ public List Items { get; set; } = [];
+ }
+}
\ No newline at end of file
diff --git a/OrderTrackingApp.ReadPersistence/MongoDbContext.cs b/OrderTrackingApp.ReadPersistence/MongoDbContext.cs
new file mode 100644
index 0000000..cf395fc
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/MongoDbContext.cs
@@ -0,0 +1,22 @@
+using Microsoft.Extensions.Configuration;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+using MongoDB.Driver;
+
+namespace OrderTrackingApp.ReadPersistence
+{
+ public class MongoDbContext
+ {
+ public IMongoDatabase Database { get; }
+
+ public MongoDbContext(IConfiguration config)
+ {
+ BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
+
+ var connectionString = config.GetConnectionString("MongoDb");
+ var mongoClient = new MongoClient(connectionString);
+ Database = mongoClient.GetDatabase("OrderReadDb");
+ }
+ }
+}
diff --git a/OrderTrackingApp.ReadPersistence/OrderTrackingApp.ReadPersistence.csproj b/OrderTrackingApp.ReadPersistence/OrderTrackingApp.ReadPersistence.csproj
new file mode 100644
index 0000000..112c0f0
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/OrderTrackingApp.ReadPersistence.csproj
@@ -0,0 +1,19 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OrderTrackingApp.ReadPersistence/Repositories/OrderReadRepository.cs b/OrderTrackingApp.ReadPersistence/Repositories/OrderReadRepository.cs
new file mode 100644
index 0000000..96c23ca
--- /dev/null
+++ b/OrderTrackingApp.ReadPersistence/Repositories/OrderReadRepository.cs
@@ -0,0 +1,72 @@
+using AutoMapper;
+using MongoDB.Driver;
+using OrderTrackingApp.Application.Contracts;
+using OrderTrackingApp.Application.Contracts.Orders;
+using OrderTrackingApp.Domain.Entities;
+using OrderTrackingApp.ReadPersistence.Models;
+
+namespace OrderTrackingApp.ReadPersistence.Repositories
+{
+ public class OrderReadRepository(MongoDbContext context, IMapper mapper) : IOrderReadRepository
+ {
+ private readonly IMongoCollection _collection = context.Database.GetCollection("Orders");
+
+ public async Task> GetAllAsync()
+ {
+ var orders = await _collection.Find(_ => true).ToListAsync();
+
+ return mapper.Map>(orders);
+ }
+
+ public async Task GetByIdAsync(Guid id)
+ {
+ var order = await _collection.Find(o => o.Id == id).FirstOrDefaultAsync();
+
+ return mapper.Map(order);
+ }
+
+ public async Task> GetPaginatedOrdersAsync(int page, int pageSize, string? status,
+ DateTime? fromDate, DateTime? toDate,
+ CancellationToken cancellationToken)
+ {
+ var filterBuilder = Builders.Filter;
+ var filters = new List>();
+
+ if (!string.IsNullOrWhiteSpace(status) && Enum.TryParse(status, out var parsedStatus))
+ {
+ filters.Add(filterBuilder.Eq(o => o.Status, parsedStatus));
+ }
+
+ if (fromDate.HasValue)
+ {
+ filters.Add(filterBuilder.Gte(o => o.CreatedAt, fromDate.Value));
+ }
+
+ if (toDate.HasValue)
+ {
+ filters.Add(filterBuilder.Lte(o => o.CreatedAt, toDate.Value));
+ }
+
+ var filter = filters.Any() ? filterBuilder.And(filters) : FilterDefinition.Empty;
+
+ var totalCount = await _collection.CountDocumentsAsync(filter, cancellationToken: cancellationToken);
+
+ var orders = await _collection.Find(filter)
+ .SortByDescending(o => o.CreatedAt)
+ .Skip((page - 1) * pageSize)
+ .Limit(pageSize)
+ .ToListAsync(cancellationToken);
+
+ var dtos = mapper.Map>(orders);
+
+ return new PaginatedResult(dtos, totalCount, page, pageSize);
+ }
+
+ public async Task InsertAsync(OrderDto orderDto)
+ {
+ var order = mapper.Map(orderDto);
+
+ await _collection.InsertOneAsync(order);
+ }
+ }
+}
diff --git a/OrderTrackingApp.sln b/OrderTrackingApp.sln
index 3c65601..1afc192 100644
--- a/OrderTrackingApp.sln
+++ b/OrderTrackingApp.sln
@@ -1,31 +1,40 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
-VisualStudioVersion = 17.14.36203.30 d17.14
+VisualStudioVersion = 17.14.36203.30
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Blazor.Server", "OrderTrackingApp\OrderTrackingApp.Blazor.Server.csproj", "{C0F58571-B42E-4301-BD65-F3E939EA37AA}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Blazor.Client", "OrderTrackingApp.Client\OrderTrackingApp.Blazor.Client.csproj", "{F4760EDE-BA06-4095-B2B5-6221E2EAC48F}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+ ProjectSection(SolutionItems) = preProject
+ .env = .env
+ docker-compose.yml = docker-compose.yml
+ README.md = README.md
+ EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{D901624D-8396-4393-B3AE-7D8405C89D69}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Api", "..\OrderTrackingApp.Api\OrderTrackingApp.Api.csproj", "{4957811C-F9FB-4259-9E1B-7F7275E75DDB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Api", "OrderTrackingApp.Api\OrderTrackingApp.Api.csproj", "{4957811C-F9FB-4259-9E1B-7F7275E75DDB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Application", "OrderTrackingApp.Application\OrderTrackingApp.Application.csproj", "{059A6F30-A0CC-4371-853A-50C76C01FEEE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Domain", "OrderTrackingApp.Domain\OrderTrackingApp.Domain.csproj", "{FE84DD55-5B8A-4629-85FC-5E403D1EFADB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Infrastructure", "OrderTrackingApp.Infrastructure\OrderTrackingApp.Infrastructure.csproj", "{F8E303DF-A47D-41C5-AF05-9673DAF4033A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Persistence", "OrderTrackingApp.Persistence\OrderTrackingApp.Persistence.csproj", "{E0B58B04-C47B-42D8-8AC5-0134B811855C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.SignalR", "..\OrderTrackingApp.SignalR\OrderTrackingApp.SignalR.csproj", "{8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.ReadPersistence", "OrderTrackingApp.ReadPersistence\OrderTrackingApp.ReadPersistence.csproj", "{AC2F58F3-C7DF-4908-83B9-98841B365CA9}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Application", "..\OrderTrackingApp.Application\OrderTrackingApp.Application.csproj", "{059A6F30-A0CC-4371-853A-50C76C01FEEE}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Consumer", "OrderTrackingApp.Consumer\OrderTrackingApp.Consumer.csproj", "{773AD28D-63B8-4A78-90D0-A5282F3BAFDD}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Domain", "..\OrderTrackingApp.Domain\OrderTrackingApp.Domain.csproj", "{FE84DD55-5B8A-4629-85FC-5E403D1EFADB}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Blazor.Server", "OrderTrackingApp.Blazor.Server\OrderTrackingApp.Blazor.Server.csproj", "{1FD0A91F-99A3-4D45-AA2B-3081183FEAC3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Infrastructure", "..\OrderTrackingApp.Infrastructure\OrderTrackingApp.Infrastructure.csproj", "{F8E303DF-A47D-41C5-AF05-9673DAF4033A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Blazor.Client", "OrderTrackingApp.Blazor.Client\OrderTrackingApp.Blazor.Client.csproj", "{43A85AAD-5188-4103-BEF6-F88971B638F2}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Persistence", "..\OrderTrackingApp.Persistence\OrderTrackingApp.Persistence.csproj", "{E0B58B04-C47B-42D8-8AC5-0134B811855C}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Persistence.Tests", "OrderTrackingApp.Persistence.Tests\OrderTrackingApp.Persistence.Tests.csproj", "{77A97D9B-3156-47EE-91A1-8D6F0D9BABE3}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Consumer", "..\OrderTrackingApp.Consumer\OrderTrackingApp.Consumer.csproj", "{3885E6E2-5813-4669-9295-051E8BEA9B0F}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.MigrationRunner", "OrderTrackingApp.MigrationRunner\OrderTrackingApp.MigrationRunner.csproj", "{C9DE7296-2132-490F-97DE-96FF65C7A5BB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.ReadPersistence", "..\OrderTrackingApp.ReadPersistence\OrderTrackingApp.ReadPersistence.csproj", "{AC2F58F3-C7DF-4908-83B9-98841B365CA9}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderTrackingApp.Application.Contracts", "OrderTrackingApp.Application.Contracts\OrderTrackingApp.Application.Contracts.csproj", "{E9BB177D-D125-4D7F-A599-126DADEDCDD1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -33,22 +42,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {C0F58571-B42E-4301-BD65-F3E939EA37AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C0F58571-B42E-4301-BD65-F3E939EA37AA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C0F58571-B42E-4301-BD65-F3E939EA37AA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C0F58571-B42E-4301-BD65-F3E939EA37AA}.Release|Any CPU.Build.0 = Release|Any CPU
- {F4760EDE-BA06-4095-B2B5-6221E2EAC48F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {F4760EDE-BA06-4095-B2B5-6221E2EAC48F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {F4760EDE-BA06-4095-B2B5-6221E2EAC48F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {F4760EDE-BA06-4095-B2B5-6221E2EAC48F}.Release|Any CPU.Build.0 = Release|Any CPU
{4957811C-F9FB-4259-9E1B-7F7275E75DDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4957811C-F9FB-4259-9E1B-7F7275E75DDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4957811C-F9FB-4259-9E1B-7F7275E75DDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4957811C-F9FB-4259-9E1B-7F7275E75DDB}.Release|Any CPU.Build.0 = Release|Any CPU
- {8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D}.Release|Any CPU.Build.0 = Release|Any CPU
{059A6F30-A0CC-4371-853A-50C76C01FEEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{059A6F30-A0CC-4371-853A-50C76C01FEEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{059A6F30-A0CC-4371-853A-50C76C01FEEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -65,29 +62,51 @@ Global
{E0B58B04-C47B-42D8-8AC5-0134B811855C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E0B58B04-C47B-42D8-8AC5-0134B811855C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E0B58B04-C47B-42D8-8AC5-0134B811855C}.Release|Any CPU.Build.0 = Release|Any CPU
- {3885E6E2-5813-4669-9295-051E8BEA9B0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3885E6E2-5813-4669-9295-051E8BEA9B0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3885E6E2-5813-4669-9295-051E8BEA9B0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3885E6E2-5813-4669-9295-051E8BEA9B0F}.Release|Any CPU.Build.0 = Release|Any CPU
{AC2F58F3-C7DF-4908-83B9-98841B365CA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AC2F58F3-C7DF-4908-83B9-98841B365CA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC2F58F3-C7DF-4908-83B9-98841B365CA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC2F58F3-C7DF-4908-83B9-98841B365CA9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {773AD28D-63B8-4A78-90D0-A5282F3BAFDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {773AD28D-63B8-4A78-90D0-A5282F3BAFDD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {773AD28D-63B8-4A78-90D0-A5282F3BAFDD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {773AD28D-63B8-4A78-90D0-A5282F3BAFDD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1FD0A91F-99A3-4D45-AA2B-3081183FEAC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1FD0A91F-99A3-4D45-AA2B-3081183FEAC3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1FD0A91F-99A3-4D45-AA2B-3081183FEAC3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1FD0A91F-99A3-4D45-AA2B-3081183FEAC3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {43A85AAD-5188-4103-BEF6-F88971B638F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {43A85AAD-5188-4103-BEF6-F88971B638F2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {43A85AAD-5188-4103-BEF6-F88971B638F2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {43A85AAD-5188-4103-BEF6-F88971B638F2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {77A97D9B-3156-47EE-91A1-8D6F0D9BABE3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {77A97D9B-3156-47EE-91A1-8D6F0D9BABE3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {77A97D9B-3156-47EE-91A1-8D6F0D9BABE3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {77A97D9B-3156-47EE-91A1-8D6F0D9BABE3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C9DE7296-2132-490F-97DE-96FF65C7A5BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C9DE7296-2132-490F-97DE-96FF65C7A5BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C9DE7296-2132-490F-97DE-96FF65C7A5BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C9DE7296-2132-490F-97DE-96FF65C7A5BB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E9BB177D-D125-4D7F-A599-126DADEDCDD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E9BB177D-D125-4D7F-A599-126DADEDCDD1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E9BB177D-D125-4D7F-A599-126DADEDCDD1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E9BB177D-D125-4D7F-A599-126DADEDCDD1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
- {C0F58571-B42E-4301-BD65-F3E939EA37AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {F4760EDE-BA06-4095-B2B5-6221E2EAC48F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{4957811C-F9FB-4259-9E1B-7F7275E75DDB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {8D2C3C59-0A56-4DFD-ABDD-D2C7521C351D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{059A6F30-A0CC-4371-853A-50C76C01FEEE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{FE84DD55-5B8A-4629-85FC-5E403D1EFADB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{F8E303DF-A47D-41C5-AF05-9673DAF4033A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{E0B58B04-C47B-42D8-8AC5-0134B811855C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
- {3885E6E2-5813-4669-9295-051E8BEA9B0F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{AC2F58F3-C7DF-4908-83B9-98841B365CA9} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {773AD28D-63B8-4A78-90D0-A5282F3BAFDD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {1FD0A91F-99A3-4D45-AA2B-3081183FEAC3} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {43A85AAD-5188-4103-BEF6-F88971B638F2} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {77A97D9B-3156-47EE-91A1-8D6F0D9BABE3} = {D901624D-8396-4393-B3AE-7D8405C89D69}
+ {C9DE7296-2132-490F-97DE-96FF65C7A5BB} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {E9BB177D-D125-4D7F-A599-126DADEDCDD1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {ECFA56EC-C322-4A64-8936-E930A2EEBE28}
diff --git a/OrderTrackingApp/OrderTrackingApp.Blazor.Server.csproj b/OrderTrackingApp/OrderTrackingApp.Blazor.Server.csproj
deleted file mode 100644
index 8eac3cb..0000000
--- a/OrderTrackingApp/OrderTrackingApp.Blazor.Server.csproj
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
- net8.0
- enable
- enable
-
-
-
-
-
-
-
-
diff --git a/README.md b/README.md
index 0a94371..c6669d0 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,112 @@
-# OrderTrackingApp
\ No newline at end of file
+[](https://github.com/ParagNaikade/OrderTrackingApp/actions/workflows/ci-pipeline.yml)
+
+# 🧾 Order Tracking System
+
+A distributed order processing system built with **.NET Clean Architecture**, featuring **CQRS**, **RabbitMQ messaging**, and dual persistence in **SQL Server** and **MongoDB**.
+
+This project demonstrates a scalable microservice pattern using domain events and message queues to decouple the system.
+
+---
+
+## 📐 Architecture Overview
+
+```
+API ──> Application ──> Infra ──> RabbitMQ ──> Consumer
+ │ │
+ SQL Server MongoDB
+```
+
+### 🔄 Flow Description
+
+1. **API**
+ - Exposes endpoints to create and retrieve orders.
+ - Triggers a `SaveOrderCommand` when a POST request is received.
+
+2. **Application**
+ - Implements **CQRS** to separate command and query logic.
+ - Handles business rules (e.g., inventory validation).
+ - Saves the order to **SQL Server**.
+ - Raises an `OrderCreatedEvent` as a domain event.
+
+3. **Infra**
+ - Listens for domain events.
+ - Publishes messages to **RabbitMQ** queue (`order.created`).
+
+4. **Consumer**
+ - Subscribes to the RabbitMQ queue.
+ - Reads order details from SQL.
+ - Syncs the order into **MongoDB** for read-side optimization or analytics.
+
+---
+
+## 🛠️ Tech Stack
+
+| Layer | Technology |
+|--------------|--------------------------------|
+| API | ASP.NET Core Web API |
+| Application | MediatR, CQRS, FluentValidation|
+| Infra | Entity Framework Core, RabbitMQ|
+| Consumer | Background Worker, MongoDB |
+| Messaging | RabbitMQ |
+| Databases | SQL Server, MongoDB |
+| Architecture | Clean Architecture |
+| DevOps | Docker, GitHub Actions (CI/CD) |
+
+---
+
+## 📂 Project Structure
+
+```
+OrderTrackingApp/
+├── API # Entry point
+├── Application # CQRS, Commands/Queries, COre app logic
+├── Application.Contracts # Interfaces, DTOs, Validation
+├── Domain # Entities, Value Objects, Events
+├── Infrastructure # Event Handlers, RabbitMQ
+├── Consumer # Background service that syncs to MongoDB
+├── ReadPersistence # MongoDB Read Models
+├── Persistence # EF Core
+└── docker-compose.yml # Services for RabbitMQ, SQL Server, MongoDB
+```
+
+---
+
+## 🚀 Getting Started
+
+### Prerequisites
+
+- [.NET 8 SDK](https://dotnet.microsoft.com/)
+- [Docker](https://www.docker.com/)
+- [MongoDB Compass](https://www.mongodb.com/products/compass) (optional for viewing MongoDB)
+- [RabbitMQ Management UI](http://localhost:15672) (user: `guest`, password: `guest`)
+
+### Run Locally
+
+```bash
+git clone https://github.com/ParagNaikade/order-tracking-app.git
+cd order-tracking-app
+docker-compose up --build
+```
+
+### Test API
+
+- `POST /api/orders` → Creates an order
+- `GET /api/orders/{id}` → Retrieves an order
+- `GET /api/orders` → Retrieves paginated list
+
+---
+
+## 🧪 Features
+
+- ✅ Clean Architecture (Separation of Concerns)
+- ✅ CQRS with MediatR
+- ✅ Domain Events and Messaging
+- ✅ Async Communication via RabbitMQ
+- ✅ SQL Write DB & MongoDB Read DB (eventual consistency)
+- ✅ Dockerized Microservices
+
+---
+
+## 📬 Contact
+
+Created with ❤️ by [Parag Naikade](https://paragnaikade.com/)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..fab4db5
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,42 @@
+services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2022-latest
+ container_name: sqlserver
+ ports:
+ - "1433:1433"
+ environment:
+ MSSQL_SA_PASSWORD: ${MSSQL_SA_PASSWORD}
+ ACCEPT_EULA: "Y"
+ volumes:
+ - sql_data:/var/opt/mssql
+
+ mongodb:
+ image: mongo
+ container_name: mongodb
+ ports:
+ - "27017:27017"
+ environment:
+ MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME}
+ MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
+ volumes:
+ - mongo_data:/data/db
+
+ rabbitmq:
+ image: rabbitmq:4-management
+ container_name: rabbitmq
+ ports:
+ - "5672:5672"
+ - "15672:15672"
+ environment:
+ RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER}
+ RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS}
+ volumes:
+ - rabbitmq_data:/var/lib/rabbitmq
+
+volumes:
+ sql_data:
+ name: sql_data
+ mongo_data:
+ name: mongo_data
+ rabbitmq_data:
+ name: rabbitmq_data
diff --git a/nscacert.crt b/nscacert.crt
new file mode 100644
index 0000000..8fd2269
--- /dev/null
+++ b/nscacert.crt
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID+jCCAuKgAwIBAgICATgwDQYJKoZIhvcNAQELBQAwgZYxCzAJBgNVBAYTAkFV
+MQwwCgYDVQQIEwNWSUMxEjAQBgNVBAcTCU1lbGJvdXJuZTEWMBQGA1UEChMNTmV0
+c2tvcGUgSW5jLjESMBAGA1UECxMJY2VydGFkbWluMRIwEAYDVQQDEwljZXJ0YWRt
+aW4xJTAjBgkqhkiG9w0BCQEWFmNlcnRhZG1pbkBuZXRza29wZS5jb20wHhcNMjEw
+MzIxMDUzODMyWhcNMzEwMzE5MDUzODMyWjCBljELMAkGA1UEBhMCQVUxDDAKBgNV
+BAgTA1ZJQzESMBAGA1UEBxMJTWVsYm91cm5lMRYwFAYDVQQKEw1OZXRza29wZSBJ
+bmMuMRIwEAYDVQQLEwljZXJ0YWRtaW4xEjAQBgNVBAMTCWNlcnRhZG1pbjElMCMG
+CSqGSIb3DQEJARYWY2VydGFkbWluQG5ldHNrb3BlLmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBAK5aO8+dAaPOlPmc+YtaYpnxByrOtx8HDapDM8WD
+7/8Y6obWjn0+uP6A0P3rtlECif0NHpKMIY10Kd+WImrMKeEDBkjK4IN4fHff2ji9
+dVPpvqBakGxpFdSDikLHkxswIJBwXPiAfvPPDYNt5tnWRp+C23u3Wj2ocLqAdyuM
++GGUc8lO5yCpDZXsyYZT6ycgAfzRf2IVYCS88l0hqMN45Z/9w/EixAFMamNho61x
+x6YQhOzTugSdEr3sFm2hs+ijKZCorGe6eQIlSm4abAQXFLGOFPmRM+Q6MAx6nuV7
+d8+PIyDf+SHBxlAbQkeBucznByLG+HcZAlK6nnuWRgT97B0CAwEAAaNQME4wDAYD
+VR0TBAUwAwEB/zAdBgNVHQ4EFgQUN2YGoXPDWG0auDkFXzQEwfPdSrYwHwYDVR0j
+BBgwFoAUN2YGoXPDWG0auDkFXzQEwfPdSrYwDQYJKoZIhvcNAQELBQADggEBAC3D
+qtNM0uZYsNeBwLCU2MEV9gV+BYBPwJvsXgIBiP2wH8VQ/QXbFw2HW3adVaEb0JgX
+Qn3SUaw2sd2SeCw6KyRoXQg+Y06wEl3iTenzbpClp7sxVFB6II9o5aQPNXiAwcM1
+lRUSWOU8kUMYFbq2ndUonJX8eB4QiOzA7JppYf4dNgBXETxBngG1uFlZgGEN6Ars
+o6iTHfZ+CTptlkuSjv2sWbK0pkIdsMHf2bScw3lu1oR3hwZCSdxI6Q5tXK4XsLLe
+hAM5vIcMC434S3RV6iN9VW2wWYrQ9DPnHxw7x7fwUAcTaxIhOEJF7AhuCtg5mjlP
+qwf9GAyaVt+8ilbHYdw=
+-----END CERTIFICATE-----