feat: Remove Sample aggregate, refactor database context to FnbContext, and introduce Table aggregate with its configurations, alongside adding an inventory repository interface.

This commit is contained in:
Ho Ngoc Hai
2026-01-18 01:03:36 +07:00
parent 04933be6dd
commit dc55dda87a
28 changed files with 177 additions and 1149 deletions

View File

@@ -1,14 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Command to change status of a Sample.
/// VI: Command để thay đổi trạng thái của Sample.
/// </summary>
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
/// <param name="NewStatus">EN: New status (activate, complete, cancel) / VI: Trạng thái mới (activate, complete, cancel)</param>
public record ChangeSampleStatusCommand(
Guid SampleId,
string NewStatus
) : IRequest<bool>;

View File

@@ -1,70 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Handler for ChangeSampleStatusCommand.
/// VI: Handler cho ChangeSampleStatusCommand.
/// </summary>
public class ChangeSampleStatusCommandHandler : IRequestHandler<ChangeSampleStatusCommand, bool>
{
private readonly ISampleRepository _sampleRepository;
private readonly ILogger<ChangeSampleStatusCommandHandler> _logger;
public ChangeSampleStatusCommandHandler(
ISampleRepository sampleRepository,
ILogger<ChangeSampleStatusCommandHandler> logger)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(
ChangeSampleStatusCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Changing status of sample {SampleId} to {NewStatus} / Thay đổi trạng thái sample {SampleId} thành {NewStatus}",
request.SampleId, request.NewStatus);
// EN: Get existing sample / VI: Lấy sample đã tồn tại
var sample = await _sampleRepository.GetAsync(request.SampleId);
if (sample is null)
{
_logger.LogWarning(
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
request.SampleId);
return false;
}
// EN: Change status based on action / VI: Thay đổi trạng thái dựa trên action
switch (request.NewStatus.ToLowerInvariant())
{
case "activate":
sample.Activate();
break;
case "complete":
sample.Complete();
break;
case "cancel":
sample.Cancel();
break;
default:
_logger.LogWarning(
"Invalid status action: {NewStatus} / Action trạng thái không hợp lệ: {NewStatus}",
request.NewStatus);
return false;
}
// EN: Save changes / VI: Lưu thay đổi
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"Sample {SampleId} status changed to {NewStatus} / Trạng thái sample {SampleId} đã đổi thành {NewStatus}",
request.SampleId, request.NewStatus);
return true;
}
}

View File

@@ -1,21 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Command to create a new Sample.
/// VI: Command để tạo một Sample mới.
/// </summary>
/// <param name="Name">EN: Sample name / VI: Tên sample</param>
/// <param name="Description">EN: Optional description / VI: Mô tả tùy chọn</param>
public record CreateSampleCommand(
string Name,
string? Description
) : IRequest<CreateSampleCommandResult>;
/// <summary>
/// EN: Result of CreateSampleCommand.
/// VI: Kết quả của CreateSampleCommand.
/// </summary>
/// <param name="Id">EN: Created sample ID / VI: ID sample đã tạo</param>
public record CreateSampleCommandResult(Guid Id);

View File

@@ -1,46 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Handler for CreateSampleCommand.
/// VI: Handler cho CreateSampleCommand.
/// </summary>
public class CreateSampleCommandHandler : IRequestHandler<CreateSampleCommand, CreateSampleCommandResult>
{
private readonly ISampleRepository _sampleRepository;
private readonly ILogger<CreateSampleCommandHandler> _logger;
public CreateSampleCommandHandler(
ISampleRepository sampleRepository,
ILogger<CreateSampleCommandHandler> logger)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<CreateSampleCommandResult> Handle(
CreateSampleCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Creating new sample with name: {Name} / Tạo sample mới với tên: {Name}",
request.Name);
// EN: Create domain entity / VI: Tạo domain entity
var sample = new Sample(request.Name, request.Description);
// EN: Add to repository / VI: Thêm vào repository
_sampleRepository.Add(sample);
// EN: Save changes (dispatches domain events) / VI: Lưu thay đổi (dispatch domain events)
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"Sample created successfully with ID: {SampleId} / Sample đã tạo thành công với ID: {SampleId}",
sample.Id);
return new CreateSampleCommandResult(sample.Id);
}
}

View File

@@ -1,10 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Command to delete a Sample.
/// VI: Command để xóa một Sample.
/// </summary>
/// <param name="SampleId">EN: Sample ID to delete / VI: ID sample cần xóa</param>
public record DeleteSampleCommand(Guid SampleId) : IRequest<bool>;

