Created
June 5, 2025 18:00
-
-
Save EklipZgit/3ba8aa7efd6115f01a3019840b24aab2 to your computer and use it in GitHub Desktop.
Async Only Entity Framework Core DBContext enforcers / loggers.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Common.Logging; | |
using MyCompany.Common.Utilities; | |
using Microsoft.EntityFrameworkCore; | |
using Microsoft.EntityFrameworkCore.Diagnostics; | |
using System; | |
using System.Data.Common; | |
using System.Text.RegularExpressions; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace MyCompany.DbContexts; | |
/// <summary> | |
/// Throw (or just log error in prod) if any code triggers a synchronous database command, eg triggering a .ToList() | |
/// instead of .ToListAsync on IQueryable, or any unexpected LINQ operation that incidentally synchronously materializes the query from DB. | |
/// This one definitely works correctly, it's been in use in exactly this form in prod in another project since 2022. | |
/// The only case for sync io is if you need to retrieve VERY large files from a column in the DB directly, which if you're doing, you | |
/// deserve your fate. There should be zero sync commands ever issued. | |
/// </summary> | |
public class AsyncOnlyCommandInterceptor : DbCommandInterceptor | |
{ | |
// Use your own logging system, this one sucks. Just here for example. | |
private static readonly ILog _log = MyCompanyLogManager.GetLogger(); | |
public bool AllowSynchronous { get; set; } = false; | |
public static ILog Log { get; set; } = _log; | |
public override InterceptionResult<int> NonQueryExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<int> result) | |
{ | |
var msg = $"Synchronous NonQueryExecuting: Sync database access is not allowed. Use the asynchronous EF Core API instead. \r\nQuery: \r\n{command.CommandText} \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult<DbDataReader> ReaderExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result) | |
{ | |
var msg = $"Synchronous ReaderExecuting: Sync database access is not allowed. Use the asynchronous EF Core API instead. \r\nQuery: \r\n{command.CommandText} \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult<object> ScalarExecuting(DbCommand command, CommandEventData eventData, InterceptionResult<object> result) | |
{ | |
var msg = $"Synchronous ScalarExecuting: Sync database access is not allowed. Use the asynchronous EF Core API instead. \r\nQuery: \r\n{command.CommandText} \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
private void ThrowIfNotAllowed(string msg) | |
{ | |
if (!AllowSynchronous) | |
throw new InvalidOperationException(msg); | |
else | |
Log.Error("(NOT THROWN) " + msg); | |
} | |
} | |
/// <summary> | |
/// Detect sync Connection operations. | |
/// Make sure you are doing: | |
/// await using var thingThatImplementsIAsyncDisposable = ....; | |
/// rather than just using var... or using (var ... ) { | |
/// if newing up your own connections and seeing this error / log. | |
/// </summary> | |
public class AsyncOnlyConnectionInterceptor : DbConnectionInterceptor | |
{ | |
private static readonly ILog _log = MyCompanyLogManager.GetLogger(); | |
public bool AllowSynchronous { get; set; } = false; | |
public bool AllowSyncDispose { get; set; } = false; | |
public static ILog Log { get; set; } = _log; | |
public override InterceptionResult ConnectionOpening(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) | |
{ | |
var msg = $"Synchronous ConnectionOpening: Sync database access is not allowed. Use the asynchronous EF Core API instead. You should not be opening EF connections yourself regardless, swap to DI'd context from DbContextPool wherever this is happening. \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult ConnectionClosing(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) | |
{ | |
var msg = $"Synchronous ConnectionClosing: Sync database access is not allowed. Use the asynchronous EF Core API instead. You should not be closing EF connections yourself regardless, swap to DI'd context from DbContextPool wherever this is happening. \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult ConnectionDisposing(DbConnection connection, ConnectionEventData eventData, InterceptionResult result) | |
{ | |
if (AllowSyncDispose) | |
return result; | |
var msg = $"Synchronous ConnectionDisposing: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
private void ThrowIfNotAllowed(string msg) | |
{ | |
if (!AllowSynchronous) | |
throw new InvalidOperationException(msg); | |
else | |
Log.Error("(NOT THROWN) " + msg); | |
} | |
} | |
/// <summary> | |
/// Detect sync Transaction operations. | |
/// Make sure you are doing: | |
/// await using var thingThatImplementsIAsyncDisposable = ....; | |
/// rather than just using var... or using (var ... ) { | |
/// if you see errors / logs from this. | |
/// </summary> | |
public class AsyncOnlyTransactionInterceptor : DbTransactionInterceptor | |
{ | |
private static readonly ILog _log = MyCompanyLogManager.GetLogger(); | |
public static ILog Log { get; set; } = _log; | |
public bool AllowSynchronous { get; set; } = false; | |
public override InterceptionResult<DbTransaction> TransactionStarting( | |
DbConnection connection, | |
TransactionStartingEventData eventData, | |
InterceptionResult<DbTransaction> result) | |
{ | |
var msg = $"Synchronous TransactionStarting: Sync database access is not allowed. Use await ...StartTransactionAsync() instead. \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult TransactionCommitting( | |
DbTransaction transaction, | |
TransactionEventData eventData, | |
InterceptionResult result) | |
{ | |
var msg = $"Synchronous TransactionCommitting: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override DbTransaction TransactionUsed(DbConnection connection, TransactionEventData eventData, DbTransaction result) | |
{ | |
var msg = $"Synchronous TransactionUsed: Sync database access is not allowed. Use await ...UseTransactionAsync() instead. \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
ThrowIfNotAllowed(msg); | |
return result; | |
} | |
public override InterceptionResult TransactionRollingBack( | |
DbTransaction transaction, | |
TransactionEventData eventData, | |
InterceptionResult result) | |
{ | |
var msg = $"(NOT THROWN) Synchronous TransactionRollingBack: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
// This one might be out of our control, not exactly sure how the runtime deals with this in certain cases of thread failure. For now just log to get a better idea? | |
Log.Error(msg); | |
return result; | |
} | |
public override InterceptionResult CreatingSavepoint( | |
DbTransaction transaction, | |
TransactionEventData eventData, | |
InterceptionResult result) | |
{ | |
var msg = $"(NOT THROWN) Synchronous CreatingSavepoint: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
// This one might be out of our control, not exactly sure how the runtime deals with this in certain cases of thread failure. For now just log to get a better idea? | |
Log.Error(msg); | |
return result; | |
} | |
public override InterceptionResult RollingBackToSavepoint( | |
DbTransaction transaction, | |
TransactionEventData eventData, | |
InterceptionResult result) | |
{ | |
var msg = $"(NOT THROWN) Synchronous RollingBackToSavepoint: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
// This one might be out of our control, not exactly sure how the runtime deals with this in certain cases of thread failure. For now just log to get a better idea? | |
Log.Error(msg); | |
return result; | |
} | |
public override InterceptionResult ReleasingSavepoint( | |
DbTransaction transaction, | |
TransactionEventData eventData, | |
InterceptionResult result) | |
{ | |
var msg = $"(NOT THROWN) Synchronous ReleasingSavepoint: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
// This one might be out of our control, not exactly sure how the runtime deals with this in certain cases of thread failure. For now just log to get a better idea? | |
Log.Error(msg); | |
return result; | |
} | |
public override void TransactionFailed(DbTransaction transaction, TransactionErrorEventData eventData) | |
{ | |
var msg = $"(NOT THROWN) Synchronous TransactionFailed: Sync database access is not allowed. Use the asynchronous EF Core API instead. Did you fail to `await using ...`? \r\nStack: \r\n{new System.Diagnostics.StackTrace().ToString()}"; | |
// This one might be out of our control, not exactly sure how the runtime deals with this in certain cases of thread failure. For now just log to get a better idea? | |
Log.Error(msg); | |
} | |
private void ThrowIfNotAllowed(string msg) | |
{ | |
if (!AllowSynchronous) | |
throw new InvalidOperationException(msg); | |
else | |
Log.Error("(NOT THROWN) " + msg); | |
} | |
} | |
public static class AsyncOnlyDbContextBuilderExtensions | |
{ | |
public static bool AllowSyncDispose { get; set; } = false; | |
public static DbContextOptionsBuilder AddMyCompanyAsyncOnlyInterceptors(this DbContextOptionsBuilder builder, bool allowSyncIo = false) | |
{ | |
// TODO: Remove the override line below when sync data accesses are fixed | |
var throwOnSync = MyCompanyEnvironmentHelper.IsDv || MyCompanyEnvironmentHelper.IsLocal || MyCompanyEnvironmentHelper.IsApiTests; | |
builder.AddInterceptors( | |
new AsyncOnlyConnectionInterceptor() { AllowSynchronous = !allowSyncIo, AllowSyncDispose = AllowSyncDispose }, | |
new AsyncOnlyCommandInterceptor() { AllowSynchronous = !allowSyncIo }, | |
new AsyncOnlyTransactionInterceptor() { AllowSynchronous = !allowSyncIo }); | |
return builder; | |
} | |
} | |
public static class Program | |
{ | |
AsyncOnlyDbContextBuilderExtensions.AllowSyncDispose = true; // Can set this for things like Hangfire that dont support async dispose. Dont override if you dont NEED to, prefer always async, all the time. | |
// ... set up your db context registrations, which should have the OnConfiguring below: | |
} | |
public class MyCompanyContext : DbContext | |
{ | |
// ... ctor etc | |
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) | |
{ | |
// throw in local, dev, tests but not in prod etc, so you can monitor in prod to weed out sync access that you've missed in the dev/test throws. | |
var throwOnSync = MyCompanyEnvironmentHelper.IsDv || MyCompanyEnvironmentHelper.IsLocal || MyCompanyEnvironmentHelper.IsApiTests; | |
optionsBuilder | |
//.EnableSensitiveDataLogging() | |
.AddMyCompanyAsyncOnlyInterceptors(throwOnSync); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment