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
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

This project demonstrates the development of the FakeStore API (https://github.com/keikaavousi/fake-store-api) based on Domain-Driven Design (DDD) documentation in ASP.NET Core, using AI to drive the implementation.

## AI Models Comparison

Comparison images are located in `docs/ai-models` folder:

### [Documentation Index](docs/ai-models//README.md)

- `docs/ai

## Documentation

The complete documentation is available in the `docs/` folder.
Expand All @@ -16,9 +24,6 @@ The complete documentation is available in the `docs/` folder.
- `docs/infrastructure`
- `docs/roadmap`

## AI Models Comparison

Comparison images are located in `docs/ai-models` folder:
### [Documentation Index](docs/ai-models//README.md)


File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes
29 changes: 29 additions & 0 deletions docs/ai/prompt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Generic Prompt

> You are a Senior .NET developer with extensive expertise in C#, ASP.NET Core, and EF Core.

> In this API project, you must use the following key NuGet packages:
> - MediatR for the command/query pipeline
> - AutoMapper for DTO mapping
> - OneOf for typed results
> - FluentValidation for command validations
> - Serilog for structured logging
> - Swagger (Swashbuckle) for documentation
> - HealthChecks for readiness probes

> Every class, method, and property must include XML comments in accordance with Microsoft’s guidelines.

> In controllers and endpoints, include these API documentation attributes:
> - [ProducesResponseType]
> - [SwaggerOperation] with a detailed description
> - Example: [SwaggerResponseExample]

> Use DataAnnotations to validate request models.

> Write unit and integration tests using xUnit, Shouldly, NSubstitute, and Bogus Faker.

> In your tests, follow the Given-When-Then pattern and assign a meaningful DisplayName to each Fact/Theory.

> Review the source code in `/src/` and the tests in `/tests/`. Also consult the design documents under `/docs/` (context-map, tactical/modeling, infrastructure/api).

> Then implement the requirements specified in `work/00-file-name.md`, covering its Description, Acceptance Criteria, Technical Refinement, Test Cases (Gherkin), and Definition of Done.
3 changes: 2 additions & 1 deletion src/FakeStoreNet.Api/FakeStoreNet.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="8.1.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FakeStoreNet.Domain\FakeStoreNet.Domain.csproj" />
<ProjectReference Include="..\FakeStoreNet.Infrastructure\FakeStoreNet.Infrastructure.csproj" />
<ProjectReference Include="..\FakeStoreNet.Application\FakeStoreNet.Application.csproj" />
</ItemGroup>

</Project>
6 changes: 6 additions & 0 deletions src/FakeStoreNet.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@

using FakeStoreNet.Application.Common;
using FakeStoreNet.Infrastructure.Caching;

namespace FakeStoreNet.Api
{
public class Program
Expand All @@ -12,6 +15,9 @@ public static void Main(string[] args)
builder.Services.AddSingleton<FakeStoreNet.Domain.Common.IEventLogRepository, FakeStoreNet.Infrastructure.EventDispatching.InMemoryEventLogRepository>();

builder.Services.AddControllers();
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
builder.Services.Configure<CacheSettings>(builder.Configuration.GetSection("CacheSettings"));

var app = builder.Build();

Expand Down
13 changes: 13 additions & 0 deletions src/FakeStoreNet.Application/Common/CacheSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace FakeStoreNet.Application.Common
{
/// <summary>
/// Settings for cache expiration policies.
/// </summary>
public class CacheSettings
{
/// <summary>
/// Absolute expiration in seconds for GetAllProductsQuery cache.
/// </summary>
public int GetAllProductsAbsoluteExpirationInSeconds { get; set; } = 60;
}
}
32 changes: 32 additions & 0 deletions src/FakeStoreNet.Application/Common/ICacheService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace FakeStoreNet.Application.Common
{
/// <summary>
/// Defines methods for interacting with a cache store.
/// </summary>
public interface ICacheService
{
/// <summary>
/// Retrieves an entry from cache.
/// </summary>
/// <typeparam name="T">Type of cached value.</typeparam>
/// <param name="key">Cache key.</param>
/// <returns>Cached value or default if not found.</returns>
Task<T?> GetAsync<T>(string key);

/// <summary>
/// Stores an entry in cache.
/// </summary>
/// <typeparam name="T">Type of value to cache.</typeparam>
/// <param name="key">Cache key.</param>
/// <param name="value">Value to cache.</param>
/// <param name="absoluteExpiration">Absolute expiration timespan (optional).</param>
/// <param name="slidingExpiration">Sliding expiration timespan (optional).</param>
Task SetAsync<T>(string key, T value, TimeSpan? absoluteExpiration = null, TimeSpan? slidingExpiration = null);

/// <summary>
/// Removes an entry from cache.
/// </summary>
/// <param name="key">Cache key.</param>
Task RemoveAsync(string key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\FakeStoreNet.Domain\FakeStoreNet.Domain.csproj" />
<ProjectReference Include="..\FakeStoreNet.Infrastructure\FakeStoreNet.Infrastructure.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using FakeStoreNet.Domain.Exceptions;
using MediatR;
using OneOf;
using FakeStoreNet.Domain.Common;

namespace FakeStoreNet.Application.Features.Product.Commands.CreateProduct
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using OneOf;
using FakeStoreNet.Application.Common;
using FakeStoreNet.Domain.Common;
using FakeStoreNet.Domain.Entities;
using FakeStoreNet.Domain.Exceptions;
using FakeStoreNet.Domain.ValueObjects;
using FakeStoreNet.Application.Features.Product.Commands.CreateProduct;
using MediatR;
using OneOf;

namespace FakeStoreNet.Application.Features.Product.Commands.CreateProduct
{
/// <summary>
/// Handles <see cref="CreateProductCommand"/> to create a new product.
/// </summary>
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, OneOf<int, DomainValidationException>>
/// <remarks>
/// Initializes a new instance of <see cref="CreateProductCommandHandler"/>.
/// </remarks>
/// <param name="repository">Product repository.</param>
public class CreateProductCommandHandler(IProductRepository repository, ICacheService cacheService) : IRequestHandler<CreateProductCommand, OneOf<int, DomainValidationException>>
{
private readonly IProductRepository _repository;
private readonly IMapper _mapper;

/// <summary>
/// Initializes a new instance of <see cref="CreateProductCommandHandler"/>.
/// </summary>
/// <param name="repository">Product repository.</param>
/// <param name="mapper">AutoMapper mapper.</param>
public CreateProductCommandHandler(IProductRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}

/// <inheritdoc/>
public Task<OneOf<int, DomainValidationException>> Handle(CreateProductCommand request, CancellationToken cancellationToken)
public async Task<OneOf<int, DomainValidationException>> Handle(CreateProductCommand request, CancellationToken cancellationToken)
{
try
{
Expand All @@ -42,13 +30,14 @@ public Task<OneOf<int, DomainValidationException>> Handle(CreateProductCommand r
request.Image,
new Rating(request.Rate, request.Count));

_repository.Add(product);
repository.Add(product);
await cacheService.RemoveAsync("GetAllProducts");

return Task.FromResult<OneOf<int, DomainValidationException>>(product.Id);
return product.Id;
}
catch (DomainValidationException ex)
{
return Task.FromResult<OneOf<int, DomainValidationException>>(ex);
return ex;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using FakeStoreNet.Domain.Exceptions;
using MediatR;
using OneOf;
using FakeStoreNet.Domain.Common;

namespace FakeStoreNet.Application.Features.Product.Commands.DeleteProduct
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,39 +1,34 @@
using System.Threading;
using System.Threading.Tasks;
using FakeStoreNet.Application.Common;
using FakeStoreNet.Domain.Common;
using FakeStoreNet.Domain.Exceptions;
using MediatR;
using OneOf;
using FakeStoreNet.Domain.Common;

namespace FakeStoreNet.Application.Features.Product.Commands.DeleteProduct
{
/// <summary>
/// Handles <see cref="DeleteProductCommand"/> to delete an existing product.
/// </summary>
public class DeleteProductCommandHandler : IRequestHandler<DeleteProductCommand, OneOf<Unit, DomainValidationException>>
/// <remarks>
/// Initializes a new instance of <see cref="DeleteProductCommandHandler"/>.
/// </remarks>
/// <param name="repository">Product repository.</param>
public class DeleteProductCommandHandler(IProductRepository repository, ICacheService cacheService) : IRequestHandler<DeleteProductCommand, OneOf<Unit, DomainValidationException>>
{
private readonly IProductRepository _repository;

/// <summary>
/// Initializes a new instance of <see cref="DeleteProductCommandHandler"/>.
/// </summary>
/// <param name="repository">Product repository.</param>
public DeleteProductCommandHandler(IProductRepository repository)
{
_repository = repository;
}

/// <inheritdoc/>
public Task<OneOf<Unit, DomainValidationException>> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
public async Task<OneOf<Unit, DomainValidationException>> Handle(DeleteProductCommand request, CancellationToken cancellationToken)
{
try
{
var product = _repository.GetById(request.Id);
_repository.Delete(product);
return Task.FromResult<OneOf<Unit, DomainValidationException>>(Unit.Value);
var product = repository.GetById(request.Id);
repository.Delete(product);
await cacheService.RemoveAsync("GetAllProducts");
return Unit.Value;
}
catch (DomainValidationException ex)
{
return Task.FromResult<OneOf<Unit, DomainValidationException>>(ex);
return ex;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using FakeStoreNet.Domain.Exceptions;
using MediatR;
using OneOf;
using FakeStoreNet.Domain.Common;

namespace FakeStoreNet.Application.Features.Product.Commands.UpdateProduct
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,52 +1,45 @@
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MediatR;
using OneOf;
using FakeStoreNet.Application.Common;
using FakeStoreNet.Domain.Common;
using FakeStoreNet.Domain.Exceptions;
using FakeStoreNet.Domain.ValueObjects;
using FakeStoreNet.Application.Features.Product.Commands.UpdateProduct;
using MediatR;
using OneOf;

namespace FakeStoreNet.Application.Features.Product.Commands.UpdateProduct
{
/// <summary>
/// Handles <see cref="UpdateProductCommand"/> to update an existing product.
/// </summary>
public class UpdateProductCommandHandler : IRequestHandler<UpdateProductCommand, OneOf<Unit, DomainValidationException>>
/// <remarks>
/// Initializes a new instance of <see cref="UpdateProductCommandHandler"/>.
/// </remarks>
/// <param name="repository">Product repository.</param>
/// <param name="cacheService">Cache service.</param>
public class UpdateProductCommandHandler(IProductRepository repository, ICacheService cacheService) : IRequestHandler<UpdateProductCommand, OneOf<Unit, DomainValidationException>>
{
private readonly IProductRepository _repository;

/// <summary>
/// Initializes a new instance of <see cref="UpdateProductCommandHandler"/>.
/// </summary>
/// <param name="repository">Product repository.</param>
public UpdateProductCommandHandler(IProductRepository repository)
{
_repository = repository;
}

/// <inheritdoc/>
public Task<OneOf<Unit, DomainValidationException>> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
public async Task<OneOf<Unit, DomainValidationException>> Handle(UpdateProductCommand request, CancellationToken cancellationToken)
{
try
{
if (string.IsNullOrWhiteSpace(request.Title))
throw new DomainValidationException("Title is required");
var product = _repository.GetById(request.Id);
var product = repository.GetById(request.Id);
product.UpdateDetails(
request.Title,
new Money(request.Price, product.Price.Currency),
new Money(request.Price, product.Price.Currency),
request.Description,
request.Category,
request.Image,
new Rating(request.Rate, request.Count)
);
_repository.Update(product);
return Task.FromResult<OneOf<Unit, DomainValidationException>>(Unit.Value);
repository.Update(product);
await cacheService.RemoveAsync("GetAllProducts");
return Unit.Value;
}
catch (DomainValidationException ex)
{
return Task.FromResult<OneOf<Unit, DomainValidationException>>(ex);
return ex;
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Collections.Generic;
using MediatR;

namespace FakeStoreNet.Application.Features.Product.Queries.GetAllProducts
Expand Down
Loading
Loading