View File

@@ -1,54 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Handler for DeleteSampleCommand.
/// VI: Handler cho DeleteSampleCommand.
/// </summary>
public class DeleteSampleCommandHandler : IRequestHandler<DeleteSampleCommand, bool>
{
private readonly ISampleRepository _sampleRepository;
private readonly ILogger<DeleteSampleCommandHandler> _logger;
public DeleteSampleCommandHandler(
ISampleRepository sampleRepository,
ILogger<DeleteSampleCommandHandler> logger)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(
DeleteSampleCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Deleting sample {SampleId} / Xóa sample {SampleId}",
request.SampleId);
// EN: Get existing sample / VI: Lấy sample đã tồn tại
var sample = await _sampleRepository.GetAsync(request.SampleId);
if (sample is null)
{
_logger.LogWarning(
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
request.SampleId);
return false;
}
// EN: Delete sample / VI: Xóa sample
_sampleRepository.Delete(sample);
// EN: Save changes / VI: Lưu thay đổi
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"Sample {SampleId} deleted successfully / Sample {SampleId} đã xóa thành công",
request.SampleId);
return true;
}
}

View File

@@ -1,16 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Command to update an existing Sample.
/// VI: Command để cập nhật một Sample đã tồn tại.
/// </summary>
/// <param name="SampleId">EN: Sample ID to update / VI: ID sample cần cập nhật</param>
/// <param name="Name">EN: New name / VI: Tên mới</param>
/// <param name="Description">EN: New description / VI: Mô tả mới</param>
public record UpdateSampleCommand(
Guid SampleId,
string Name,
string? Description
) : IRequest<bool>;

View File

@@ -1,54 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Commands;
/// <summary>
/// EN: Handler for UpdateSampleCommand.
/// VI: Handler cho UpdateSampleCommand.
/// </summary>
public class UpdateSampleCommandHandler : IRequestHandler<UpdateSampleCommand, bool>
{
private readonly ISampleRepository _sampleRepository;
private readonly ILogger<UpdateSampleCommandHandler> _logger;
public UpdateSampleCommandHandler(
ISampleRepository sampleRepository,
ILogger<UpdateSampleCommandHandler> logger)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<bool> Handle(
UpdateSampleCommand request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"Updating sample {SampleId} / Cập nhật sample {SampleId}",
request.SampleId);
// EN: Get existing sample / VI: Lấy sample đã tồn tại
var sample = await _sampleRepository.GetAsync(request.SampleId);
if (sample is null)
{
_logger.LogWarning(
"Sample {SampleId} not found / Sample {SampleId} không tìm thấy",
request.SampleId);
return false;
}
// EN: Update sample using domain method / VI: Cập nhật sample sử dụng domain method
sample.Update(request.Name, request.Description);
// EN: Save changes / VI: Lưu thay đổi
await _sampleRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
_logger.LogInformation(
"Sample {SampleId} updated successfully / Sample {SampleId} đã cập nhật thành công",
request.SampleId);
return true;
}
}

View File

@@ -1,23 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Query to get a Sample by ID.
/// VI: Query để lấy một Sample theo ID.
/// </summary>
/// <param name="SampleId">EN: Sample ID / VI: ID sample</param>
public record GetSampleQuery(Guid SampleId) : IRequest<SampleViewModel?>;
/// <summary>
/// EN: Sample view model for API responses.
/// VI: Sample view model cho API responses.
/// </summary>
public record SampleViewModel(
Guid Id,
string Name,
string? Description,
string Status,
DateTime CreatedAt,
DateTime? UpdatedAt
);

View File

@@ -1,39 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Handler for GetSampleQuery.
/// VI: Handler cho GetSampleQuery.
/// </summary>
public class GetSampleQueryHandler : IRequestHandler<GetSampleQuery, SampleViewModel?>
{
private readonly ISampleRepository _sampleRepository;
public GetSampleQueryHandler(ISampleRepository sampleRepository)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
}
public async Task<SampleViewModel?> Handle(
GetSampleQuery request,
CancellationToken cancellationToken)
{
var sample = await _sampleRepository.GetAsync(request.SampleId);
if (sample is null)
{
return null;
}
return new SampleViewModel(
sample.Id,
sample.Name,
sample.Description,
sample.Status.Name,
sample.CreatedAt,
sample.UpdatedAt
);
}
}

View File

