feat: Implement inventory domain by replacing generic service context with dedicated inventory context, entities, and repository.
This commit is contained in:
@@ -13,11 +13,11 @@ namespace InventoryService.API.Application.Behaviors;
|
||||
public class TransactionBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
private readonly InventoryServiceContext _dbContext;
|
||||
private readonly InventoryContext _dbContext;
|
||||
private readonly ILogger<TransactionBehavior<TRequest, TResponse>> _logger;
|
||||
|
||||
public TransactionBehavior(
|
||||
InventoryServiceContext dbContext,
|
||||
InventoryContext dbContext,
|
||||
ILogger<TransactionBehavior<TRequest, TResponse>> logger)
|
||||
{
|
||||
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using InventoryService.Domain.AggregatesModel.SampleAggregate;
|
||||
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
|
||||
using InventoryService.Infrastructure.Idempotency;
|
||||
using InventoryService.Infrastructure.Repositories;
|
||||
|
||||
@@ -22,7 +22,7 @@ public static class DependencyInjection
|
||||
IConfiguration configuration)
|
||||
{
|
||||
// EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL
|
||||
services.AddDbContext<InventoryServiceContext>(options =>
|
||||
services.AddDbContext<InventoryContext>(options =>
|
||||
{
|
||||
var connectionString = configuration.GetConnectionString("DefaultConnection")
|
||||
?? configuration["DATABASE_URL"]
|
||||
@@ -30,7 +30,7 @@ public static class DependencyInjection
|
||||
|
||||
options.UseNpgsql(connectionString, npgsqlOptions =>
|
||||
{
|
||||
npgsqlOptions.MigrationsAssembly(typeof(InventoryServiceContext).Assembly.FullName);
|
||||
npgsqlOptions.MigrationsAssembly(typeof(InventoryContext).Assembly.FullName);
|
||||
npgsqlOptions.EnableRetryOnFailure(
|
||||
maxRetryCount: 5,
|
||||
maxRetryDelay: TimeSpan.FromSeconds(30),
|
||||
@@ -47,7 +47,7 @@ public static class DependencyInjection
|
||||
});
|
||||
|
||||
// EN: Register repositories / VI: Đăng ký repositories
|
||||
services.AddScoped<ISampleRepository, SampleRepository>();
|
||||
services.AddScoped<IInventoryRepository, InventoryRepository>();
|
||||
|
||||
// EN: Register idempotency services / VI: Đăng ký idempotency services
|
||||
services.AddScoped<IRequestManager, RequestManager>();
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// EN: InventoryItem entity configuration.
|
||||
// VI: Cấu hình entity InventoryItem.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
|
||||
|
||||
namespace InventoryService.Infrastructure.EntityConfigurations;
|
||||
|
||||
public class InventoryItemEntityTypeConfiguration : IEntityTypeConfiguration<InventoryItem>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<InventoryItem> builder)
|
||||
{
|
||||
builder.ToTable("inventory_items");
|
||||
|
||||
builder.HasKey(i => i.Id);
|
||||
builder.Property(i => i.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
|
||||
builder.Property<Guid>("_productId").HasColumnName("product_id").IsRequired();
|
||||
builder.Property<Guid>("_shopId").HasColumnName("shop_id").IsRequired();
|
||||
builder.Property<int>("_quantityOnHand").HasColumnName("quantity_on_hand").IsRequired();
|
||||
builder.Property<int>("_reservedQuantity").HasColumnName("reserved_quantity").IsRequired();
|
||||
builder.Property<int>("_reorderLevel").HasColumnName("reorder_level").HasDefaultValue(10);
|
||||
builder.Property<DateTime>("_updatedAt").HasColumnName("updated_at").IsRequired();
|
||||
|
||||
// Owned InventoryTransaction collection
|
||||
builder.OwnsMany(i => i.Transactions, txn =>
|
||||
{
|
||||
txn.ToTable("inventory_transactions");
|
||||
txn.WithOwner().HasForeignKey("InventoryItemId");
|
||||
txn.Property<Guid>("InventoryItemId").HasColumnName("inventory_item_id");
|
||||
txn.HasKey(t => t.Id);
|
||||
txn.Property(t => t.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
txn.Property(t => t.TypeId).HasColumnName("type_id").IsRequired();
|
||||
txn.Property<int>("_quantity").HasColumnName("quantity").IsRequired();
|
||||
txn.Property<Guid?>("_referenceId").HasColumnName("reference_id");
|
||||
txn.Property<string?>("_notes").HasColumnName("notes").HasMaxLength(500);
|
||||
txn.Property<DateTime>("_createdAt").HasColumnName("created_at").IsRequired();
|
||||
|
||||
txn.Ignore(t => t.Type);
|
||||
txn.Ignore(t => t.Quantity);
|
||||
txn.Ignore(t => t.ReferenceId);
|
||||
txn.Ignore(t => t.Notes);
|
||||
txn.Ignore(t => t.CreatedAt);
|
||||
});
|
||||
|
||||
builder.HasIndex("_productId").HasDatabaseName("ix_inventory_product_id");
|
||||
builder.HasIndex("_shopId").HasDatabaseName("ix_inventory_shop_id");
|
||||
|
||||
builder.Ignore(i => i.ProductId);
|
||||
builder.Ignore(i => i.ShopId);
|
||||
builder.Ignore(i => i.QuantityOnHand);
|
||||
builder.Ignore(i => i.ReservedQuantity);
|
||||
builder.Ignore(i => i.AvailableQuantity);
|
||||
builder.Ignore(i => i.ReorderLevel);
|
||||
builder.Ignore(i => i.UpdatedAt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// EN: TransactionType enumeration configuration.
|
||||
// VI: Cấu hình TransactionType enumeration.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
|
||||
|
||||
namespace InventoryService.Infrastructure.EntityConfigurations;
|
||||
|
||||
public class TransactionTypeEntityTypeConfiguration : IEntityTypeConfiguration<TransactionType>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<TransactionType> builder)
|
||||
{
|
||||
builder.ToTable("transaction_types");
|
||||
|
||||
builder.HasKey(t => t.Id);
|
||||
builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedNever();
|
||||
builder.Property(t => t.Name).HasColumnName("name").HasMaxLength(50).IsRequired();
|
||||
|
||||
builder.HasData(
|
||||
TransactionType.In,
|
||||
TransactionType.Out,
|
||||
TransactionType.Adjustment,
|
||||
TransactionType.Reserve,
|
||||
TransactionType.Release
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,9 @@ namespace InventoryService.Infrastructure.Idempotency;
|
||||
/// </summary>
|
||||
public class RequestManager : IRequestManager
|
||||
{
|
||||
private readonly InventoryServiceContext _context;
|
||||
private readonly InventoryContext _context;
|
||||
|
||||
public RequestManager(InventoryServiceContext context)
|
||||
public RequestManager(InventoryContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using MediatR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using InventoryService.Domain.AggregatesModel.SampleAggregate;
|
||||
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
|
||||
using InventoryService.Domain.SeedWork;
|
||||
using InventoryService.Infrastructure.EntityConfigurations;
|
||||
|
||||
@@ -11,16 +11,16 @@ namespace InventoryService.Infrastructure;
|
||||
/// EN: EF Core DbContext for InventoryService.
|
||||
/// VI: EF Core DbContext cho InventoryService.
|
||||
/// </summary>
|
||||
public class InventoryServiceContext : DbContext, IUnitOfWork
|
||||
public class InventoryContext : DbContext, IUnitOfWork
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private IDbContextTransaction? _currentTransaction;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Samples table.
|
||||
/// VI: Bảng Samples.
|
||||
/// EN: Inventory items table.
|
||||
/// VI: Bảng Inventory items.
|
||||
/// </summary>
|
||||
public DbSet<Sample> Samples => Set<Sample>();
|
||||
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
|
||||
|
||||
/// <summary>
|
||||
/// EN: Read-only access to current transaction.
|
||||
@@ -34,64 +34,35 @@ public class InventoryServiceContext : DbContext, IUnitOfWork
|
||||
/// </summary>
|
||||
public bool HasActiveTransaction => _currentTransaction != null;
|
||||
|
||||
public InventoryServiceContext(DbContextOptions<InventoryServiceContext> options) : base(options)
|
||||
{
|
||||
_mediator = null!;
|
||||
}
|
||||
|
||||
public InventoryServiceContext(DbContextOptions<InventoryServiceContext> options, IMediator mediator) : base(options)
|
||||
public InventoryContext(DbContextOptions<InventoryContext> options, IMediator mediator) : base(options)
|
||||
{
|
||||
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
||||
|
||||
System.Diagnostics.Debug.WriteLine("InventoryServiceContext::ctor - " + GetHashCode());
|
||||
System.Diagnostics.Debug.WriteLine("InventoryContext::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());
|
||||
modelBuilder.ApplyConfiguration(new InventoryItemEntityTypeConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new TransactionTypeEntityTypeConfiguration());
|
||||
}
|
||||
|
||||
/// <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");
|
||||
|
||||
@@ -115,10 +86,6 @@ public class InventoryServiceContext : DbContext, IUnitOfWork
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Rollback the current transaction.
|
||||
/// VI: Rollback transaction hiện tại.
|
||||
/// </summary>
|
||||
public void RollbackTransaction()
|
||||
{
|
||||
try
|
||||
@@ -135,10 +102,6 @@ public class InventoryServiceContext : DbContext, IUnitOfWork
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
@@ -0,0 +1,44 @@
|
||||
using InventoryService.Domain.AggregatesModel.InventoryAggregate;
|
||||
using InventoryService.Domain.SeedWork;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace InventoryService.Infrastructure.Repositories;
|
||||
|
||||
public class InventoryRepository : IInventoryRepository
|
||||
{
|
||||
private readonly InventoryContext _context;
|
||||
public IUnitOfWork UnitOfWork => _context;
|
||||
|
||||
public InventoryRepository(InventoryContext context)
|
||||
{
|
||||
_context = context ?? throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
public InventoryItem Add(InventoryItem item)
|
||||
{
|
||||
return _context.InventoryItems.Add(item).Entity;
|
||||
}
|
||||
|
||||
public void Update(InventoryItem item)
|
||||
{
|
||||
_context.Entry(item).State = EntityState.Modified;
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<InventoryItem?> GetByProductIdAsync(Guid productId, Guid shopId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.FirstOrDefaultAsync(i => i.ProductId == productId && i.ShopId == shopId, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<InventoryItem>> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _context.InventoryItems
|
||||
.Where(i => i.ShopId == shopId)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// EN: Remove the existing DbContext registration
|
||||
// VI: Xóa đăng ký DbContext hiện tại
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(DbContextOptions<InventoryServiceContext>));
|
||||
d => d.ServiceType == typeof(DbContextOptions<InventoryContext>));
|
||||
|
||||
if (descriptor != null)
|
||||
{
|
||||
@@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// EN: Remove DbContext service
|
||||
// VI: Xóa DbContext service
|
||||
var dbContextDescriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(InventoryServiceContext));
|
||||
d => d.ServiceType == typeof(InventoryContext));
|
||||
|
||||
if (dbContextDescriptor != null)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
|
||||
// EN: Add in-memory database for testing
|
||||
// VI: Thêm in-memory database để test
|
||||
services.AddDbContext<InventoryServiceContext>(options =>
|
||||
services.AddDbContext<InventoryContext>(options =>
|
||||
{
|
||||
options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString());
|
||||
});
|
||||
@@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory<Program>
|
||||
// VI: Đảm bảo database được tạo với seed data
|
||||
var sp = services.BuildServiceProvider();
|
||||
using var scope = sp.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<InventoryServiceContext>();
|
||||
var db = scope.ServiceProvider.GetRequiredService<InventoryContext>();
|
||||
db.Database.EnsureCreated();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user