diff --git a/Casbin.Persist.Adapter.EFCore.UnitTest/DbContextFactoryTest.cs b/Casbin.Persist.Adapter.EFCore.UnitTest/DbContextFactoryTest.cs new file mode 100644 index 0000000..a1eec08 --- /dev/null +++ b/Casbin.Persist.Adapter.EFCore.UnitTest/DbContextFactoryTest.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Casbin.Persist.Adapter.EFCore.Entities; +using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Casbin.Persist.Adapter.EFCore.UnitTest +{ +#if NET5_0_OR_GREATER + // Custom DbContext class for factory testing + public class TestCasbinDbContext : CasbinDbContext + { + public TestCasbinDbContext(DbContextOptions options) : base(options) + { + } + } + + public class DbContextFactoryTest : TestUtil, IClassFixture + { + private readonly ModelProvideFixture _modelProvideFixture; + + public DbContextFactoryTest(ModelProvideFixture modelProvideFixture) + { + _modelProvideFixture = modelProvideFixture; + } + + [Fact] + public void TestDbContextFactoryBasicUsage() + { + // Setup DI container with DbContextFactory + var services = new ServiceCollection(); + services.AddDbContextFactory(options => + { + options.UseSqlite("Data Source=FactoryTest1.db"); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Get the factory and create an adapter + var factory = serviceProvider.GetRequiredService>(); + + // Ensure database is created + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + } + + var adapter = new EFCoreAdapter, TestCasbinDbContext>(factory); + var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); + + // Add some policies + enforcer.AddPolicy("alice", "data1", "read"); + enforcer.AddPolicy("bob", "data2", "write"); + + // Verify policies were saved + Assert.True(enforcer.Enforce("alice", "data1", "read")); + Assert.True(enforcer.Enforce("bob", "data2", "write")); + Assert.False(enforcer.Enforce("alice", "data2", "read")); + + // Create a new enforcer to verify persistence + var adapter2 = new EFCoreAdapter, TestCasbinDbContext>(factory); + var enforcer2 = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter2); + enforcer2.LoadPolicy(); + + // Verify policies were loaded + Assert.True(enforcer2.Enforce("alice", "data1", "read")); + Assert.True(enforcer2.Enforce("bob", "data2", "write")); + + TestGetPolicy(enforcer2, AsList( + AsList("alice", "data1", "read"), + AsList("bob", "data2", "write") + )); + + // Cleanup + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + } + } + + [Fact] + public async Task TestDbContextFactoryAsyncOperations() + { + // Setup DI container with DbContextFactory + var services = new ServiceCollection(); + services.AddDbContextFactory(options => + { + options.UseSqlite("Data Source=FactoryTest2.db"); + }); + + var serviceProvider = services.BuildServiceProvider(); + + // Get the factory and create an adapter + var factory = serviceProvider.GetRequiredService>(); + + // Ensure database is created + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + } + + var adapter = new EFCoreAdapter, TestCasbinDbContext>(factory); + var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); + + // Add some policies asynchronously + await enforcer.AddPolicyAsync("alice", "data1", "read"); + await enforcer.AddPolicyAsync("bob", "data2", "write"); + await enforcer.AddPoliciesAsync(new [] + { + new List{"alice", "data2", "write"}, + new List{"bob", "data1", "read"} + }); + + // Verify policies were saved + Assert.True(enforcer.Enforce("alice", "data1", "read")); + Assert.True(enforcer.Enforce("bob", "data2", "write")); + Assert.True(enforcer.Enforce("alice", "data2", "write")); + Assert.True(enforcer.Enforce("bob", "data1", "read")); + + // Remove a policy + await enforcer.RemovePolicyAsync("alice", "data2", "write"); + Assert.False(enforcer.Enforce("alice", "data2", "write")); + + // Create a new enforcer to verify persistence + var adapter2 = new EFCoreAdapter, TestCasbinDbContext>(factory); + var enforcer2 = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter2); + await enforcer2.LoadPolicyAsync(); + + // Verify policies were loaded correctly + Assert.True(enforcer2.Enforce("alice", "data1", "read")); + Assert.True(enforcer2.Enforce("bob", "data2", "write")); + Assert.False(enforcer2.Enforce("alice", "data2", "write")); + Assert.True(enforcer2.Enforce("bob", "data1", "read")); + + TestGetPolicy(enforcer2, AsList( + AsList("alice", "data1", "read"), + AsList("bob", "data2", "write"), + AsList("bob", "data1", "read") + )); + + // Cleanup + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + } + } + + [Fact] + public void TestDbContextFactorySimulatesDIScenario() + { + // This test simulates the real DI scenario where the enforcer + // outlives the individual DbContext instances + + var services = new ServiceCollection(); + services.AddDbContextFactory(options => + { + options.UseSqlite("Data Source=FactoryTest3.db"); + }); + + var serviceProvider = services.BuildServiceProvider(); + var factory = serviceProvider.GetRequiredService>(); + + // Ensure database is created + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + } + + // Create enforcer once (like in the DI scenario) + var adapter = new EFCoreAdapter, TestCasbinDbContext>(factory); + var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); + + // Simulate multiple requests - each operation should create/dispose its own context + for (int i = 0; i < 5; i++) + { + enforcer.AddPolicy($"user{i}", "data1", "read"); + } + + // Load policy - should work even after multiple operations + enforcer.LoadPolicy(); + + // Verify all policies are still there + for (int i = 0; i < 5; i++) + { + Assert.True(enforcer.Enforce($"user{i}", "data1", "read")); + } + + // Remove some policies + enforcer.RemovePolicy("user0", "data1", "read"); + enforcer.RemovePolicy("user1", "data1", "read"); + + // Verify removal worked + Assert.False(enforcer.Enforce("user0", "data1", "read")); + Assert.False(enforcer.Enforce("user1", "data1", "read")); + Assert.True(enforcer.Enforce("user2", "data1", "read")); + + // Reload to verify persistence + enforcer.LoadPolicy(); + + Assert.False(enforcer.Enforce("user0", "data1", "read")); + Assert.False(enforcer.Enforce("user1", "data1", "read")); + Assert.True(enforcer.Enforce("user2", "data1", "read")); + Assert.True(enforcer.Enforce("user3", "data1", "read")); + Assert.True(enforcer.Enforce("user4", "data1", "read")); + + // Cleanup + using (var context = factory.CreateDbContext()) + { + context.Database.EnsureDeleted(); + } + } + } +#endif +} diff --git a/Casbin.Persist.Adapter.EFCore/EFCoreAdapter.cs b/Casbin.Persist.Adapter.EFCore/EFCoreAdapter.cs index ef4b5af..aacf7ed 100644 --- a/Casbin.Persist.Adapter.EFCore/EFCoreAdapter.cs +++ b/Casbin.Persist.Adapter.EFCore/EFCoreAdapter.cs @@ -19,6 +19,13 @@ public EFCoreAdapter(CasbinDbContext context) : base(context) { } + +#if NET5_0_OR_GREATER + public EFCoreAdapter(IDbContextFactory> contextFactory) : base(contextFactory) + { + + } +#endif } public class EFCoreAdapter : EFCoreAdapter> @@ -31,6 +38,15 @@ public EFCoreAdapter(CasbinDbContext context) : base(context) { } + +#if NET5_0_OR_GREATER + // For dependency injection with DbContext factory (recommended for DI scenarios) + // ReSharper disable once SuggestBaseTypeForParameter + public EFCoreAdapter(IDbContextFactory> contextFactory) : base(contextFactory) + { + + } +#endif } public partial class EFCoreAdapter : IAdapter, IFilteredAdapter @@ -39,29 +55,98 @@ public partial class EFCoreAdapter : IAdapter, where TKey : IEquatable { private DbSet _persistPolicies; - protected TDbContext DbContext { get; } - protected DbSet PersistPolicies => _persistPolicies ??= GetCasbinRuleDbSet(DbContext); + private readonly TDbContext _context; +#if NET5_0_OR_GREATER + private readonly IDbContextFactory _contextFactory; + private readonly bool _useFactory; +#endif + + protected TDbContext DbContext + { + get + { +#if NET5_0_OR_GREATER + if (_useFactory) + { + throw new InvalidOperationException( + "Cannot access DbContext directly when using IDbContextFactory. " + + "The adapter creates and disposes contexts automatically for each operation."); + } +#endif + return _context; + } + } + + protected DbSet PersistPolicies => _persistPolicies ??= GetCasbinRuleDbSet(_context); public EFCoreAdapter(TDbContext context) { - DbContext = context ?? throw new ArgumentNullException(nameof(context)); + _context = context ?? throw new ArgumentNullException(nameof(context)); +#if NET5_0_OR_GREATER + _useFactory = false; +#endif } +#if NET5_0_OR_GREATER + public EFCoreAdapter(IDbContextFactory contextFactory) + { + _contextFactory = contextFactory ?? throw new ArgumentNullException(nameof(contextFactory)); + _useFactory = true; + } + + private TDbContext CreateDbContext() + { + return _contextFactory.CreateDbContext(); + } + + private async ValueTask CreateDbContextAsync() + { +#if NET6_0_OR_GREATER + return await _contextFactory.CreateDbContextAsync(); +#else + // EF Core 5.0 doesn't have CreateDbContextAsync, use synchronous version + return await Task.FromResult(_contextFactory.CreateDbContext()); +#endif + } +#endif + #region Load policy public virtual void LoadPolicy(IPolicyStore store) { - var casbinRules = PersistPolicies.AsNoTracking(); - casbinRules = OnLoadPolicy(store, casbinRules); - store.LoadPolicyFromPersistPolicy(casbinRules.ToList()); +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var casbinRules = GetCasbinRuleDbSet(context).AsNoTracking(); + casbinRules = OnLoadPolicy(store, casbinRules); + store.LoadPolicyFromPersistPolicy(casbinRules.ToList()); + IsFiltered = false; + return; + } +#endif + var rules = PersistPolicies.AsNoTracking(); + rules = OnLoadPolicy(store, rules); + store.LoadPolicyFromPersistPolicy(rules.ToList()); IsFiltered = false; } public virtual async Task LoadPolicyAsync(IPolicyStore store) { - var casbinRules = PersistPolicies.AsNoTracking(); - casbinRules = OnLoadPolicy(store, casbinRules); - store.LoadPolicyFromPersistPolicy(await casbinRules.ToListAsync()); +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var casbinRules = GetCasbinRuleDbSet(context).AsNoTracking(); + casbinRules = OnLoadPolicy(store, casbinRules); + store.LoadPolicyFromPersistPolicy(await casbinRules.ToListAsync()); + IsFiltered = false; + return; + } +#endif + var rules = PersistPolicies.AsNoTracking(); + rules = OnLoadPolicy(store, rules); + store.LoadPolicyFromPersistPolicy(await rules.ToListAsync()); IsFiltered = false; } @@ -79,13 +164,29 @@ public virtual void SavePolicy(IPolicyStore store) return; } - var existRule = PersistPolicies.ToList(); - PersistPolicies.RemoveRange(existRule); - DbContext.SaveChanges(); +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + var existRule = policies.ToList(); + policies.RemoveRange(existRule); + context.SaveChanges(); + + var saveRules = OnSavePolicy(store, persistPolicies); + policies.AddRange(saveRules); + context.SaveChanges(); + return; + } +#endif - var saveRules = OnSavePolicy(store, persistPolicies); - PersistPolicies.AddRange(saveRules); - DbContext.SaveChanges(); + var existingRules = PersistPolicies.ToList(); + PersistPolicies.RemoveRange(existingRules); + _context.SaveChanges(); + + var rulesToSave = OnSavePolicy(store, persistPolicies); + PersistPolicies.AddRange(rulesToSave); + _context.SaveChanges(); } public virtual async Task SavePolicyAsync(IPolicyStore store) @@ -98,13 +199,29 @@ public virtual async Task SavePolicyAsync(IPolicyStore store) return; } - var existRule = PersistPolicies.ToList(); - PersistPolicies.RemoveRange(existRule); - await DbContext.SaveChangesAsync(); +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + var existRule = policies.ToList(); + policies.RemoveRange(existRule); + await context.SaveChangesAsync(); + + var saveRules = OnSavePolicy(store, persistPolicies); + await policies.AddRangeAsync(saveRules); + await context.SaveChangesAsync(); + return; + } +#endif + + var existingRules = PersistPolicies.ToList(); + PersistPolicies.RemoveRange(existingRules); + await _context.SaveChangesAsync(); - var saveRules = OnSavePolicy(store, persistPolicies); - await PersistPolicies.AddRangeAsync(saveRules); - await DbContext.SaveChangesAsync(); + var rulesToSave = OnSavePolicy(store, persistPolicies); + await PersistPolicies.AddRangeAsync(rulesToSave); + await _context.SaveChangesAsync(); } #endregion @@ -118,8 +235,27 @@ public virtual void AddPolicy(string section, string policyType, IPolicyValues v return; } - var filter = new PolicyFilter(policyType, 0, values); - var persistPolicies = filter.Apply(PersistPolicies); +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, 0, values); + var existingPolicies = filter.Apply(policies); + if (existingPolicies.Any()) + { + return; + } + var persistPolicy = PersistPolicy.Create(section, policyType, values); + persistPolicy = OnAddPolicy(section, policyType, values, persistPolicy); + policies.Add(persistPolicy); + context.SaveChanges(); + return; + } +#endif + + var policyFilter = new PolicyFilter(policyType, 0, values); + var persistPolicies = policyFilter.Apply(PersistPolicies); if (persistPolicies.Any()) { @@ -127,7 +263,7 @@ public virtual void AddPolicy(string section, string policyType, IPolicyValues v } InternalAddPolicy(section, policyType, values); - DbContext.SaveChanges(); + _context.SaveChanges(); } public virtual async Task AddPolicyAsync(string section, string policyType, IPolicyValues values) @@ -137,8 +273,27 @@ public virtual async Task AddPolicyAsync(string section, string policyType, IPol return; } - var filter = new PolicyFilter(policyType, 0, values); - var persistPolicies = filter.Apply(PersistPolicies); +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, 0, values); + var existingPolicies = filter.Apply(policies); + if (existingPolicies.Any()) + { + return; + } + var persistPolicy = PersistPolicy.Create(section, policyType, values); + persistPolicy = OnAddPolicy(section, policyType, values, persistPolicy); + await policies.AddAsync(persistPolicy); + await context.SaveChangesAsync(); + return; + } +#endif + + var policyFilter = new PolicyFilter(policyType, 0, values); + var persistPolicies = policyFilter.Apply(PersistPolicies); if (persistPolicies.Any()) { @@ -146,7 +301,7 @@ public virtual async Task AddPolicyAsync(string section, string policyType, IPol } await InternalAddPolicyAsync(section, policyType, values); - await DbContext.SaveChangesAsync(); + await _context.SaveChangesAsync(); } public virtual void AddPolicies(string section, string policyType, IReadOnlyList valuesList) @@ -155,8 +310,22 @@ public virtual void AddPolicies(string section, string policyType, IReadOnlyLis { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + var persistPolicies = valuesList.Select(v => PersistPolicy.Create(section, policyType, v)); + persistPolicies = OnAddPolicies(section, policyType, valuesList, persistPolicies); + policies.AddRange(persistPolicies); + context.SaveChanges(); + return; + } +#endif + InternalAddPolicies(section, policyType, valuesList); - DbContext.SaveChanges(); + _context.SaveChanges(); } public virtual async Task AddPoliciesAsync(string section, string policyType, IReadOnlyList valuesList) @@ -165,8 +334,22 @@ public virtual async Task AddPoliciesAsync(string section, string policyType, IR { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + var persistPolicies = valuesList.Select(v => PersistPolicy.Create(section, policyType, v)); + persistPolicies = OnAddPolicies(section, policyType, valuesList, persistPolicies); + await policies.AddRangeAsync(persistPolicies); + await context.SaveChangesAsync(); + return; + } +#endif + await InternalAddPoliciesAsync(section, policyType, valuesList); - await DbContext.SaveChangesAsync(); + await _context.SaveChangesAsync(); } #endregion @@ -179,8 +362,23 @@ public virtual void RemovePolicy(string section, string policyType, IPolicyValue { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, 0, values); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, 0, values, persistPolicies); + policies.RemoveRange(persistPolicies); + context.SaveChanges(); + return; + } +#endif + InternalRemovePolicy(section, policyType, values); - DbContext.SaveChanges(); + _context.SaveChanges(); } public virtual async Task RemovePolicyAsync(string section, string policyType, IPolicyValues values) @@ -189,8 +387,23 @@ public virtual async Task RemovePolicyAsync(string section, string policyType, I { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, 0, values); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, 0, values, persistPolicies); + policies.RemoveRange(persistPolicies); + await context.SaveChangesAsync(); + return; + } +#endif + InternalRemovePolicy(section, policyType, values); - await DbContext.SaveChangesAsync(); + await _context.SaveChangesAsync(); } public virtual void RemoveFilteredPolicy(string section, string policyType, int fieldIndex, IPolicyValues fieldValues) @@ -199,8 +412,23 @@ public virtual void RemoveFilteredPolicy(string section, string policyType, int { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, fieldIndex, fieldValues); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, fieldIndex, fieldValues, persistPolicies); + policies.RemoveRange(persistPolicies); + context.SaveChanges(); + return; + } +#endif + InternalRemoveFilteredPolicy(section, policyType, fieldIndex, fieldValues); - DbContext.SaveChanges(); + _context.SaveChanges(); } public virtual async Task RemoveFilteredPolicyAsync(string section, string policyType, int fieldIndex, IPolicyValues fieldValues) @@ -209,8 +437,23 @@ public virtual async Task RemoveFilteredPolicyAsync(string section, string polic { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + var filter = new PolicyFilter(policyType, fieldIndex, fieldValues); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, fieldIndex, fieldValues, persistPolicies); + policies.RemoveRange(persistPolicies); + await context.SaveChangesAsync(); + return; + } +#endif + InternalRemoveFilteredPolicy(section, policyType, fieldIndex, fieldValues); - await DbContext.SaveChangesAsync(); + await _context.SaveChangesAsync(); } @@ -220,8 +463,26 @@ public virtual void RemovePolicies(string section, string policyType, IReadOnlyL { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context); + foreach (var value in valuesList) + { + var filter = new PolicyFilter(policyType, 0, value); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, 0, value, persistPolicies); + policies.RemoveRange(persistPolicies); + } + context.SaveChanges(); + return; + } +#endif + InternalRemovePolicies(section, policyType, valuesList); - DbContext.SaveChanges(); + _context.SaveChanges(); } public virtual async Task RemovePoliciesAsync(string section, string policyType, IReadOnlyList valuesList) @@ -230,8 +491,26 @@ public virtual async Task RemovePoliciesAsync(string section, string policyType, { return; } + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context); + foreach (var value in valuesList) + { + var filter = new PolicyFilter(policyType, 0, value); + var persistPolicies = filter.Apply(policies); + persistPolicies = OnRemoveFilteredPolicy(section, policyType, 0, value, persistPolicies); + policies.RemoveRange(persistPolicies); + } + await context.SaveChangesAsync(); + return; + } +#endif + InternalRemovePolicies(section, policyType, valuesList); - await DbContext.SaveChangesAsync(); + await _context.SaveChangesAsync(); } #endregion @@ -244,10 +523,35 @@ public void UpdatePolicy(string section, string policyType, IPolicyValues oldVal { return; } - using var transaction = DbContext.Database.BeginTransaction(); + +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + using var transaction = context.Database.BeginTransaction(); + var policies = GetCasbinRuleDbSet(context); + + // Remove old policy + var filterOld = new PolicyFilter(policyType, 0, oldValues); + var oldPolicies = filterOld.Apply(policies); + oldPolicies = OnRemoveFilteredPolicy(section, policyType, 0, oldValues, oldPolicies); + policies.RemoveRange(oldPolicies); + + // Add new policy + var persistPolicy = PersistPolicy.Create(section, policyType, newValues); + persistPolicy = OnAddPolicy(section, policyType, newValues, persistPolicy); + policies.Add(persistPolicy); + + context.SaveChanges(); + transaction.Commit(); + return; + } +#endif + + using var trans = _context.Database.BeginTransaction(); InternalUpdatePolicy(section, policyType, oldValues, newValues); - DbContext.SaveChanges(); - transaction.Commit(); + _context.SaveChanges(); + trans.Commit(); } public async Task UpdatePolicyAsync(string section, string policyType, IPolicyValues oldValues, IPolicyValues newValues) @@ -256,10 +560,35 @@ public async Task UpdatePolicyAsync(string section, string policyType, IPolicyVa { return; } - await using var transaction = await DbContext.Database.BeginTransactionAsync(); + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + await using var transaction = await context.Database.BeginTransactionAsync(); + var policies = GetCasbinRuleDbSet(context); + + // Remove old policy + var filterOld = new PolicyFilter(policyType, 0, oldValues); + var oldPolicies = filterOld.Apply(policies); + oldPolicies = OnRemoveFilteredPolicy(section, policyType, 0, oldValues, oldPolicies); + policies.RemoveRange(oldPolicies); + + // Add new policy + var persistPolicy = PersistPolicy.Create(section, policyType, newValues); + persistPolicy = OnAddPolicy(section, policyType, newValues, persistPolicy); + await policies.AddAsync(persistPolicy); + + await context.SaveChangesAsync(); + await transaction.CommitAsync(); + return; + } +#endif + + await using var trans = await _context.Database.BeginTransactionAsync(); await InternalUpdatePolicyAsync(section, policyType, oldValues, newValues); - await DbContext.SaveChangesAsync(); - await transaction.CommitAsync(); + await _context.SaveChangesAsync(); + await trans.CommitAsync(); } public void UpdatePolicies(string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) @@ -268,10 +597,38 @@ public void UpdatePolicies(string section, string policyType, IReadOnlyList PersistPolicy.Create(section, policyType, v)); + persistPolicies = OnAddPolicies(section, policyType, newValuesList, persistPolicies); + policies.AddRange(persistPolicies); + + context.SaveChanges(); + transaction.Commit(); + return; + } +#endif + + using var trans = _context.Database.BeginTransaction(); InternalUpdatePolicies(section, policyType, oldValuesList, newValuesList); - DbContext.SaveChanges(); - transaction.Commit(); + _context.SaveChanges(); + trans.Commit(); } public async Task UpdatePoliciesAsync(string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) @@ -280,10 +637,38 @@ public async Task UpdatePoliciesAsync(string section, string policyType, IReadOn { return; } - await using var transaction = await DbContext.Database.BeginTransactionAsync(); + +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + await using var transaction = await context.Database.BeginTransactionAsync(); + var policies = GetCasbinRuleDbSet(context); + + // Remove old policies + foreach (var oldValue in oldValuesList) + { + var filterOld = new PolicyFilter(policyType, 0, oldValue); + var oldPolicies = filterOld.Apply(policies); + oldPolicies = OnRemoveFilteredPolicy(section, policyType, 0, oldValue, oldPolicies); + policies.RemoveRange(oldPolicies); + } + + // Add new policies + var persistPolicies = newValuesList.Select(v => PersistPolicy.Create(section, policyType, v)); + persistPolicies = OnAddPolicies(section, policyType, newValuesList, persistPolicies); + await policies.AddRangeAsync(persistPolicies); + + await context.SaveChangesAsync(); + await transaction.CommitAsync(); + return; + } +#endif + + await using var trans = await _context.Database.BeginTransactionAsync(); await InternalUpdatePoliciesAsync(section, policyType, oldValuesList, newValuesList); - await DbContext.SaveChangesAsync(); - await transaction.CommitAsync(); + await _context.SaveChangesAsync(); + await trans.CommitAsync(); } #endregion @@ -294,6 +679,19 @@ public async Task UpdatePoliciesAsync(string section, string policyType, IReadOn public void LoadFilteredPolicy(IPolicyStore store, IPolicyFilter filter) { +#if NET5_0_OR_GREATER + if (_useFactory) + { + using var context = CreateDbContext(); + var policies = GetCasbinRuleDbSet(context).AsNoTracking(); + policies = filter.Apply(policies); + policies = OnLoadPolicy(store, policies); + store.LoadPolicyFromPersistPolicy(policies.ToList()); + IsFiltered = true; + return; + } +#endif + var persistPolicies = PersistPolicies.AsNoTracking(); persistPolicies = filter.Apply(persistPolicies); persistPolicies = OnLoadPolicy(store, persistPolicies); @@ -303,6 +701,19 @@ public void LoadFilteredPolicy(IPolicyStore store, IPolicyFilter filter) public async Task LoadFilteredPolicyAsync(IPolicyStore store, IPolicyFilter filter) { +#if NET5_0_OR_GREATER + if (_useFactory) + { + await using var context = await CreateDbContextAsync(); + var policies = GetCasbinRuleDbSet(context).AsNoTracking(); + policies = filter.Apply(policies); + policies = OnLoadPolicy(store, policies); + store.LoadPolicyFromPersistPolicy(await policies.ToListAsync()); + IsFiltered = true; + return; + } +#endif + var persistPolicies = PersistPolicies.AsNoTracking(); persistPolicies = filter.Apply(persistPolicies); persistPolicies = OnLoadPolicy(store, persistPolicies); diff --git a/README.md b/README.md index b12d493..80e48b0 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,103 @@ namespace ConsoleAppExample } ``` +## Using with Dependency Injection + +When using the adapter with Dependency Injection (e.g., in ASP.NET Core), it's recommended to use `IDbContextFactory` instead of directly injecting the `DbContext`. This prevents issues with the `DbContext` being disposed while the enforcer is still using it. + +**For .NET 5.0 and later:** + +```csharp +using Casbin.Adapter.EFCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +// Define a custom DbContext (if using default CasbinDbContext) +public class MyCasbinDbContext : CasbinDbContext +{ + public MyCasbinDbContext(DbContextOptions options) + : base(options) + { + } +} + +// In your Program.cs or Startup.cs +public class Program +{ + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Register the DbContext factory (recommended for DI scenarios) + builder.Services.AddDbContextFactory(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("Default"))); + + // Configure Casbin + builder.Services.AddCasbinAuthorization(options => + { + options.DefaultModelPath = "model.conf"; + + // Use the factory-based adapter constructor + options.DefaultEnforcerFactory = (serviceProvider, model) => + { + var contextFactory = serviceProvider.GetRequiredService>(); + var adapter = new EFCoreAdapter, MyCasbinDbContext>(contextFactory); + return new Enforcer(model, adapter); + }; + }); + + var app = builder.Build(); + + // Ensure database is created + using (var scope = app.Services.CreateScope()) + { + var factory = scope.ServiceProvider.GetRequiredService>(); + using var context = factory.CreateDbContext(); + context.Database.EnsureCreated(); + } + + app.Run(); + } +} +``` + +**Usage in a controller or page model:** + +```csharp +public class MyController : Controller +{ + private readonly IEnforcer _enforcer; + + public MyController(IEnforcerProvider enforcerProvider) + { + _enforcer = enforcerProvider.GetEnforcer(); + } + + public IActionResult Index() + { + // The enforcer can safely perform operations across multiple requests + // Each operation creates and disposes its own DbContext + _enforcer.LoadPolicy(); + + if (_enforcer.Enforce(User.Identity.Name, "resource", "read")) + { + // Allow access + } + + return View(); + } +} +``` + +### Why use IDbContextFactory? + +When you inject a `DbContext` directly into a service with a longer lifetime than the context (like a singleton enforcer), you'll encounter `ObjectDisposedException` when the context is disposed but the enforcer tries to use it. + +Using `IDbContextFactory` solves this by: +- Creating a new `DbContext` for each operation +- Automatically disposing the context after the operation completes +- Allowing the enforcer to outlive individual request scopes + ## Getting Help - [Casbin.NET](https://github.com/casbin/Casbin.NET)