Tìm hiểu về EF Core interceptors trong .NET 10 – tất cả 7 loại, cách đăng ký chúng, và cách thêm các trường audit, soft deletes, và người dùng hiện tại mà không phá vỡ dependency injection.
Một EF Core interceptor là một lớp (class) được đưa vào pipeline thực thi của Entity Framework Core để bạn có thể quan sát, thay đổi hoặc hủy bỏ những gì EF Core sắp thực hiện, như lưu thay đổi hoặc chạy lệnh SQL, mà không cần chạm vào code đã kích hoạt nó. Hãy nghĩ nó như middleware cho các lệnh gọi database. Trường hợp sử dụng phổ biến nhất là tự động đóng dấu các trường audit (CreatedAt, UpdatedAt, CreatedBy) trên mỗi lần lưu.
Mô hình tư duy này là toàn bộ bài viết trong một câu. Phần còn lại là học loại interceptor nào trong bảy loại để sử dụng, cách đăng ký chúng, và một điều mà hầu hết mọi người đều gặp phải: lấy người dùng hiện tại (một scoped service) vào interceptor mà không phá vỡ dependency injection. Tôi đã thử nghiệm mọi thứ ở đây trên .NET 10 với EF Core 10.0.0. Bắt đầu thôi.
Tóm tắt nhanh. EF Core có 7 interceptor interface. Bạn sẽ sử dụng
ISaveChangesInterceptor90% thời gian (các trường audit, soft deletes) vàIDbCommandInterceptorcho công việc cấp SQL (ghi nhật chậm, thêm hints). Đăng ký chúng vớioptionsBuilder.AddInterceptors(...). Để inject một scoped service như người dùng hiện tại, đăng ký interceptor làScopedvà kết nối thông quaAddDbContext((sp, options) => ...)overload. Interceptor cũng có thể hủy các thao tác thông quaInterceptionResult.Suppress(). Chỉ sử dụng interceptor khi hành vi là cross-cutting và ở cấp cơ sở hạ tầng. Nếu đó là business logic, sử dụng domain event. Nếu đó là read filtering, sử dụng global query filter.
Mục lục
Interceptor trong EF Core là gì?
Interceptor là một hook vào pipeline nội bộ của EF Core. Mỗi khi EF Core thực hiện một hành động có ý nghĩa, như mở kết nối, xây dựng lệnh SQL, hoặc gọi SaveChanges, nó cho các interceptor đã đăng ký cơ hội chạy code ngay trước và ngay sau thao tác đó.
Điều khiến interceptor khác với logging đơn giản là chúng có thể thay đổi kết quả. Logging chỉ quan sát. Một interceptor có thể sửa đổi SQL, chặn toàn bộ lệnh, thay đổi kết quả mà EF Core thấy, hoặc ngăn một exception được ném ra. Sức mạnh đó là lý do chúng là công cụ phù hợp cho các vấn đề database cross-cutting, và là lý do bạn phải cẩn thận với chúng.
Đây là sự khác biệt chính mà hầu hết các tutorial bỏ qua. Một interceptor chạy ở cấp cơ sở hạ tầng, bên dưới business logic của bạn. Nó chạy cho mọi SaveChanges từ mọi đường dẫn code, dù lệnh gọi đến từ controller, background job, hay unit test. Đó chính xác là những gì bạn muốn cho việc đóng dấu audit, và chính xác là những gì bạn không muốn cho các business rule chỉ nên chạy trong các tình huống cụ thể.
Các loại interceptor trong EF Core là gì?
EF Core 10 cung cấp bảy interceptor interface. Hầu hết các bài viết chỉ cho bạn thấy SaveChangesInterceptor, nhưng biết bộ đầy đủ sẽ ngăn bạn sử dụng sai loại. Đây là bản đồ đầy đủ:
| Interceptor | Nó hook vào đâu | Sử dụng khi |
|---|---|---|
ISaveChangesInterceptor |
SaveChanges / SaveChangesAsync |
Các trường audit, soft deletes, domain events, outbox pattern |
IDbCommandInterceptor |
Lệnh SQL, trước và sau khi thực thi | Ghi nhật chậm, thêm query hints, sửa đổi SQL |
IDbConnectionInterceptor |
Mở và đóng kết nối | Chuỗi kết nối cho từng tenant, lấy access token |
IDbTransactionInterceptor |
Bắt đầu, commit, rollback, savepoints | Ghi nhật hoặc hành vi giao dịch tùy chỉnh |
IMaterializationInterceptor |
Các thực thể được tạo từ kết quả truy vấn | Đặt các thuộc tính chưa mapping, inject services vào entities |
IQueryExpressionInterceptor |
Cây biểu thức LINQ trước khi biên dịch | Inject ordering hoặc filters vào mọi truy vấn |
IIdentityResolutionInterceptor |
Xung đột identity khi theo dõi entities | Hợp nhất hai thực thể được theo dõi với cùng khóa |
Một vài sự thật值得 xác định, vì chúng ảnh hưởng đến cách bạn đăng ký và tái sử dụng interceptors:
- Database interception chỉ dành cho relational. Command, connection, và transaction interceptors chỉ hoạt động với các relational provider (SQL Server, PostgreSQL, SQLite). Chúng không chạy trên in-memory provider.
- 3 trong 7 là singleton.
IMaterializationInterceptor,IQueryExpressionInterceptor, vàIIdentityResolutionInterceptorthực hiệnISingletonInterceptor. EF Core chia sẻ một instance duy nhất cho mọiDbContext, vì vậy chúng phải là stateless và thread-safe. Điều này quan trọng sau này khi nói về dependency injection. - Bốn interceptor không phải singleton đi kèm với base class no-op.
SaveChangesInterceptor,DbCommandInterceptor,DbConnectionInterceptor, vàDbTransactionInterceptorđã thực hiện mọi phương thức như pass-through. Kế thừa từ base class và chỉ ghi đè một hoặc hai phương thức bạn quan tâm. Ba singleton interceptor không có base class, vì vậy bạn thực hiện interface trực tiếp. - Có thể nói là có interceptor thứ tám.
IInstantizationBindingInterceptorlà hook cấp thấp hơn để tùy chỉnh cách EF Core xây dựng các thực thể trong quá trình model building. Nó nằm ngoài pipeline hàng ngày và bạn hiếm khi sử dụng nó, vì vậy bảy loại trên là bộ值得 biết.
Tôi đăng ký EF Core interceptor như thế nào?
Bạn đăng ký interceptors với optionsBuilder.AddInterceptors(...) khi cấu hình DbContext. Có hai nơi để làm điều này, và chúng hoạt động hơi khác nhau.
Thứ nhất là bên trong OnConfiguring trên context:
public class AppDbContext : DbContext
{
private static readonly SlowQueryInterceptor _slowQueryInterceptor = new();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.AddInterceptors(_slowQueryInterceptor);
}
Chú ý trường static readonly. Đối với interceptor stateless, bạn muốn một shared instance duy nhất, không phải một instance mới mỗi context. Tôi sẽ giải thích tại sao điều đó quan trọng trong phần dependency injection.
Địa điểm thứ hai, và là địa điểm bạn sẽ sử dụng trong ứng dụng ASP.NET Core thực tế, là trong AddDbContext tại Program.cs:
builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
.AddInterceptors(new SlowQueryInterceptor()));
Một chi tiết hữu ích: OnConfiguring vẫn chạy ngay cả khi bạn sử dụng AddDbContext. EF Core gọi cả hai. Vì vậy OnConfiguring là nơi tốt để đặt cấu hình nên áp dụng bất kể context được xây dựng như thế nào, bao gồm từ design-time factory trong quá trình migrations.
Tôi thêm các trường audit với SaveChangesInterceptor như thế nào?
Đây là trường hợp sử dụng bán chạy nhất của interceptors. Bạn muốn CreatedAtUtc, UpdatedAtUtc, CreatedBy, và UpdatedBy được tự động điền trên mỗi thực thể, mà không cần nhớ đặt chúng trong mỗi handler.
Bắt đầu với hai marker interface. Bất kỳ thực thể nào thực hiện chúng đều chọn tham gia hành vi:
public interface IAuditable
{
DateTime CreatedAtUtc { get; set; }
string? CreatedBy { get; set; }
DateTime? UpdatedAtUtc { get; set; }
string? UpdatedBy { get; set; }
}
public interface ISoftDeletable
{
bool IsDeleted { get; set; }
DateTime? DeletedAtUtc { get; set; }
}
Một thực thể Product chọn tham gia cả hai sẽ trông như thế này:
public class Product : IAuditable, ISoftDeletable
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public DateTime CreatedAtUtc { get; set; }
public string? CreatedBy { get; set; }
public DateTime? UpdatedAtUtc { get; set; }
public string? UpdatedBy { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedAtUtc { get; set; }
}
Bây giờ là interceptor. Nó kế thừa từ SaveChangesInterceptor và ghi đè các hook SavingChanges, chạy ngay trước khi EF Core gửi bất kỳ thứ gì đến database. Dependency ICurrentUser được inject thông qua constructor:
public sealed class AuditableInterceptor(ICurrentUser currentUser) : SaveChangesInterceptor
{
public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
ApplyAudit(eventData.Context);
return base.SavingChanges(eventData, result);
}
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = default)
{
ApplyAudit(eventData.Context);
return base.SavingChangesAsync(eventData, result, cancellationToken);
}
private void ApplyAudit(DbContext? context)
{
if (context is null)
{
return;
}
// Đọc ChangeTracker.Entries<T>() đã buộc change detection
// khi auto-detect được bật (mặc định), nhưng gọi DetectChanges
// rõ ràng giữ cho audit chính xác ngay cả khi một đường dẫn code
// đã tắt auto-detect. Đây là những gì EF Core audit sample làm.
context.ChangeTracker.DetectChanges();
var now = DateTime.UtcNow;
var user = currentUser.UserId ?? "system";
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
if (entry.State == EntityState.Added)
{
entry.Entity.CreatedAtUtc = now;
entry.Entity.CreatedBy = user;
}
else if (entry.State == EntityState.Modified)
{
entry.Entity.UpdatedAtUtc = now;
entry.Entity.UpdatedBy = user;
}
}
foreach (var entry in context.ChangeTracker.Entries<ISoftDeletable>())
{
if (entry.State == EntityState.Deleted)
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
entry.Entity.DeletedAtUtc = now;
}
}
}
}
Hai điều đang xảy ra trong ApplyAudit. Vòng lặp đầu tiên đóng dấu các trường audit dựa trên việc thực thể là mới hay đã thay đổi. Vòng lặp thứ hai là thủ thuật soft-delete: khi thứ gì đó được đánh dấu để xóa, tôi lật trạng thái từ Deleted trở lại Modified và đặt cờ thay vì. EF Core sau đó gửi lệnh UPDATE thay vì DELETE, vì vậy hàng vẫn ở trong bảng.
Tại sao bạn phải ghi đè cả Sync và Async
Nhìn lại interceptor. Tôi đã ghi đè cả SavingChanges và SavingChangesAsync. Đây là bug im lặng cắn mọi người. Nếu bạn chỉ ghi đè phiên bản async và một đường dẫn code nào đó gọi phiên bản sync SaveChanges(), các trường audit của bạn sẽ không bao giờ được đặt, và không có lỗi nào. EF Core chỉ gọi phương thức bạn không ghi đè, đó là base class no-op.
Quy tắc: thực hiện cả hai trừ khi bạn đã cấm hoàn toàn một trong hai. Một số đội ném từ sync SavingChanges để buộc mọi lệnh gọi phải đi async. Đó là lựa chọn hợp lệ, nhưng hãy thực hiện một cách có chủ đích.
Ẩn các hàng soft-deleted khi đọc
Interceptor xử lý phía ghi. Để ngăn các hàng đã xóa xuất hiện trong truy vấn, kết hợp nó với global query filter trong OnModelCreating:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasQueryFilter(p => !p.IsDeleted);
}
Bây giờ context.Products.ToList() tự động loại trừ các hàng soft-deleted. Interceptor đặt cờ, query filter ẩn hàng. Chúng là hai nửa của một tính năng.
Tôi inject người dùng hiện tại vào interceptor như thế nào?
Đây là câu hỏi khiến hầu hết mọi người vấp ngã, và là phần mà các tutorial cạnh tranh bỏ qua hoặc sai. AuditableInterceptor của bạn cần ICurrentUser. Service này là scoped: nó đọc người dùng từ HTTP request hiện tại, vì vậy nó thay đổi mỗi request. Interceptors và scoped services không mix nhau một cách tình cờ.
Cách sai, mà bạn sẽ thấy trong nhiều blog posts, là đăng ký interceptor là singleton:
// Sai. Singleton interceptor không thể phụ thuộc vào scoped service.
builder.Services.AddSingleton<AuditableInterceptor>();
Singleton được tạo một lần và tồn tại mãi. Nó không thể giữ ICurrentUser cho mỗi request, vì vậy bạn sẽ nhận lỗi captive-dependency hoặc user cũ sai cho mọi request sau request đầu tiên.
Mẫu đúng là đăng ký interceptor là scoped và phân giải thông qua AddDbContext overload cung cấp service provider:
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, CurrentUser>();
builder.Services.AddScoped<AuditableInterceptor>();
builder.Services.AddDbContext<AppDbContext>((serviceProvider, options) =>
options
.UseNpgsql(builder.Configuration.GetConnectionString("Default"))
.AddInterceptors(serviceProvider.GetRequiredService<AuditableInterceptor>()));
Vì AddDbContext đăng ký context và options của nó là scoped, lambda options chạy mỗi request với service provider riêng của request đó. Vì vậy GetRequiredService<AuditableInterceptor>() trả về interceptor mới mà ICurrentUser được inject thuộc về request hiện tại. Thời gian sống khớp nhau.
Một ngoại lệ: nếu bạn sử dụng
AddDbContextPoolthay vìAddDbContext, options được xây dựng một lần và chia sẻ trong pool, vì vậy scoped interceptor sẽ không phân giải mỗi request. Với pooling, giữ interceptor stateless và sử dụng scoped services thông quaeventData.Contextbên trong phương thức.
Triển khai ICurrentUser là thông thường:
public interface ICurrentUser
{
string? UserId { get; }
}
public sealed class CurrentUser(IHttpContextAccessor accessor) : ICurrentUser
{
public string? UserId =>
accessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
}
Mẫu đăng ký scoped này an toàn cho SaveChangesInterceptor, IDbCommandInterceptor, và các interceptor không phải singleton khác. Nó không an toàn cho ba singleton interceptors (materialization, query expression, identity resolution). Truyền một instance mới của một trong những loại đó mỗi lần buộc EF Core xây dựng service provider nội bộ mới mỗi request, cuối cùng kích hoạt ManyServiceProvidersCreatedWarning và làm giảm hiệu suất. Đối với chúng, tái sử dụng một static instance duy nhất, và nếu chúng cần scoped service, sử dụng thông qua eventData.Context.
Khi nào bạn nên sử dụng interceptor (và khi nào không)?
Đây là phần mà tôi ước nhiều bài viết hơn có. Interceptors mạnh mẽ, vì vậy chúng bị lạm dụng. Rất nhiều logic kết thúc trong interceptor thuộc về nơi sạch hơn hơn. Đây là cách tôi quyết định:
| Bạn muốn… | Công cụ tốt nhất | Tại sao không phải interceptor |
|---|---|---|
Đóng dấu CreatedAt / UpdatedAt trên mỗi lần lưu |
SaveChangesInterceptor |
Đây là trường hợp fits điển hình, hãy sử dụng nó |
| Chuyển đổi xóa thành soft delete | SaveChangesInterceptor |
Fits điển hình, kết hợp với query filter |
| Ẩn các hàng soft-deleted khỏi đọc | Global query filter | Interceptor xử lý ghi, không phải đọc |
| Chạy side effect sau khi lưu (email, event) | Domain events / MediatR | Business logic không thuộc về cơ sở hạ tầng |
| Lọc mọi truy vấn theo tenant | Global query filter | Đơn giản và an toàn hơn so với sửa đổi expression trees |
| Validate request hoặc định hình HTTP response | Middleware hoặc filters | Interceptors không bao giờ thấy HTTP |
| Ghi nhật SQL chậm hoặc thêm query hint | DbCommandInterceptor |
Fits điển hình, hãy sử dụng nó |
Kiểm tra 30 giây của tôi trước khi viết interceptor: chỉ sử dụng interceptor khi hành vi là cross-cutting, nằm ở cấp cơ sở hạ tầng (không phải business rule), và phải chạy trên mỗi lần lưu hoặc lệnh bất kể đường dẫn code nào kích hoạt nó. Đóng dấu audit đạt cả ba tiêu chí. ‘Gửi email chào mừng khi người dùng đăng ký’ không đạt cả ba: đó là business logic, nó cụ thể, và nó thuộc về domain event handler.
Tôi ghi nhật các truy vấn chậm với command interceptor như thế nào?
Không phải mọi thứ đều là SaveChanges. DbCommandInterceptor nằm ở cấp SQL, vì vậy nó hoàn hảo cho diagnostics như đánh dấu các truy vấn chạy quá lâu. CommandExecutedEventData mang theo Duration bạn có thể kiểm tra:
public sealed class SlowQueryInterceptor(ILogger<SlowQueryInterceptor> logger)
: DbCommandInterceptor
{
private const int SlowQueryThresholdMs = 500;
public override DbDataReader ReaderExecuted(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result)
{
Log(command, eventData);
return base.ReaderExecuted(command, eventData, result);
}
public override ValueTask<DbDataReader> ReaderExecutedAsync(
DbCommand command,
CommandExecutedEventData eventData,
DbDataReader result,
CancellationToken cancellationToken = default)
{
Log(command, eventData);
return base.ReaderExecutedAsync(command, eventData, result, cancellationToken);
}
private void Log(DbCommand command, CommandExecutedEventData eventData)
{
if (eventData.Duration.TotalMilliseconds >= SlowQueryThresholdMs)
{
logger.LogWarning(
"Slow query took {ElapsedMs}ms: {Sql}",
eventData.Duration.TotalMilliseconds,
command.CommandText);
}
}
}
Bất kỳ truy vấn đọc nào chậm hơn 500ms giờ sẽ xuất hiện trong nhật ký của bạn với SQL và thời gian chạy. Đó là cách nhanh hơn để bắt các vấn đề N+1 và thiếu index so với việc chờ người dùng phàn nàn về trang chậm.
Interceptor có thể thay đổi những gì EF Core làm không?
Vâng, và đây là tính năng phân biệt interceptor với logging. Mỗi phương thức interception trả về một InterceptionResult. Trả về nó không thay đổi và EF Core tiến hành bình thường. Nhưng bạn cũng có thể nói EF Core dừng lại.
Một ví dụ thực tế là chặn exception concurrency trên xóa. Nếu hai request xóa cùng một hàng gần như cùng lúc, request thứ hai thất bại vì hàng đã biến mất. Trạng thái cuối cùng là ổn, hàng đã bị xóa, vì vậy bạn có thể nuốt exception cụ thể đó với InterceptionResult.Suppress():
public sealed class SuppressDeleteConcurrencyInterceptor : ISaveChangesInterceptor
{
public InterceptionResult ThrowingConcurrencyException(
ConcurrencyExceptionEventData eventData,
InterceptionResult result)
{
if (eventData.Entries.All(e => e.State == EntityState.Deleted))
{
return InterceptionResult.Suppress();
}
return result;
}
public ValueTask<InterceptionResult> ThrowingConcurrencyExceptionAsync(
ConcurrencyExceptionEventData eventData,
InterceptionResult result,
CancellationToken cancellationToken = default)
=> new(ThrowingConcurrencyException(eventData, result));
}
Có một phương thức anh em, SuppressWithResult, đi xa hơn: nó hủy thao tác và đưa cho EF Core một kết quả bạn cung cấp, đó là cách một bộ nhớ đệm truy vấn đơn giản có thể trả dữ liệu mà không bao giờ chạm vào database.
Một cảnh báo mà tài liệu EF Core đưa ra và tôi sẽ lặp lại: thay đổi hành vi EF Core như vậy rất mạnh mẽ và dễ mắc sai lầm. EF Core có thể hoạt động không đúng nếu bạn đưa cho nó kết quả nó không thể xử lý. Chặn exception khi bạn hiểu chính xác tại sao nó an toàn, không phải như một cách để im lặng lỗi bạn chưa chẩn đoán.
Các vấn đề và lưu ý hiệu suất
Một vài điều tôi đã học được theo cách khó, cộng với những gì tài liệu cảnh báo:
- Interceptors chạy trong hot path.
SavingChangeschạy trên mỗi lần lưu, command interceptors chạy trên mỗi truy vấn. Đừng thực hiện cuộc gọi mạng hoặc gọi dịch vụ bên ngoài trong interceptor. Thao tác chờ đợi bạn. - Instance chia sẻ phải thread-safe. Một instance interceptor duy nhất có thể phục vụ nhiều
DbContextcùng lúc. Nếu nó giữ mutable state, bạn có race condition. Giữ chúng stateless khi có thể. - Gọi
DetectChanges()trước khi đọc các entries. ĐọcChangeTracker.Entries<T>()đã buộc change detection khi auto-detect được bật (mặc định), vì vậy các thực thể đã sửa đổi báo cáo trạng thái đúng. Gọicontext.ChangeTracker.DetectChanges()rõ ràng giữ cho audit chính xác ngay cả khi một đường dẫn code đã tắt auto-detect, và đó chính xác là những gì EF Core audit sample làm. Riêng biệt,ExecuteUpdatevàExecuteDeletebypass change tracker hoàn toàn, vì vậy interceptor không bao giờ thấy những hàng đó. - Ưu tiên base class hơn interface. Kế thừa từ
SaveChangesInterceptorthay vì thực hiệnISaveChangesInterceptorcó nghĩa là bạn ghi đè hai phương thức bạn quan tâm thay vì thực hiện mọi phương thức trên interface.
Những điểm chính
- Một EF Core interceptor là middleware cho các lệnh gọi database của bạn. Nó có thể quan sát, sửa đổi hoặc hủy bỏ các thao tác EF Core từ cấp cơ sở hạ tầng.
- Có 7 loại interceptor, nhưng
ISaveChangesInterceptorvàIDbCommandInterceptorbao gồm gần như mọi thứ bạn sẽ làm. - Đăng ký với
optionsBuilder.AddInterceptors(...), trongOnConfiguringhoặc trongAddDbContext.OnConfiguringluôn chạy, ngay cả vớiAddDbContext. - Để inject scoped service như người dùng hiện tại, đăng ký interceptor là Scoped và kết nối thông qua
AddDbContext((sp, options) => options.AddInterceptors(sp.GetRequiredService<T>())). Không bao giờ đăng ký interceptor như vậy là singleton. - Ghi đè cả sync và async methods, nếu không logic của bạn sẽ chạy im lặng trên đường dẫn bạn đã bỏ qua.
- Chỉ sử dụng interceptor khi hành vi là cross-cutting và ở cấp cơ sở hạ tầng. Business logic thuộc về domain event, read filtering thuộc về query filter.
Câu hỏi thường gặp
01 Interceptor trong EF Core là gì?
Một EF Core interceptor là một lớp hook vào pipeline thực thi của Entity Framework Core để bạn có thể quan sát, sửa đổi hoặc chặn các thao tác như SaveChanges, SQL commands, connections, và transactions. Không giống logging, interceptor có thể sửa đổi thao tác, không chỉ quan sát nó. Trường hợp sử dụng phổ biến nhất là thêm các trường audit tự động trên mỗi lần lưu.
02 Các loại EF Core interceptor khác nhau là gì?
EF Core 10 có bảy interceptor interface: ISaveChangesInterceptor, IDbCommandInterceptor, IDbConnectionInterceptor, IDbTransactionInterceptor, IMaterializationInterceptor, IQueryExpressionInterceptor, và IIdentityResolutionInterceptor. Save-changes và command interceptors bao gồm hầu hết các trường hợp sử dụng thực tế. Materialization, query expression, và identity resolution interceptors là singleton được chia sẻ trên tất cả contexts.
03 Tôi đăng ký EF Core interceptor như thế nào?
Gọi optionsBuilder.AddInterceptors(...) khi cấu hình DbContext. Bạn có thể làm điều này bên trong OnConfiguring override trên context, hoặc trong AddDbContext tại Program.cs. OnConfiguring vẫn chạy ngay cả khi bạn sử dụng AddDbContext, vì vậy đó là nơi tốt cho cấu hình nên áp dụng bất kể context được xây dựng như thế nào.
04 Tôi truy cập người dùng hiện tại hoặc scoped service bên trong interceptor như thế nào?
Đăng ký interceptor là Scoped, sau đó kết nối nó thông qua AddDbContext overload cung cấp service provider: AddDbContext((sp, options) => options.AddInterceptors(sp.GetRequiredService<YourInterceptor>())). Vì AddDbContext là scoped, lambda chạy mỗi request và phân giải interceptor mà scoped services được inject thuộc về request đó. Không đăng ký interceptor như vậy là singleton, vì singleton không thể giữ dependency cho mỗi request.
05 Sự khác biệt giữa interceptor và ghi đè SaveChanges là gì?
Cả hai cho phép bạn chạy logic trước khi lưu. Ghi đè SaveChanges trên DbContext đơn giản hơn và ổn cho một context. Interceptor có thể tái sử dụng trên nhiều contexts, được đăng ký thông qua dependency injection, và có thể hook các sự kiện cấp thấp hơn như SQL commands, connections, và transactions. Sử dụng override cho logic cụ thể context và interceptor cho hành vi cơ sở hạ tầng cross-cutting.
06 EF Core interceptors có thread-safe hay singleton không?
Tùy thuộc vào loại. Materialization, query expression, và identity resolution interceptors thực hiện ISingletonInterceptor và được chia sẻ trên tất cả DbContext instances, vì vậy chúng phải là stateless và thread-safe. Command, connection, transaction, và save-charges interceptors không phải singleton và có thể được đăng ký mỗi scope. Truyền một instance mới của singleton interceptor mỗi request kích hoạt ManyServiceProvidersCreatedWarning và ảnh hưởng hiệu suất.
07 EF Core interceptors có ảnh hưởng hiệu suất không?
Có. Interceptors chạy bên trong hot path, vì vậy SaveChanges interceptor chạy trên mỗi lần lưu và command interceptor chạy trên mỗi truy vấn. Bản thân interception là rẻ, nhưng bất kỳ công việc chậm nào bạn làm bên trong nó, như gọi dịch vụ bên ngoài, sẽ chặn thao tác database. Giữ interceptors nhanh và stateless, và không bao giờ đặt cuộc gọi mạng trong chúng.
08 Interceptor có thể hủy hoặc thay đổi những gì EF Core làm không?
Có. Mỗi phương thức interception trả về InterceptionResult. Trả về InterceptionResult.Suppress() nói EF Core bỏ qua thao tác nó sắp thực hiện, như ném exception concurrency. SuppressWithResult đi xa hơn và đưa cho EF Core kết quả thay thế. Khả năng thay đổi hành vi, không chỉ quan sát nó, là điều chính phân biệt interceptor với logging, nhưng nó nên được sử dụng cẩn thận.
Kết luận
Interceptors là một trong những tính năng EF Core cảm thấy nâng cao cho đến khi mô hình tư duy rõ ràng, và sau đó chúng hiển nhiên: middleware cho database của bạn. Bắt đầu với SaveChangesInterceptor cho các trường audit, vì đó là nơi bạn sẽ cảm nhận lợi ích ngay lập tức. Thêm DbCommandInterceptor khi bạn muốn visibility vào SQL chậm. Nhận dependency injection scoped đúng để người dùng hiện tại chảy vào một cách gọn gàng. Và trước khi bạn viết interceptor, chạy kiểm tra 30 giây, vì nửa thời gian domain event hoặc query filter là ngôi nhà tốt hơn.
Mẫu đầy đủ, với interceptor audit, soft deletes, command interceptor, và registration wiring, nằm trong repo GitHub. Clone nó và đi qua nó.
Chúc bạn Coding 🙂



