Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
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
Expand Down
22 changes: 22 additions & 0 deletions OrderTrackingApp.Api/Controllers/OrdersController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using AutoMapper;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using OrderTrackingApp.Api.Models;
using OrderTrackingApp.Application.Commands.Orders;

namespace OrderTrackingApp.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrdersController(IMediator mediator, IMapper mapper) : ControllerBase
{
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
var command = mapper.Map<CreateOrderCommand>(request);
var orderId = await mediator.Send(command);

return CreatedAtAction(nameof(CreateOrder), new { id = orderId }, new { OrderId = orderId });
}
}
}
33 changes: 0 additions & 33 deletions OrderTrackingApp.Api/Controllers/WeatherForecastController.cs

This file was deleted.

27 changes: 27 additions & 0 deletions OrderTrackingApp.Api/Extensions/ApiServiceRegistrations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using FluentValidation;
using OrderTrackingApp.Api.MappingProfiles;
using OrderTrackingApp.Api.Validators;
using System.Text.Json;

namespace OrderTrackingApp.Api.Extensions
{
public static class ApiServiceRegistrations
{
public static IServiceCollection AddApiServices(this IServiceCollection services)
{
services.AddEndpointsApiExplorer();
services.AddSwaggerGen();

services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});

services.AddValidatorsFromAssemblyContaining<CreateOrderRequestValidator>();
services.AddAutoMapper(config => { }, typeof(OrderMappingProfile).Assembly);

return services;
}
}
}
15 changes: 15 additions & 0 deletions OrderTrackingApp.Api/MappingProfiles/OrderMappingProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using AutoMapper;
using OrderTrackingApp.Api.Models;
using OrderTrackingApp.Application.Commands.Orders;

namespace OrderTrackingApp.Api.MappingProfiles
{
public class OrderMappingProfile : Profile
{
public OrderMappingProfile()
{
CreateMap<CreateOrderRequest, CreateOrderCommand>();
CreateMap<CreateOrderItemRequest, CreateOrderItemDto>();
}
}
}
22 changes: 22 additions & 0 deletions OrderTrackingApp.Api/Models/CreateOrderRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;

namespace OrderTrackingApp.Api.Models
{
public class CreateOrderRequest
{
[Required]
public Guid CustomerId { get; set; }

[Required]
public List<CreateOrderItemRequest> Items { get; set; } = [];
}

public class CreateOrderItemRequest
{
[Required]
public Guid ProductId { get; set; }

[Range(1, int.MaxValue)]
public int Quantity { get; set; }
}
}
5 changes: 5 additions & 0 deletions OrderTrackingApp.Api/OrderTrackingApp.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper" Version="15.0.1" />
<PackageReference Include="FluentValidation" Version="12.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
<PackageReference Include="Scalar.AspNetCore" Version="2.6.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OrderTrackingApp.Infrastructure\OrderTrackingApp.Infrastructure.csproj" />
<ProjectReference Include="..\OrderTrackingApp.Persistence\OrderTrackingApp.Persistence.csproj" />
<ProjectReference Include="..\OrderTrackingApp.ReadPersistence\OrderTrackingApp.ReadPersistence.csproj" />
</ItemGroup>
Expand Down
24 changes: 13 additions & 11 deletions OrderTrackingApp.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
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);

// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddPersistenceServices(builder.Configuration);
builder.Services.AddReadPersistence(builder.Configuration);
builder.Services.AddReadPersistence();
builder.Services.AddInfrastructureServices();
builder.Services.AddApplicationServices();
builder.Services.AddApiServices();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
app.UseSwagger(options =>
{
options.RouteTemplate = "/openapi/{documentName}.json";
});
app.MapScalarApiReference();
}

app.UseHttpsRedirection();
Expand Down
5 changes: 3 additions & 2 deletions OrderTrackingApp.Api/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"launchBrowser": false,
"launchUrl": "scalar",
"applicationUrl": "http://localhost:5077",
"hotReloadEnabled": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
Expand Down
20 changes: 20 additions & 0 deletions OrderTrackingApp.Api/Validators/CreateOrderRequestValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using FluentValidation;
using OrderTrackingApp.Api.Models;

