It's possible if you're willing to implement your own version of the ILazyLoader interface which creates an instance of your DbContext on each call to Load/LoadAsync rather than using a shared one.
Below is a minimal example. It's based on the default Microsoft.EntityFrameworkCore.Infrastructure.Internal.LazyLoader.
public class MyLazyLoader<T> : ILazyLoader where T : DbContext
{
private bool _detached;
private QueryTrackingBehavior? _queryTrackingBehavior;
private ConcurrentDictionary<string, bool>? loadedStates;
private readonly ConcurrentDictionary<(object Entity, string NavName), bool> _isLoading = new(NavEntryEqualityComparer.Instance);
private HashSet<string>? _nonLazyNavigations;
protected virtual IDiagnosticsLogger<DbLoggerCategory.Infrastructure> Logger { get; }
public void Dispose()
{
// no need for any special code, we're already disposing our context on every request.
}
public MyLazyLoader(
ICurrentDbContext _currentContext,
IDiagnosticsLogger<DbLoggerCategory.Infrastructure> logger)
{
Logger = logger;
}
public bool IsLoaded(object entity, string navigationName)
{
return loadedStates != null
&& loadedStates.TryGetValue(navigationName, out var loaded)
&& loaded;
}
public void Load(object entity, [CallerMemberName] string navigationName = "")
{
using (var ctx = NewContextOfType())
{
Check.NotNull(entity, nameof(entity));
Check.NotEmpty(navigationName, nameof(navigationName));
var navEntry = (entity, navigationName);
if (_isLoading.TryAdd(navEntry, true))
{
try
{
if (ShouldLoad(entity, navigationName, out var entry, ctx))
{
try
{
entry.Load(
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
? LoadOptions.ForceIdentityResolution
: LoadOptions.None);
}
catch
{
entry.IsLoaded = false;
throw;
}
}
}
finally
{
_isLoading.TryRemove(navEntry, out _);
}
}
}
}
public async System.Threading.Tasks.Task LoadAsync(object entity, CancellationToken cancellationToken, [CallerMemberName] string navigationName = "")
{
using (var ctx = NewContextOfType())
{
Check.NotNull(entity, nameof(entity));
Check.NotEmpty(navigationName, nameof(navigationName));
var navEntry = (entity, navigationName);
if (_isLoading.TryAdd(navEntry, true))
{
try
{
if (ShouldLoad(entity, navigationName, out var entry, ctx))
{
try
{
await entry.LoadAsync(
_queryTrackingBehavior == QueryTrackingBehavior.NoTrackingWithIdentityResolution
? LoadOptions.ForceIdentityResolution
: LoadOptions.None,
cancellationToken).ConfigureAwait(false);
}
catch
{
entry.IsLoaded = false;
throw;
}
}
}
finally
{
_isLoading.TryRemove(navEntry, out _);
}
}
}
}
public void SetLoaded(object entity, [CallerMemberName] string navigationName = "", bool loaded = true)
{
loadedStates ??= new ConcurrentDictionary<string, bool>();
loadedStates[navigationName] = loaded;
}
private bool ShouldLoad(object entity, string navigationName, [NotNullWhen(true)] out NavigationEntry? navigationEntry, T context)
{
if (!_detached && !IsLoaded(entity, navigationName))
{
if (_nonLazyNavigations == null
|| !_nonLazyNavigations.Contains(navigationName))
{
if (context!.ChangeTracker.LazyLoadingEnabled)
{
navigationEntry = context.Entry(entity).Navigation(navigationName);
if (!navigationEntry.IsLoaded)
{
Logger.NavigationLazyLoading(context, entity, navigationName);
return true;
}
}
}
}
navigationEntry = null;
return false;
}
internal T NewContextOfType()
{
return Activator.CreateInstance<T>();
}
}
internal sealed class NavEntryEqualityComparer : IEqualityComparer<(object Entity, string NavigationName)>
{
public static readonly NavEntryEqualityComparer Instance = new();
private NavEntryEqualityComparer()
{
}
public bool Equals((object Entity, string NavigationName) x, (object Entity, string NavigationName) y)
=> ReferenceEquals(x.Entity, y.Entity)
&& string.Equals(x.NavigationName, y.NavigationName, StringComparison.Ordinal);
public int GetHashCode((object Entity, string NavigationName) obj)
=> HashCode.Combine(RuntimeHelpers.GetHashCode(obj.Entity), obj.NavigationName.GetHashCode());
}
internal static class Check
{
[ContractAnnotation("value:null => halt")]
[return: System.Diagnostics.CodeAnalysis.NotNull]
public static T NotNull<T>([NoEnumeration][AllowNull][System.Diagnostics.CodeAnalysis.NotNull] T value, [InvokerParameterName] string parameterName)
{
if (value is null)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentNullException(parameterName);
}
return value;
}
[ContractAnnotation("value:null => halt")]
public static IReadOnlyList<T> NotEmpty<T>(
[NotNull] IReadOnlyList<T>? value,
[InvokerParameterName] string parameterName)
{
NotNull(value, parameterName);
if (value.Count == 0)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(AbstractionsStrings.CollectionArgumentIsEmpty(parameterName));
}
return value;
}
[ContractAnnotation("value:null => halt")]
public static string NotEmpty([NotNull] string? value, [InvokerParameterName] string parameterName)
{
if (value is null)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentNullException(parameterName);
}
if (value.Trim().Length == 0)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName));
}
return value;
}
public static string? NullButNotEmpty(string? value, [InvokerParameterName] string parameterName)
{
if (value is not null && value.Length == 0)
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(AbstractionsStrings.ArgumentIsEmpty(parameterName));
}
return value;
}
public static IReadOnlyList<T> HasNoNulls<T>(
[NotNull] IReadOnlyList<T>? value,
[InvokerParameterName] string parameterName)
where T : class
{
NotNull(value, parameterName);
if (value.Any(e => e == null))
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(parameterName);
}
return value;
}
public static IReadOnlyList<string> HasNoEmptyElements(
[NotNull] IReadOnlyList<string>? value,
[InvokerParameterName] string parameterName)
{
NotNull(value, parameterName);
if (value.Any(s => string.IsNullOrWhiteSpace(s)))
{
NotEmpty(parameterName, nameof(parameterName));
throw new ArgumentException(AbstractionsStrings.CollectionArgumentHasEmptyElements(parameterName));
}
return value;
}
[Conditional("DEBUG")]
public static void DebugAssert([DoesNotReturnIf(false)] bool condition, string message)
{
if (!condition)
{
throw new UnreachableException($"Check.DebugAssert failed: {message}");
}
}
[Conditional("DEBUG")]
[DoesNotReturn]
public static void DebugFail(string message)
=> throw new UnreachableException($"Check.DebugFail failed: {message}");
}
Then, in your DbContext's OnConfiguring method, replace the default ILazyLoader with your own:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.ReplaceService<ILazyLoader, MyLazyLoader<YourDbContext>>()
/* other configuration... */;
}