@@ -1,9 +0,0 @@
using MediatR;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Query to get all Samples.
/// VI: Query để lấy tất cả Samples.
/// </summary>
public record GetSamplesQuery : IRequest<IEnumerable<SampleViewModel>>;

View File

@@ -1,34 +0,0 @@
using MediatR;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.API.Application.Queries;
/// <summary>
/// EN: Handler for GetSamplesQuery.
/// VI: Handler cho GetSamplesQuery.
/// </summary>
public class GetSamplesQueryHandler : IRequestHandler<GetSamplesQuery, IEnumerable<SampleViewModel>>
{
private readonly ISampleRepository _sampleRepository;
public GetSamplesQueryHandler(ISampleRepository sampleRepository)
{
_sampleRepository = sampleRepository ?? throw new ArgumentNullException(nameof(sampleRepository));
}
public async Task<IEnumerable<SampleViewModel>> Handle(
GetSamplesQuery request,
CancellationToken cancellationToken)
{
var samples = await _sampleRepository.GetAllAsync();
return samples.Select(sample => new SampleViewModel(
sample.Id,
sample.Name,
sample.Description,
sample.Status.Name,
sample.CreatedAt,
sample.UpdatedAt
));
}
}

View File

@@ -1,25 +0,0 @@
using FluentValidation;
using FnbEngine.API.Application.Commands;
namespace FnbEngine.API.Application.Validations;
/// <summary>
/// EN: Validator for CreateSampleCommand.
/// VI: Validator cho CreateSampleCommand.
/// </summary>
public class CreateSampleCommandValidator : AbstractValidator<CreateSampleCommand>
{
public CreateSampleCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required / Tên là bắt buộc")
.MaximumLength(200)
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
RuleFor(x => x.Description)
.MaximumLength(1000)
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
.When(x => x.Description != null);
}
}

View File

@@ -1,29 +0,0 @@
using FluentValidation;
using FnbEngine.API.Application.Commands;
namespace FnbEngine.API.Application.Validations;
/// <summary>
/// EN: Validator for UpdateSampleCommand.
/// VI: Validator cho UpdateSampleCommand.
/// </summary>
public class UpdateSampleCommandValidator : AbstractValidator<UpdateSampleCommand>
{
public UpdateSampleCommandValidator()
{
RuleFor(x => x.SampleId)
.NotEmpty()
.WithMessage("Sample ID is required / ID sample là bắt buộc");
RuleFor(x => x.Name)
.NotEmpty()
.WithMessage("Name is required / Tên là bắt buộc")
.MaximumLength(200)
.WithMessage("Name must be less than 200 characters / Tên phải ít hơn 200 ký tự");
RuleFor(x => x.Description)
.MaximumLength(1000)
.WithMessage("Description must be less than 1000 characters / Mô tả phải ít hơn 1000 ký tự")
.When(x => x.Description != null);
}
}

View File