namespace OrderTrackingApp.Api.Validators
{
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
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);
});
}
}
}
6 changes: 3 additions & 3 deletions OrderTrackingApp.Api/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
},
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "",
"MongoDb": ""
"SqlConnection": "Server=localhost,1433;Database=OrderDb;User=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True",
"MongoDb": "mongodb://mongoadmin:secret123@localhost:27017",
"RabbitMq": "amqp://guest:guest@localhost:5672/"
}

}
14 changes: 11 additions & 3 deletions OrderTrackingApp.Application/Commands/Orders/CreateOrderCommand.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
using MediatR;
using OrderTrackingApp.Application.DTOs;

namespace OrderTrackingApp.Application.Commands.Orders
{
public class CreateOrderCommand : IRequest<OrderDto>
public class CreateOrderCommand : IRequest<Guid>
{
public string CustomerName { get; set; } = string.Empty;
public Guid CustomerId { get; set; }

public List<CreateOrderItemDto> Items { get; set; } = [];
}

public class CreateOrderItemDto
{
public Guid ProductId { get; set; }

public int Quantity { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,59 @@
using MediatR;
using OrderTrackingApp.Application.DTOs;
using OrderTrackingApp.Application.Interfaces;
using OrderTrackingApp.Domain.Entities;
using OrderTrackingApp.Domain.Events;
using OrderTrackingApp.Domain.Interfaces;

namespace OrderTrackingApp.Application.Commands.Orders
{
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, OrderDto>
public class CreateOrderCommandHandler(IOrderWriteRepository orderRepository, IProductRepository productRepository, IMediator mediator)
: IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderWriteRepository _orderRepository;

public CreateOrderCommandHandler(IOrderWriteRepository orderRepository)
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
_orderRepository = orderRepository;
}
var orderItems = new List<OrderItem>();
var updatedProducts = new List<Product>();

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 OrderItem { ProductId = item.ProductId, Quantity = item.Quantity, UnitPrice = product.Price });

product.StockQuantity -= item.Quantity;

updatedProducts.Add(product);
}

await productRepository.UpdateProducts(updatedProducts);

public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order
{
CustomerName = request.CustomerName,
Id = Guid.NewGuid(),
OrderNumber = $"ORD-{DateTime.UtcNow.Ticks}",
CustomerId = request.CustomerId,
OrderDate = DateTime.UtcNow,
Status = OrderStatus.Pending,
Items = orderItems
};

await _orderRepository.AddAsync(order);
await orderRepository.AddAsync(order);

return new OrderDto
{
Id = order.Id,
CustomerName = order.CustomerName,
Status = order.Status,
CreatedAt = order.CreatedAt
};
await mediator.Publish(new OrderCreatedEvent(order.Id), cancellationToken);

return order.Id;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Microsoft.Extensions.DependencyInjection;
using OrderTrackingApp.Application.Commands.Orders;

namespace OrderTrackingApp.Application.Extensions
{
public static class ApplicationServiceRegistrations
{
public static IServiceCollection AddApplicationServices(this IServiceCollection services)
{
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommandHandler).Assembly));
return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,7 @@ public interface IOrderWriteRepository
Task UpdateAsync(Order order);

Task DeleteAsync(Guid id);

Task<Order?> GetByIdAsync(Guid id);
}
}
1 change: 0 additions & 1 deletion OrderTrackingApp.Blazor.Server/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using OrderTrackingApp.Blazor.Client.Pages;
using OrderTrackingApp.Blazor.Server.Components;

var builder = WebApplication.CreateBuilder(args);
Expand Down
6 changes: 6 additions & 0 deletions OrderTrackingApp.Consumer/OrderTrackingApp.Consumer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="7.1.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\OrderTrackingApp.Persistence\OrderTrackingApp.Persistence.csproj" />
<ProjectReference Include="..\OrderTrackingApp.ReadPersistence\OrderTrackingApp.ReadPersistence.csproj" />
</ItemGroup>
</Project>
5 changes: 5 additions & 0 deletions OrderTrackingApp.Consumer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using OrderTrackingApp.Consumer;
using OrderTrackingApp.Persistence.Extensions;
using OrderTrackingApp.ReadPersistence.Extensions;

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();

builder.Services.AddPersistenceServices(builder.Configuration);
builder.Services.AddReadPersistence();

var host = builder.Build();
host.Run();
1 change: 1 addition & 0 deletions OrderTrackingApp.Consumer/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"OrderTrackingApp.Consumer": {
"commandName": "Project",
"dotnetRunMessages": true,
"hotReloadEnabled": false,
"environmentVariables": {
"DOTNET_ENVIRONMENT": "Development"
}
Expand Down
Loading