Việc triển khai DbContext tùy chỉnh cho phép tích hợp các mẫu thiết kế như Unit of Work và xử lý Domain Events trực tiếp trong quá trình lưu thay đổi. Dưới đây là định nghĩa cho lớp ngữ cảnh cơ sở dữ liệu chính.
using Microsoft.EntityFrameworkCore;
using MyGame.Domain.Core;
using MyGame.Domain.Events;
using MyGame.Infrastructure.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
namespace MyGame.Infrastructure.Data
{
public class AppDbContext : DbContext, IUnitOfWork
{
private readonly IInternalEventBus _internalBus = default!;
private readonly IExternalEventBus _externalBus = default!;
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public AppDbContext(
DbContextOptions<AppDbContext> options,
IInternalEventBus internalBus,
IExternalEventBus externalBus) : base(options)
{
_internalBus = internalBus;
_externalBus = externalBus;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Áp dụng tự động các cấu hình từ assembly hiện tại
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
}
public DbSet<GameEntity> Games { get; set; }
public DbSet<PlayerEntity> Players { get; set; }
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// 1. Thu thập các domain events từ các Aggregate Root
await DispatchDomainEventsBeforeSavingAsync(cancellationToken);
// 2. Lưu thay đổi vào cơ sở dữ liệu
var result = await base.SaveChangesAsync(cancellationToken);
// 3. Xử lý các sự kiện tích hợp sau khi lưu thành công
await DispatchDomainEventsAfterSavingAsync(cancellationToken);
return result;
}
private async Task DispatchDomainEventsBeforeSavingAsync(CancellationToken cancellationToken)
{
var entities = ChangeTracker.Entries<IAggregateRoot>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.Select(e => e.Entity);
var internalTasks = new List<Task>();
foreach (var entity in entities)
{
var events = entity.GetInternalEvents();
entity.ClearInternalEvents();
if (_internalBus != null)
{
foreach (var @event in events)
{
internalTasks.Add(_internalBus.PublishAsync(@event));
}
}
}
await Task.WhenAll(internalTasks);
}
private async Task DispatchDomainEventsAfterSavingAsync(CancellationToken cancellationToken)
{
var entities = ChangeTracker.Entries<IAggregateRoot>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
.Select(e => e.Entity);
var externalTasks = new List<Task>();
foreach (var entity in entities)
{
var events = entity.GetExternalEvents();
entity.ClearExternalEvents();
if (_externalBus != null)
{
foreach (var @event in events)
{
externalTasks.Add(_externalBus.PublishAsync(@event));
}
}
}
await Task.WhenAll(externalTasks);
}
}
}
Để hỗ trợ việc tạo migration tại thời điểm thiết kế (Design Time) cho các loại cơ sở dữ liệu khác nhau, chúng ta triển khai interface IDesignTimeDbContextFactory<T>.
1. Cấu hình cho MySQL
Đối với MySQL, cần cài đặt gói Pomelo.EntityFrameworkCore.MySql và thiết lập Factory như sau:
Tệp .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.25" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyGame.Infrastructure.Data\MyGame.Infrastructure.Data.csproj" />
</ItemGroup>
</Project>
Lớp DesignTime Factory:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using MyGame.Infrastructure.Data;
using System;
namespace MyGame.Infrastructure.Persistence.MySql
{
public class MySqlDesignFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] parameters)
{
var connectionString = "Server=localhost;Database=GameDb;uid=root;pwd=secret;";
var serverVersion = new MySqlServerVersion(new Version(8, 0, 31));
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseMySql(connectionString, serverVersion,
mysqlOptions => mysqlOptions.MigrationsAssembly(typeof(MySqlDesignFactory).Assembly.FullName));
return new AppDbContext(optionsBuilder.Options);
}
}
}
2. Cấu hình cho SQLite
SQLite thường được sử dụng cho môi trường phát triển nhẹ nhàng.
Tệp .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.25" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.25" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyGame.Infrastructure.Data\MyGame.Infrastructure.Data.csproj" />
</ItemGroup>
</Project>
Lớp DesignTime Factory:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using MyGame.Infrastructure.Data;
using System.IO;
namespace MyGame.Infrastructure.Persistence.Sqlite
{
public class SqliteDesignFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] parameters)
{
// Lưu trữ DB trong thư mục dữ liệu ứng dụng cục bộ
var folder = Environment.SpecialFolder.LocalApplicationData;
var directory = Path.Combine(Environment.GetFolderPath(folder), "MyGameApp");
Directory.CreateDirectory(directory);
var dbPath = Path.Combine(directory, "game_data.db");
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
optionsBuilder.UseSqlite($"Data Source={dbPath}",
x => x.MigrationsAssembly(typeof(SqliteDesignFactory).Assembly.FullName));
return new AppDbContext(optionsBuilder.Options);
}
}
}
3. Cấu hình cho SQL Server
Cấu hình dành cho SQL Server truyền thống.
Tệp .csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.25" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.25" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyGame.Infrastructure.Data\MyGame.Infrastructure.Data.csproj" />
</ItemGroup>
</Project>
Lớp DesignTime Factory:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using MyGame.Infrastructure.Data;
namespace MyGame.Infrastructure.Persistence.SqlServer
{
public class SqlServerDesignFactory : IDesignTimeDbContextFactory<AppDbContext>
{
public AppDbContext CreateDbContext(string[] parameters)
{
var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
var connectionString = "Server=(localdb)\\mssqllocaldb;Database=GameSqlServerDb;Trusted_Connection=True;";
optionsBuilder.UseSqlServer(connectionString,
x => x.MigrationsAssembly(typeof(SqlServerDesignFactory).Assembly.FullName));
return new AppDbContext(optionsBuilder.Options);
}
}
}
Tạo Script Migration
Sau khi định nghĩa Factory và tạo các file migration, bạn có thể sử dụng các lệnh CLI sau để xuất script SQL:
# Tạo script SQL từ migration mới nhất
dotnet ef migrations script
# Tạo script cho một phiên bản migration cụ thể
dotnet ef migrations script 20231001000000_AddInitialSchema
# Tạo script có thể chạy nhiều lần (Idempotent)
dotnet ef migrations script --idempotent