mirror of
https://github.com/rmcrackan/Libation.git
synced 2026-05-01 20:23:13 -04:00
Enforce sequential access to DbContext.
This commit is contained in:
@@ -9,7 +9,7 @@ namespace ApplicationServices
|
||||
{
|
||||
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
|
||||
public static LibationContext GetContext()
|
||||
=> LibationContext.Create(SqliteStorage.ConnectionString);
|
||||
=> InstanceQueue<LibationContext>.WaitToCreateInstance(() => LibationContext.Create(SqliteStorage.ConnectionString));
|
||||
|
||||
/// <summary>Use for full library querying. No lazy loading</summary>
|
||||
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)
|
||||
|
||||
103
Source/DataLayer/InstanceQueue.cs
Normal file
103
Source/DataLayer/InstanceQueue.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
#nullable enable
|
||||
namespace DataLayer;
|
||||
|
||||
/// <summary> Notifies clients that the object is being disposed. </summary>
|
||||
public interface INotifyDisposed : IDisposable
|
||||
{
|
||||
/// <summary> Event raised when the object is disposed. </summary>
|
||||
event EventHandler? ObjectDisposed;
|
||||
}
|
||||
|
||||
/// <summary> Creates a single instance of <typeparamref name="TDisposable"/> at a time, blocking subsequent creations until the previous creations are disposed. </summary>
|
||||
public static class InstanceQueue<TDisposable> where TDisposable : INotifyDisposed
|
||||
{
|
||||
/// <summary> Synchronization object for access to <see cref="LastInLine"/>"/> </summary>
|
||||
private static Lock Locker { get; } = new();
|
||||
/// <summary> Ticket holder for the last instance creator in line. </summary>
|
||||
private static Ticket? LastInLine { get; set; }
|
||||
|
||||
/// <summary> Waits for all previously created instances of <typeparamref name="TDisposable"/> to be disposed, then creates and returns a new instance of <typeparamref name="TDisposable"/> using the provided <paramref name="creator"/> factory. </summary>
|
||||
public static TDisposable WaitToCreateInstance(Func<TDisposable> creator)
|
||||
{
|
||||
Ticket ticket;
|
||||
lock (Locker)
|
||||
{
|
||||
ticket = LastInLine = new Ticket(creator, LastInLine);
|
||||
}
|
||||
|
||||
return ticket.Fulfill();
|
||||
}
|
||||
|
||||
/// <summary> A queue ticket for an instance creator. </summary>
|
||||
/// <param name="creator">Factory to create a new instance of <typeparamref name="TDisposable"/></param>
|
||||
/// <param name="inFront">The ticket immediately in preceding this new ticket. This new ticket must wait for <paramref name="inFront"/> to signal its instance has been disposed before creating a new instance of <typeparamref name="TDisposable"/></param>
|
||||
private class Ticket(Func<TDisposable> creator, Ticket? inFront) : IDisposable
|
||||
{
|
||||
/// <summary> Factory to create a new instance of <typeparamref name="TDisposable"/> </summary>
|
||||
private Func<TDisposable> Creator { get; } = creator;
|
||||
/// <summary> Ticket immediately in front of this one. This instance must wait for <see cref="InFront"/> to signal its instance has been disposed before creating a new instance of <typeparamref name="TDisposable"/></summary>
|
||||
private Ticket? InFront { get; } = inFront;
|
||||
/// <summary> Wait handle to signal when this ticket's created instance is disposed </summary>
|
||||
private EventWaitHandle WaitHandle { get; } = new(false, EventResetMode.ManualReset);
|
||||
/// <summary> This ticket's created instance of <typeparamref name="TDisposable"/> </summary>
|
||||
private TDisposable? CreatedInstance { get; set; }
|
||||
|
||||
/// <summary> Disposes of this ticket and every ticket queued in front of it. </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
WaitHandle.Dispose();
|
||||
InFront?.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Waits for the <see cref="InFront"/> ticket's instance to be disposed, then creates and returns a new instance of <typeparamref name="TDisposable"/> using the <see cref="Creator"/> factory.
|
||||
/// </summary>
|
||||
public TDisposable Fulfill()
|
||||
{
|
||||
#if DEBUG
|
||||
var sw = Stopwatch.StartNew();
|
||||
#endif
|
||||
//Wait for the previous ticket's instance to be disposed, then dispose of the previous ticket.
|
||||
InFront?.WaitHandle.WaitOne(Timeout.Infinite);
|
||||
InFront?.Dispose();
|
||||
#if DEBUG
|
||||
sw.Stop();
|
||||
Debug.WriteLine($"Waited {sw.ElapsedMilliseconds}ms to create instance of {typeof(TDisposable).Name}");
|
||||
#endif
|
||||
CreatedInstance = Creator();
|
||||
CreatedInstance.ObjectDisposed += CreatedInstance_ObjectDisposed;
|
||||
return CreatedInstance;
|
||||
}
|
||||
|
||||
private void CreatedInstance_ObjectDisposed(object? sender, EventArgs e)
|
||||
{
|
||||
Debug.WriteLine($"{typeof(TDisposable).Name} Disposed");
|
||||
if (CreatedInstance is not null)
|
||||
{
|
||||
CreatedInstance.ObjectDisposed -= CreatedInstance_ObjectDisposed;
|
||||
CreatedInstance = default;
|
||||
}
|
||||
|
||||
lock (Locker)
|
||||
{
|
||||
if (this == LastInLine)
|
||||
{
|
||||
//There are no ticket holders waiting after this one.
|
||||
//This ticket is fulfilled and will never be waited on.
|
||||
LastInLine = null;
|
||||
Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
//Signal the that this ticket has been fulfilled so that
|
||||
//the next ticket in line may proceed.
|
||||
WaitHandle.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using DataLayer.Configurations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DataLayer
|
||||
{
|
||||
public class LibationContext : DbContext
|
||||
public class LibationContext : DbContext, INotifyDisposed
|
||||
{
|
||||
// IMPORTANT: USING DbSet<>
|
||||
// ========================
|
||||
@@ -25,6 +27,18 @@ namespace DataLayer
|
||||
public DbSet<Category> Categories { get; private set; }
|
||||
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
|
||||
|
||||
public event EventHandler ObjectDisposed;
|
||||
public override void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
await base.DisposeAsync();
|
||||
ObjectDisposed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public static LibationContext Create(string connectionString)
|
||||
{
|
||||
var factory = new LibationContextFactory();
|
||||
|
||||
Reference in New Issue
Block a user