@@ -1,200 +0,0 @@
using Asp.Versioning;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using FnbEngine.API.Application.Commands;
using FnbEngine.API.Application.Queries;
namespace FnbEngine.API.Controllers;
/// <summary>
/// EN: Controller for Sample CRUD operations using CQRS pattern.
/// VI: Controller cho các thao tác CRUD Sample sử dụng pattern CQRS.
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class SamplesController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<SamplesController> _logger;
public SamplesController(IMediator mediator, ILogger<SamplesController> logger)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// EN: Get all samples.
/// VI: Lấy tất cả samples.
/// </summary>
/// <returns>EN: List of samples / VI: Danh sách samples</returns>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<SampleViewModel>), StatusCodes.Status200OK)]
public async Task<IActionResult> GetSamples()
{
var samples = await _mediator.Send(new GetSamplesQuery());
return Ok(new { success = true, data = samples });
}
/// <summary>
/// EN: Get a sample by ID.
/// VI: Lấy một sample theo ID.
/// </summary>
/// <param name="id">EN: Sample ID / VI: ID sample</param>
/// <returns>EN: Sample details / VI: Chi tiết sample</returns>
[HttpGet("{id:guid}")]
[ProducesResponseType(typeof(SampleViewModel), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> GetSample(Guid id)
{
var sample = await _mediator.Send(new GetSampleQuery(id));
if (sample is null)
{
return NotFound(new
{
success = false,
error = new
{
code = "SAMPLE_NOT_FOUND",
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
}
});
}
return Ok(new { success = true, data = sample });
}
/// <summary>
/// EN: Create a new sample.
/// VI: Tạo một sample mới.
/// </summary>
/// <param name="request">EN: Create request / VI: Request tạo</param>
/// <returns>EN: Created sample ID / VI: ID sample đã tạo</returns>
[HttpPost]
[ProducesResponseType(typeof(CreateSampleCommandResult), StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> CreateSample([FromBody] CreateSampleRequest request)
{
var command = new CreateSampleCommand(request.Name, request.Description);
var result = await _mediator.Send(command);
return CreatedAtAction(
nameof(GetSample),
new { id = result.Id },
new { success = true, data = result });
}
/// <summary>
/// EN: Update an existing sample.
/// VI: Cập nhật một sample đã tồn tại.
/// </summary>
/// <param name="id">EN: Sample ID / VI: ID sample</param>
/// <param name="request">EN: Update request / VI: Request cập nhật</param>
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
[HttpPut("{id:guid}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateSample(Guid id, [FromBody] UpdateSampleRequest request)
{
var command = new UpdateSampleCommand(id, request.Name, request.Description);
var result = await _mediator.Send(command);
if (!result)
{
return NotFound(new
{
success = false,
error = new
{
code = "SAMPLE_NOT_FOUND",
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
}
});
}
return Ok(new { success = true, message = "Sample updated successfully / Sample đã cập nhật thành công" });
}
/// <summary>
/// EN: Delete a sample.
/// VI: Xóa một sample.
/// </summary>
/// <param name="id">EN: Sample ID / VI: ID sample</param>
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
[HttpDelete("{id:guid}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteSample(Guid id)
{
var command = new DeleteSampleCommand(id);
var result = await _mediator.Send(command);
if (!result)
{
return NotFound(new
{
success = false,
error = new
{
code = "SAMPLE_NOT_FOUND",
message = $"Sample with ID {id} not found / Sample với ID {id} không tìm thấy"
}
});
}
return NoContent();
}
/// <summary>
/// EN: Change sample status.
/// VI: Thay đổi trạng thái sample.
/// </summary>
/// <param name="id">EN: Sample ID / VI: ID sample</param>
/// <param name="request">EN: Status change request / VI: Request thay đổi trạng thái</param>
/// <returns>EN: Success status / VI: Trạng thái thành công</returns>
[HttpPatch("{id:guid}/status")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> ChangeSampleStatus(Guid id, [FromBody] ChangeStatusRequest request)
{
var command = new ChangeSampleStatusCommand(id, request.Status);
var result = await _mediator.Send(command);
if (!result)
{
return BadRequest(new
{
success = false,
error = new
{
code = "STATUS_CHANGE_FAILED",
message = "Failed to change sample status / Thay đổi trạng thái sample thất bại"
}
});
}
return Ok(new { success = true, message = "Sample status changed successfully / Trạng thái sample đã thay đổi thành công" });
}
}
/// <summary>
/// EN: Request model for creating a sample.
/// VI: Model request để tạo sample.
/// </summary>
public record CreateSampleRequest(string Name, string? Description);
/// <summary>
/// EN: Request model for updating a sample.
/// VI: Model request để cập nhật sample.
/// </summary>
public record UpdateSampleRequest(string Name, string? Description);
/// <summary>
/// EN: Request model for changing sample status.
/// VI: Model request để thay đổi trạng thái sample.
/// </summary>
public record ChangeStatusRequest(string Status);

View File

@@ -14,6 +14,10 @@
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />

View File

@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
using FnbEngine.Infrastructure.Idempotency;
using FnbEngine.Infrastructure.Repositories;

View File

@@ -1,61 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.Infrastructure.EntityConfigurations;
/// <summary>
/// EN: EF Core configuration for Sample entity.
/// VI: Cấu hình EF Core cho entity Sample.
/// </summary>
public class SampleEntityTypeConfiguration : IEntityTypeConfiguration<Sample>
{
public void Configure(EntityTypeBuilder<Sample> builder)
{
// EN: Table name / VI: Tên bảng
builder.ToTable("samples");
// EN: Primary key / VI: Khóa chính
builder.HasKey(s => s.Id);
// EN: Ignore domain events (not persisted)
// VI: Bỏ qua domain events (không lưu)
builder.Ignore(s => s.DomainEvents);
// EN: Properties / VI: Các thuộc tính
builder.Property(s => s.Id)
.HasColumnName("id")
.IsRequired();
builder.Property<string>("_name")
.HasColumnName("name")
.HasMaxLength(200)
.IsRequired();
builder.Property<string?>("_description")
.HasColumnName("description")
.HasMaxLength(1000);
builder.Property<DateTime>("_createdAt")
.HasColumnName("created_at")
.IsRequired();
builder.Property<DateTime?>("_updatedAt")
.HasColumnName("updated_at");
// EN: Status relationship / VI: Quan hệ với Status
builder.Property(s => s.StatusId)
.HasColumnName("status_id")
.IsRequired();
builder.HasOne(s => s.Status)
.WithMany()
.HasForeignKey(s => s.StatusId)
.OnDelete(DeleteBehavior.Restrict);
// EN: Indexes / VI: Các index
builder.HasIndex("_name");
builder.HasIndex(s => s.StatusId);
builder.HasIndex("_createdAt");
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
namespace FnbEngine.Infrastructure.EntityConfigurations;
/// <summary>
/// EN: EF Core configuration for SampleStatus enumeration.
/// VI: Cấu hình EF Core cho enumeration SampleStatus.
/// </summary>
public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration<SampleStatus>
{
public void Configure(EntityTypeBuilder<SampleStatus> builder)
{
// EN: Table name / VI: Tên bảng
builder.ToTable("sample_statuses");
// EN: Primary key / VI: Khóa chính
builder.HasKey(s => s.Id);
builder.Property(s => s.Id)
.HasColumnName("id")
.ValueGeneratedNever()
.IsRequired();
builder.Property(s => s.Name)
.HasColumnName("name")
.HasMaxLength(50)
.IsRequired();
// EN: Seed initial data / VI: Seed dữ liệu ban đầu
builder.HasData(
SampleStatus.Draft,
SampleStatus.Active,
SampleStatus.Completed,
SampleStatus.Cancelled
);
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
namespace FnbEngine.Infrastructure.EntityConfigurations;
public class TableEntityTypeConfiguration : IEntityTypeConfiguration<Table>
{
public void Configure(EntityTypeBuilder<Table> builder)
{
builder.ToTable("tables");
builder.HasKey(t => t.Id);
builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedNever();
builder.Property<Guid>("_shopId").HasColumnName("shop_id").IsRequired();
builder.Property<string>("_tableNumber").HasColumnName("table_number").HasMaxLength(20).IsRequired();
builder.Property(t => t.StatusId).HasColumnName("status_id").IsRequired();
builder.Property<int>("_capacity").HasColumnName("capacity").IsRequired();
builder.Property<string?>("_location").HasColumnName("location").HasMaxLength(100);
builder.OwnsMany(t => t.Sessions, s =>
{
s.ToTable("table_sessions");
s.WithOwner().HasForeignKey("TableId");
s.Property<Guid>("TableId").HasColumnName("table_id");
s.HasKey(x => x.Id);
s.Property(x => x.Id).HasColumnName("id").ValueGeneratedNever();
s.Property<Guid?>("_orderId").HasColumnName("order_id");
s.Property<DateTime>("_startTime").HasColumnName("start_time").IsRequired();
s.Property<DateTime?>("_endTime").HasColumnName("end_time");
s.Property<bool>("_isActive").HasColumnName("is_active").IsRequired();
s.Ignore(x => x.OrderId);
s.Ignore(x => x.StartTime);
s.Ignore(x => x.EndTime);
s.Ignore(x => x.IsActive);
});
builder.HasIndex("_shopId").HasDatabaseName("ix_tables_shop_id");
builder.HasIndex("_tableNumber").HasDatabaseName("ix_tables_table_number");
builder.Ignore(t => t.ShopId);
builder.Ignore(t => t.TableNumber);
builder.Ignore(t => t.Status);
builder.Ignore(t => t.Capacity);
builder.Ignore(t => t.Location);
}
}

View File

@@ -0,0 +1,17 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
namespace FnbEngine.Infrastructure.EntityConfigurations;
public class TableStatusEntityTypeConfiguration : IEntityTypeConfiguration<TableStatus>
{
public void Configure(EntityTypeBuilder<TableStatus> builder)
{
builder.ToTable("table_statuses");
builder.HasKey(s => s.Id);
builder.Property(s => s.Id).HasColumnName("id").ValueGeneratedNever();
builder.Property(s => s.Name).HasColumnName("name").HasMaxLength(50).IsRequired();
builder.HasData(TableStatus.Available, TableStatus.Occupied, TableStatus.Reserved, TableStatus.Cleaning);
}
}

View File

@@ -0,0 +1,67 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using FnbEngine.Domain.AggregatesModel.TableAggregate;
using FnbEngine.Domain.SeedWork;
using FnbEngine.Infrastructure.EntityConfigurations;
namespace FnbEngine.Infrastructure;
public class FnbContext : DbContext, IUnitOfWork
{
private readonly IMediator _mediator;
private IDbContextTransaction? _currentTransaction;
public DbSet<Table> Tables => Set<Table>();
public IDbContextTransaction? CurrentTransaction => _currentTransaction;
public bool HasActiveTransaction => _currentTransaction != null;
public FnbContext(DbContextOptions<FnbContext> options, IMediator mediator) : base(options)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new TableEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new TableStatusEntityTypeConfiguration());
}
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
await DispatchDomainEventsAsync();
await base.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<IDbContextTransaction?> BeginTransactionAsync()
{
if (_currentTransaction != null) return null;
_currentTransaction = await Database.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);
return _currentTransaction;
}
public async Task CommitTransactionAsync(IDbContextTransaction transaction)
{
ArgumentNullException.ThrowIfNull(transaction);
if (transaction != _currentTransaction)
throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
try { await SaveChangesAsync(); await transaction.CommitAsync(); }
catch { RollbackTransaction(); throw; }
finally { if (_currentTransaction != null) { _currentTransaction.Dispose(); _currentTransaction = null; } }
}
public void RollbackTransaction()
{
try { _currentTransaction?.Rollback(); }
finally { if (_currentTransaction != null) { _currentTransaction.Dispose(); _currentTransaction = null; } }
}
private async Task DispatchDomainEventsAsync()
{
var domainEntities = ChangeTracker.Entries<Entity>().Where(x => x.Entity.DomainEvents.Any()).ToList();
var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();
domainEntities.ForEach(entity => entity.Entity.ClearDomainEvents());
foreach (var domainEvent in domainEvents) await _mediator.Publish(domainEvent);
}
}

View File

@@ -1,160 +0,0 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
using FnbEngine.Domain.SeedWork;
using FnbEngine.Infrastructure.EntityConfigurations;
namespace FnbEngine.Infrastructure;
/// <summary>
/// EN: EF Core DbContext for FnbEngine.
/// VI: EF Core DbContext cho FnbEngine.
/// </summary>
public class FnbEngineContext : DbContext, IUnitOfWork
{
private readonly IMediator _mediator;
private IDbContextTransaction? _currentTransaction;
/// <summary>
/// EN: Samples table.
/// VI: Bảng Samples.
/// </summary>
public DbSet<Sample> Samples => Set<Sample>();
/// <summary>
/// EN: Read-only access to current transaction.
/// VI: Truy cập chỉ đọc đến transaction hiện tại.
/// </summary>
public IDbContextTransaction? CurrentTransaction => _currentTransaction;
/// <summary>
/// EN: Check if there is an active transaction.
/// VI: Kiểm tra xem có transaction đang hoạt động không.
/// </summary>
public bool HasActiveTransaction => _currentTransaction != null;
public FnbEngineContext(DbContextOptions<FnbEngineContext> options) : base(options)
{
_mediator = null!;
}
public FnbEngineContext(DbContextOptions<FnbEngineContext> options, IMediator mediator) : base(options)
{
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
System.Diagnostics.Debug.WriteLine("FnbEngineContext::ctor - " + GetHashCode());
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// EN: Apply entity configurations
// VI: Áp dụng các cấu hình entity
modelBuilder.ApplyConfiguration(new SampleEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new SampleStatusEntityTypeConfiguration());
}
/// <summary>
/// EN: Save entities and dispatch domain events.
/// VI: Lưu entities và dispatch domain events.
/// </summary>
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
// EN: Dispatch domain events before saving (side effects)
// VI: Dispatch domain events trước khi lưu (side effects)
await DispatchDomainEventsAsync();
// EN: Save changes to database
// VI: Lưu thay đổi vào database
await base.SaveChangesAsync(cancellationToken);
return true;
}
/// <summary>
/// EN: Begin a new transaction if none is active.
/// VI: Bắt đầu một transaction mới nếu không có transaction nào đang hoạt động.
/// </summary>
public async Task<IDbContextTransaction?> BeginTransactionAsync()
{
if (_currentTransaction != null) return null;
_currentTransaction = await Database.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted);
return _currentTransaction;
}
/// <summary>
/// EN: Commit the current transaction.
/// VI: Commit transaction hiện tại.
/// </summary>
public async Task CommitTransactionAsync(IDbContextTransaction transaction)
{
ArgumentNullException.ThrowIfNull(transaction);
if (transaction != _currentTransaction)
throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
try
{
await SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
RollbackTransaction();
throw;
}
finally
{
if (_currentTransaction != null)
{
_currentTransaction.Dispose();
_currentTransaction = null;
}
}
}
/// <summary>
/// EN: Rollback the current transaction.
/// VI: Rollback transaction hiện tại.
/// </summary>
public void RollbackTransaction()
{
try
{
_currentTransaction?.Rollback();
}
finally
{
if (_currentTransaction != null)
{
_currentTransaction.Dispose();
_currentTransaction = null;
}
}
}
/// <summary>
/// EN: Dispatch all domain events from tracked entities.
/// VI: Dispatch tất cả domain events từ các entities đang được track.
/// </summary>
private async Task DispatchDomainEventsAsync()
{
var domainEntities = ChangeTracker
.Entries<Entity>()
.Where(x => x.Entity.DomainEvents.Any())
.ToList();
var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
domainEntities.ForEach(entity => entity.Entity.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent);
}
}
}

View File

@@ -1,72 +0,0 @@
using Microsoft.EntityFrameworkCore;
using FnbEngine.Domain.AggregatesModel.SampleAggregate;
using FnbEngine.Domain.SeedWork;
namespace FnbEngine.Infrastructure.Repositories;
/// <summary>
/// EN: Repository implementation for Sample aggregate.
/// VI: Triển khai repository cho Sample aggregate.
/// </summary>
public class SampleRepository : ISampleRepository
{
private readonly FnbEngineContext _context;
/// <summary>
/// EN: Unit of work for transaction management.
/// VI: Unit of work cho quản lý transaction.
/// </summary>
public IUnitOfWork UnitOfWork => _context;
public SampleRepository(FnbEngineContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <inheritdoc/>
public async Task<Sample?> GetAsync(Guid sampleId)
{
var sample = await _context.Samples
.Include(s => s.Status)
.FirstOrDefaultAsync(s => s.Id == sampleId);
return sample;
}
/// <inheritdoc/>
public async Task<IEnumerable<Sample>> GetAllAsync()
{
return await _context.Samples
.Include(s => s.Status)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync();
}
/// <inheritdoc/>
public Sample Add(Sample sample)
{
return _context.Samples.Add(sample).Entity;
}
/// <inheritdoc/>
public void Update(Sample sample)
{
_context.Entry(sample).State = EntityState.Modified;
}
/// <inheritdoc/>
public void Delete(Sample sample)
{
_context.Samples.Remove(sample);
}
/// <inheritdoc/>
public async Task<IEnumerable<Sample>> GetByStatusAsync(int statusId)
{
return await _context.Samples
.Include(s => s.Status)
.Where(s => s.StatusId == statusId)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync();
}
}

View File

@@ -0,0 +1,43 @@
// EN: Inventory repository interface.
// VI: Interface repository Inventory.
using InventoryService.Domain.SeedWork;
namespace InventoryService.Domain.AggregatesModel.InventoryAggregate;
/// <summary>
/// EN: Repository interface for InventoryItem aggregate.
/// VI: Interface repository cho InventoryItem aggregate.
/// </summary>
public interface IInventoryRepository : IRepository<InventoryItem>
{
/// <summary>
/// EN: Add a new inventory item.
/// VI: Thêm inventory item mới.
/// </summary>
InventoryItem Add(InventoryItem item);
/// <summary>
/// EN: Update an inventory item.
/// VI: Cập nhật inventory item.
/// </summary>
void Update(InventoryItem item);
/// <summary>
/// EN: Get inventory item by ID.
/// VI: Lấy inventory item theo ID.
/// </summary>
Task<InventoryItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
/// <summary>
/// EN: Get inventory item by product ID and shop ID.
/// VI: Lấy inventory item theo product ID và shop ID.
/// </summary>
Task<InventoryItem?> GetByProductIdAsync(Guid productId, Guid shopId, CancellationToken cancellationToken = default);
/// <summary>
/// EN: Get all inventory items for a shop.
/// VI: Lấy tất cả inventory items cho một shop.
/// </summary>
Task<IEnumerable<InventoryItem>> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default);
}

View File

@@ -1,61 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using InventoryService.Domain.AggregatesModel.SampleAggregate;
namespace InventoryService.Infrastructure.EntityConfigurations;
/// <summary>
/// EN: EF Core configuration for Sample entity.
/// VI: Cấu hình EF Core cho entity Sample.
/// </summary>
public class SampleEntityTypeConfiguration : IEntityTypeConfiguration<Sample>
{
public void Configure(EntityTypeBuilder<Sample> builder)
{
// EN: Table name / VI: Tên bảng
builder.ToTable("samples");
// EN: Primary key / VI: Khóa chính
builder.HasKey(s => s.Id);
// EN: Ignore domain events (not persisted)
// VI: Bỏ qua domain events (không lưu)
builder.Ignore(s => s.DomainEvents);
// EN: Properties / VI: Các thuộc tính
builder.Property(s => s.Id)
.HasColumnName("id")
.IsRequired();
builder.Property<string>("_name")
.HasColumnName("name")
.HasMaxLength(200)
.IsRequired();
builder.Property<string?>("_description")
.HasColumnName("description")
.HasMaxLength(1000);
builder.Property<DateTime>("_createdAt")
.HasColumnName("created_at")
.IsRequired();
builder.Property<DateTime?>("_updatedAt")
.HasColumnName("updated_at");
// EN: Status relationship / VI: Quan hệ với Status
builder.Property(s => s.StatusId)
.HasColumnName("status_id")
.IsRequired();
builder.HasOne(s => s.Status)
.WithMany()
.HasForeignKey(s => s.StatusId)
.OnDelete(DeleteBehavior.Restrict);
// EN: Indexes / VI: Các index
builder.HasIndex("_name");
builder.HasIndex(s => s.StatusId);
builder.HasIndex("_createdAt");
}
}

View File

@@ -1,39 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using InventoryService.Domain.AggregatesModel.SampleAggregate;
namespace InventoryService.Infrastructure.EntityConfigurations;
/// <summary>
/// EN: EF Core configuration for SampleStatus enumeration.
/// VI: Cấu hình EF Core cho enumeration SampleStatus.
/// </summary>
public class SampleStatusEntityTypeConfiguration : IEntityTypeConfiguration<SampleStatus>
{
public void Configure(EntityTypeBuilder<SampleStatus> builder)
{
// EN: Table name / VI: Tên bảng
builder.ToTable("sample_statuses");
// EN: Primary key / VI: Khóa chính
builder.HasKey(s => s.Id);
builder.Property(s => s.Id)
.HasColumnName("id")
.ValueGeneratedNever()
.IsRequired();
builder.Property(s => s.Name)
.HasColumnName("name")
.HasMaxLength(50)
.IsRequired();
// EN: Seed initial data / VI: Seed dữ liệu ban đầu
builder.HasData(
SampleStatus.Draft,
SampleStatus.Active,
SampleStatus.Completed,
SampleStatus.Cancelled
);
}
}

View File

@@ -1,72 +0,0 @@
using Microsoft.EntityFrameworkCore;
using InventoryService.Domain.AggregatesModel.SampleAggregate;
using InventoryService.Domain.SeedWork;
namespace InventoryService.Infrastructure.Repositories;
/// <summary>
/// EN: Repository implementation for Sample aggregate.
/// VI: Triển khai repository cho Sample aggregate.
/// </summary>
public class SampleRepository : ISampleRepository
{
private readonly InventoryServiceContext _context;
/// <summary>
/// EN: Unit of work for transaction management.
/// VI: Unit of work cho quản lý transaction.
/// </summary>
public IUnitOfWork UnitOfWork => _context;
public SampleRepository(InventoryServiceContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
/// <inheritdoc/>
public async Task<Sample?> GetAsync(Guid sampleId)
{
var sample = await _context.Samples
.Include(s => s.Status)
.FirstOrDefaultAsync(s => s.Id == sampleId);
return sample;
}
/// <inheritdoc/>
public async Task<IEnumerable<Sample>> GetAllAsync()
{
return await _context.Samples
.Include(s => s.Status)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync();
}
/// <inheritdoc/>
public Sample Add(Sample sample)
{
return _context.Samples.Add(sample).Entity;
}
/// <inheritdoc/>
public void Update(Sample sample)
{
_context.Entry(sample).State = EntityState.Modified;
}
/// <inheritdoc/>
public void Delete(Sample sample)
{
_context.Samples.Remove(sample);
}
/// <inheritdoc/>
public async Task<IEnumerable<Sample>> GetByStatusAsync(int statusId)
{
return await _context.Samples
.Include(s => s.Status)
.Where(s => s.StatusId == statusId)
.OrderByDescending(s => s.CreatedAt)
.ToListAsync();
}
}