initial commit
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
using Indotalent.ConfigBackEnd.Extensions;
|
||||
using Indotalent.ConfigBackEnd.Interfaces;
|
||||
using Indotalent.Data.Abstracts;
|
||||
using Indotalent.Data.Entities;
|
||||
using Indotalent.Data.Interfaces;
|
||||
using Indotalent.Infrastructure.AutoNumberGenerator;
|
||||
using Indotalent.Shared.Consts;
|
||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Indotalent.Infrastructure.Database;
|
||||
|
||||
public class AppDbContext : IdentityDbContext<ApplicationUser>
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public AppDbContext(
|
||||
DbContextOptions<AppDbContext> options,
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ICurrentUserService currentUserService) : base(options)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public string CurrentTenantId => _currentUserService?.TenantId ?? "NO_TENANT_SELECTED";
|
||||
|
||||
public DbSet<AutoNumberSequence> AutoNumberSequence { get; set; } = default!;
|
||||
public DbSet<Currency> Currency { get; set; } = default!;
|
||||
public DbSet<Company> Company { get; set; } = default!;
|
||||
public DbSet<Tax> Tax { get; set; } = default!;
|
||||
public DbSet<PaymentMethod> PaymentMethod { get; set; } = default!;
|
||||
public DbSet<Booking> Booking { get; set; } = default!;
|
||||
public DbSet<BookingGroup> BookingGroup { get; set; } = default!;
|
||||
public DbSet<BookingResource> BookingResource { get; set; } = default!;
|
||||
public DbSet<Campaign> Campaign { get; set; } = default!;
|
||||
public DbSet<Lead> Lead { get; set; } = default!;
|
||||
public DbSet<LeadContact> LeadContact { get; set; } = default!;
|
||||
public DbSet<LeadActivity> LeadActivity { get; set; } = default!;
|
||||
public DbSet<Budget> Budget { get; set; } = default!;
|
||||
public DbSet<Expense> Expense { get; set; } = default!;
|
||||
public DbSet<GoodsReceive> GoodsReceive { get; set; } = default!;
|
||||
public DbSet<Bill> Bill { get; set; } = default!;
|
||||
public DbSet<PaymentDisburse> PaymentDisburse { get; set; } = default!;
|
||||
public DbSet<DeliveryOrder> DeliveryOrder { get; set; } = default!;
|
||||
public DbSet<Invoice> Invoice { get; set; } = default!;
|
||||
public DbSet<PaymentReceive> PaymentReceive { get; set; } = default!;
|
||||
public DbSet<DebitNote> DebitNote { get; set; } = default!;
|
||||
public DbSet<CreditNote> CreditNote { get; set; } = default!;
|
||||
public DbSet<PositiveAdjustment> PositiveAdjustment { get; set; } = default!;
|
||||
public DbSet<NegativeAdjustment> NegativeAdjustment { get; set; } = default!;
|
||||
public DbSet<ProductGroup> ProductGroup { get; set; } = default!;
|
||||
public DbSet<ProgramManager> ProgramManager { get; set; } = default!;
|
||||
public DbSet<ProgramManagerResource> ProgramManagerResource { get; set; } = default!;
|
||||
public DbSet<PurchaseOrder> PurchaseOrder { get; set; } = default!;
|
||||
public DbSet<PurchaseOrderItem> PurchaseOrderItem { get; set; } = default!;
|
||||
public DbSet<PurchaseRequisition> PurchaseRequisition { get; set; } = default!;
|
||||
public DbSet<PurchaseRequisitionItem> PurchaseRequisitionItem { get; set; } = default!;
|
||||
public DbSet<PurchaseReturn> PurchaseReturn { get; set; } = default!;
|
||||
public DbSet<Customer> Customer { get; set; } = default!;
|
||||
public DbSet<CustomerCategory> CustomerCategory { get; set; } = default!;
|
||||
public DbSet<CustomerContact> CustomerContact { get; set; } = default!;
|
||||
public DbSet<CustomerGroup> CustomerGroup { get; set; } = default!;
|
||||
public DbSet<Product> Product { get; set; } = default!;
|
||||
public DbSet<SalesOrder> SalesOrder { get; set; } = default!;
|
||||
public DbSet<SalesOrderItem> SalesOrderItem { get; set; } = default!;
|
||||
public DbSet<SalesQuotation> SalesQuotation { get; set; } = default!;
|
||||
public DbSet<SalesQuotationItem> SalesQuotationItem { get; set; } = default!;
|
||||
public DbSet<SalesRepresentative> SalesRepresentative { get; set; } = default!;
|
||||
public DbSet<SalesReturn> SalesReturn { get; set; } = default!;
|
||||
public DbSet<SalesTeam> SalesTeam { get; set; } = default!;
|
||||
public DbSet<Scrapping> Scrapping { get; set; } = default!;
|
||||
public DbSet<StockCount> StockCount { get; set; } = default!;
|
||||
public DbSet<Todo> Todo { get; set; } = default!;
|
||||
public DbSet<TodoItem> TodoItem { get; set; } = default!;
|
||||
public DbSet<TransferIn> TransferIn { get; set; } = default!;
|
||||
public DbSet<TransferOut> TransferOut { get; set; } = default!;
|
||||
public DbSet<UnitMeasure> UnitMeasure { get; set; } = default!;
|
||||
public DbSet<Vendor> Vendor { get; set; } = default!;
|
||||
public DbSet<VendorCategory> VendorCategory { get; set; } = default!;
|
||||
public DbSet<VendorContact> VendorContact { get; set; } = default!;
|
||||
public DbSet<VendorGroup> VendorGroup { get; set; } = default!;
|
||||
public DbSet<Warehouse> Warehouse { get; set; } = default!;
|
||||
public DbSet<InventoryTransaction> InventoryTransaction { get; set; } = default!;
|
||||
public DbSet<Tenant> Tenant { get; set; } = default!;
|
||||
public DbSet<TenantUser> TenantUser { get; set; } = default!;
|
||||
|
||||
public override int SaveChanges()
|
||||
{
|
||||
ApplySoftDelete();
|
||||
ApplyAudit();
|
||||
ApplyMultiTenant();
|
||||
return base.SaveChanges();
|
||||
}
|
||||
|
||||
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
ChangeTracker.DetectChanges();
|
||||
ApplySoftDelete();
|
||||
ApplyAudit();
|
||||
ApplyMultiTenant();
|
||||
await ApplyAutoNumberAsync(cancellationToken);
|
||||
return await base.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private void ApplySoftDelete()
|
||||
{
|
||||
var entries = ChangeTracker.Entries<IHasIsDeleted>()
|
||||
.Where(e => e.State == EntityState.Deleted)
|
||||
.ToList();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
entry.State = EntityState.Modified;
|
||||
entry.Entity.IsDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAudit()
|
||||
{
|
||||
var entries = ChangeTracker.Entries<IHasAudit>()
|
||||
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
|
||||
.ToList();
|
||||
|
||||
if (!entries.Any()) return;
|
||||
|
||||
var userId = _currentUserService.UserId ?? "SYSTEM";
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
throw new InvalidOperationException("UserId not found in the current user session context.");
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var auditEntity = entry.Entity;
|
||||
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
auditEntity.CreatedAt = now;
|
||||
auditEntity.CreatedBy = userId;
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.Property(nameof(IHasAudit.CreatedAt)).IsModified = false;
|
||||
entry.Property(nameof(IHasAudit.CreatedBy)).IsModified = false;
|
||||
}
|
||||
|
||||
auditEntity.UpdatedAt = now;
|
||||
auditEntity.UpdatedBy = userId;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyAutoNumberAsync(CancellationToken ct)
|
||||
{
|
||||
var entries = ChangeTracker
|
||||
.Entries<IHasAutoNumber>()
|
||||
.Where(e => e.State == EntityState.Added && string.IsNullOrEmpty(e.Entity.AutoNumber))
|
||||
.ToList();
|
||||
|
||||
if (!entries.Any()) return;
|
||||
|
||||
var currentTenantId = _currentUserService.TenantId ?? "SYSTEM_GLOBAL_TENANT";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(currentTenantId))
|
||||
{
|
||||
throw new InvalidOperationException("TenantId cannot be null or empty for multi-tenant auto-number sequence generation. Ensure the user session context is valid.");
|
||||
}
|
||||
|
||||
using var scope = _scopeFactory.CreateScope();
|
||||
var generator = scope.ServiceProvider.GetRequiredService<AutoNumberGeneratorService>();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var entityName = entry.Entity.GetType().Name;
|
||||
var shortName = entityName.ToShortNameConsonant(3);
|
||||
|
||||
entry.Entity.AutoNumber = await generator.GenerateNextNumberAsync(
|
||||
tenantId: currentTenantId ?? string.Empty,
|
||||
entityName: entityName,
|
||||
prefixTemplate: $"{shortName}/{{Year}}/",
|
||||
paddingLength: 4,
|
||||
cancellationToken: ct
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyMultiTenant()
|
||||
{
|
||||
var entries = ChangeTracker.Entries<IMultiTenant>()
|
||||
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
|
||||
.ToList();
|
||||
|
||||
if (!entries.Any()) return;
|
||||
|
||||
var currentTenantId = _currentUserService.TenantId;
|
||||
|
||||
if (string.IsNullOrEmpty(currentTenantId))
|
||||
{
|
||||
throw new InvalidOperationException("TenantId not found in the current user session context.");
|
||||
}
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.State == EntityState.Added)
|
||||
{
|
||||
entry.Entity.TenantId = currentTenantId;
|
||||
}
|
||||
else if (entry.State == EntityState.Modified)
|
||||
{
|
||||
var dbTenantId = entry.Property(nameof(IMultiTenant.TenantId)).OriginalValue;
|
||||
if (dbTenantId != null)
|
||||
{
|
||||
if (entry.Entity.TenantId != dbTenantId.ToString() || entry.Entity.TenantId != currentTenantId)
|
||||
{
|
||||
throw new InvalidOperationException("Cross-tenant operation detected or TenantId data corruption attempted.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
if (Database.ProviderName?.Contains("MySql") == true)
|
||||
{
|
||||
foreach (var entity in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
if (entity.GetTableName()!.StartsWith("AspNet"))
|
||||
{
|
||||
foreach (var property in entity.GetProperties().Where(p => p.ClrType == typeof(string)))
|
||||
{
|
||||
property.SetMaxLength(191);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
|
||||
{
|
||||
var type = entityType.ClrType;
|
||||
|
||||
if (typeof(BaseEntity).IsAssignableFrom(type))
|
||||
{
|
||||
modelBuilder.Entity(type)
|
||||
.Property("Id")
|
||||
.HasMaxLength(GlobalConsts.StringLengthId)
|
||||
.IsFixedLength();
|
||||
}
|
||||
|
||||
if (typeof(IHasAudit).IsAssignableFrom(type))
|
||||
{
|
||||
modelBuilder.Entity(type)
|
||||
.Property(nameof(IHasAudit.CreatedBy))
|
||||
.HasMaxLength(GlobalConsts.StringLengthShort);
|
||||
|
||||
modelBuilder.Entity(type)
|
||||
.Property(nameof(IHasAudit.UpdatedBy))
|
||||
.HasMaxLength(GlobalConsts.StringLengthShort);
|
||||
}
|
||||
|
||||
if (typeof(IMultiTenant).IsAssignableFrom(type))
|
||||
{
|
||||
modelBuilder.Entity(type)
|
||||
.Property(nameof(IMultiTenant.TenantId))
|
||||
.HasMaxLength(GlobalConsts.StringLengthShort);
|
||||
}
|
||||
|
||||
var hasDeleted = typeof(IHasIsDeleted).IsAssignableFrom(type);
|
||||
var isMultiTenant = typeof(IMultiTenant).IsAssignableFrom(type);
|
||||
|
||||
if (hasDeleted || isMultiTenant)
|
||||
{
|
||||
modelBuilder.Entity(type).HasQueryFilter(GetGlobalFilters(type, hasDeleted, isMultiTenant));
|
||||
}
|
||||
}
|
||||
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
}
|
||||
|
||||
|
||||
private System.Linq.Expressions.LambdaExpression GetGlobalFilters(Type type, bool hasDeleted, bool isMultiTenant)
|
||||
{
|
||||
var parameter = System.Linq.Expressions.Expression.Parameter(type, "e");
|
||||
System.Linq.Expressions.Expression? combinedBody = null;
|
||||
|
||||
if (hasDeleted)
|
||||
{
|
||||
var property = System.Linq.Expressions.Expression.Property(parameter, nameof(IHasIsDeleted.IsDeleted));
|
||||
var falseConstant = System.Linq.Expressions.Expression.Constant(false);
|
||||
combinedBody = System.Linq.Expressions.Expression.Equal(property, falseConstant);
|
||||
}
|
||||
|
||||
if (isMultiTenant)
|
||||
{
|
||||
var tenantProperty = System.Linq.Expressions.Expression.Property(parameter, nameof(IMultiTenant.TenantId));
|
||||
|
||||
var dbContextInstance = System.Linq.Expressions.Expression.Constant(this);
|
||||
|
||||
var currentTenantIdProperty = System.Linq.Expressions.Expression.Property(dbContextInstance, nameof(CurrentTenantId));
|
||||
|
||||
var tenantComparison = System.Linq.Expressions.Expression.Equal(tenantProperty, currentTenantIdProperty);
|
||||
|
||||
combinedBody = combinedBody == null
|
||||
? tenantComparison
|
||||
: System.Linq.Expressions.Expression.AndAlso(combinedBody, tenantComparison);
|
||||
}
|
||||
|
||||
return System.Linq.Expressions.Expression.Lambda(combinedBody!, parameter);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user