Reputation: 4065
I'm having issues with Entity Framework and multiple threads and I am wondering if there is a solution that keeps the ability to lazy load. From my understanding the data context is not thread safe which is why when I have multiple threads using the same data context I get various data readers error. The solution to this problem is to use a separate data context for each connection to the database and then destroy the data context. Unfortunately destroying my data context then prevents me from doing lazy loading.
Is there a pattern to allow me to have a shared context across my application, but still properly handle multiple threads?
Upvotes: 9
Views: 4649
Reputation: 31
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... */;
}
Upvotes: 0
Reputation: 364249
No, there is no such solution. Your choices in multithreaded application are:
Doing the second approach with proxied attached entities is way to disaster. It would require to detect all hidden interactions with the context and make related code also synchronized. You will probably end with single threaded process running in multiple switching threads.
Upvotes: 10