├── Casbin.Persist.Adapter.EFCore ├── casbin.png ├── IPersistPolicy.cs ├── Entities │ └── EFCorePersistPolicy.cs ├── Extensions │ ├── PolicyStoreExtension.cs │ └── ServiceCollectionExtensions.cs ├── DefaultPersistPolicyEntityTypeConfiguration.cs ├── SingleContextProvider.cs ├── ICasbinDbContextProvider.cs ├── CasbinDbContext.cs ├── Casbin.Persist.Adapter.EFCore.csproj └── EFCoreAdapter.Internal.cs ├── Casbin.Persist.Adapter.EFCore.UnitTest ├── examples │ ├── rbac_policy.csv │ └── rbac_model.conf ├── xunit.runner.json ├── Fixtures │ ├── ModelProvideFixture.cs │ ├── DbContextProviderFixture.cs │ ├── TestHostFixture.cs │ ├── SimpleFieldFilter.cs │ ├── PolicyTypeContextProvider.cs │ └── MultiContextProviderFixture.cs ├── Extensions │ └── CasbinDbContextExtension.cs ├── AutoTest.cs ├── Casbin.Persist.Adapter.EFCore.UnitTest.csproj ├── DependencyInjectionTest.cs ├── SpecialPolicyTest.cs ├── TestUtil.cs ├── BackwardCompatibilityTest.cs └── MultiContextTest.cs ├── NuGet.config ├── Casbin.Persist.Adapter.EFCore.IntegrationTest ├── xunit.runner.json ├── examples │ └── multi_context_model.conf ├── Integration │ ├── IntegrationTestCollection.cs │ ├── TransactionIntegrityTestFixture.cs │ ├── README.md │ └── SchemaDistributionTests.cs └── Casbin.Persist.Adapter.EFCore.IntegrationTest.csproj ├── .releaserc.json ├── EFCore-Adapter.sln.DotSettings ├── .github ├── semantic.yml └── workflows │ ├── build.yml │ └── release.yml ├── EFCore-Adapter.sln ├── .gitignore ├── README.md ├── LICENSE └── MULTI_CONTEXT_USAGE_GUIDE.md /Casbin.Persist.Adapter.EFCore/casbin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/casbin-net/efcore-adapter/HEAD/Casbin.Persist.Adapter.EFCore/casbin.png -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/examples/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | p, data2_admin, data2, read 4 | p, data2_admin, data2, write 5 | g, alice, data2_admin -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "parallelizeTestCollections": true, 4 | "maxParallelThreads": -1 5 | } 6 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "parallelizeTestCollections": true, 4 | "maxParallelThreads": -1 5 | } 6 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/IPersistPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Casbin.Persist.Adapter.EFCore 4 | { 5 | public interface IEFCorePersistPolicy : IPersistPolicy where TKey : IEquatable 6 | { 7 | public TKey Id { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/Entities/EFCorePersistPolicy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Casbin.Persist.Adapter.EFCore.Entities 4 | { 5 | public class EFCorePersistPolicy : PersistPolicy, IEFCorePersistPolicy where TKey : IEquatable 6 | { 7 | public TKey Id { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/examples/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/examples/multi_context_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | g2 = _, _ 10 | 11 | [policy_effect] 12 | e = some(where (p.eft == allow)) 13 | 14 | [matchers] 15 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act 16 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/ModelProvideFixture.cs: -------------------------------------------------------------------------------- 1 | // using System.IO; 2 | using Casbin.Model; 3 | 4 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 5 | { 6 | public class ModelProvideFixture 7 | { 8 | private readonly string _rbacModelText = System.IO.File.ReadAllText("examples/rbac_model.conf"); 9 | 10 | public IModel GetNewRbacModel() 11 | { 12 | return DefaultModel.CreateFromText(_rbacModelText); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "debug": true, 3 | "branches": [ 4 | "+([0-9])?(.{+([0-9]),x}).x", 5 | "master", 6 | { 7 | "name": "alpha", 8 | "prerelease": true 9 | }, 10 | { 11 | "name": "preview", 12 | "prerelease": true 13 | }, 14 | { 15 | "name": "rc", 16 | "prerelease": true 17 | } 18 | ], 19 | "plugins": [ 20 | "@semantic-release/commit-analyzer", 21 | "@semantic-release/release-notes-generator", 22 | "@semantic-release/github" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /EFCore-Adapter.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | EF 3 | True 4 | True -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate the PR title AND all the commits 2 | titleAndCommits: true 3 | # Require at least one commit to be valid 4 | # this is only relevant when using commitsOnly: true or titleAndCommits: true, 5 | # which validate all commits by default 6 | anyCommit: true 7 | # Allow use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns") 8 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 9 | allowMergeCommits: false 10 | # Allow use of Revert commits (eg on github: "Revert "feat: ride unicorns"") 11 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 12 | allowRevertCommits: false -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/DbContextProviderFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 5 | { 6 | public class DbContextProviderFixture 7 | { 8 | public CasbinDbContext GetContext(string name) where TKey : IEquatable 9 | { 10 | var options = new DbContextOptionsBuilder>() 11 | .UseSqlite($"Data Source={name}.db") 12 | .Options; 13 | var context = new CasbinDbContext(options); 14 | context.Database.EnsureCreated(); 15 | return context; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/Integration/IntegrationTestCollection.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Integration 4 | { 5 | /// 6 | /// Collection definition for integration tests. 7 | /// This ensures all test classes marked with [Collection("IntegrationTests")] 8 | /// share a single TransactionIntegrityTestFixture instance. 9 | /// 10 | /// DisableParallelization = true ensures tests run sequentially to prevent 11 | /// race conditions and schema conflicts. 12 | /// 13 | [CollectionDefinition("IntegrationTests", DisableParallelization = true)] 14 | public class IntegrationTestCollection : ICollectionFixture 15 | { 16 | // This class has no code, and is never instantiated. 17 | // Its purpose is simply to be the place to apply [CollectionDefinition] 18 | // and all the ICollectionFixture<> interfaces. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/TestHostFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.TestHost; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using Casbin.Persist.Adapter.EFCore.Extensions; 6 | 7 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 8 | { 9 | public class TestHostFixture 10 | { 11 | public TestHostFixture() 12 | { 13 | // Use unique database name to allow parallel test execution 14 | var uniqueDbName = $"CasbinHostTest_{Guid.NewGuid():N}.db"; 15 | 16 | Services = new ServiceCollection() 17 | .AddDbContext>(options => 18 | { 19 | options.UseSqlite($"Data Source={uniqueDbName}"); 20 | }) 21 | .AddEFCoreAdapter() 22 | .BuildServiceProvider(); 23 | Server = new TestServer(Services); 24 | } 25 | 26 | public TestServer Server { get; set; } 27 | 28 | public IServiceProvider Services { get; set; } 29 | } 30 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/SimpleFieldFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Casbin.Model; 3 | using Casbin.Persist; 4 | 5 | #nullable enable 6 | 7 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 8 | { 9 | /// 10 | /// Simple field-based policy filter for testing. 11 | /// Replaces the deprecated Filter class for basic field filtering scenarios. 12 | /// 13 | public class SimpleFieldFilter : IPolicyFilter 14 | { 15 | private readonly PolicyFilter _policyFilter; 16 | 17 | /// 18 | /// Creates a filter that filters policies of the specified type by field values. 19 | /// 20 | /// The policy type to filter (e.g., "p", "g", "g2") 21 | /// The field index to start filtering from (usually 0) 22 | /// The field values to filter by 23 | public SimpleFieldFilter(string policyType, int fieldIndex, IPolicyValues values) 24 | { 25 | _policyFilter = new PolicyFilter(policyType, fieldIndex, values); 26 | } 27 | 28 | /// 29 | /// Applies the filter to the policy collection. 30 | /// 31 | public IQueryable Apply(IQueryable policies) where T : IPersistPolicy 32 | { 33 | return _policyFilter.Apply(policies); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Extensions/CasbinDbContextExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Extensions 5 | { 6 | public static class CasbinDbContextExtension 7 | { 8 | internal static void Clear(this CasbinDbContext dbContext) where TKey : IEquatable 9 | { 10 | // Force model initialization before ensuring database exists 11 | // This ensures EF Core knows about all entity configurations 12 | _ = dbContext.Model; 13 | 14 | // Ensure database and tables exist before attempting to clear 15 | dbContext.Database.EnsureCreated(); 16 | 17 | // Try to access and clear policies 18 | try 19 | { 20 | var policies = dbContext.Policies.ToList(); 21 | if (policies.Count > 0) 22 | { 23 | dbContext.RemoveRange(policies); 24 | dbContext.SaveChanges(); 25 | } 26 | } 27 | catch (Microsoft.Data.Sqlite.SqliteException) 28 | { 29 | // If table still doesn't exist after EnsureCreated, 30 | // force a second attempt with model refresh 31 | dbContext.Database.EnsureDeleted(); 32 | _ = dbContext.Model; 33 | dbContext.Database.EnsureCreated(); 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/Extensions/PolicyStoreExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Casbin.Model; 3 | 4 | namespace Casbin.Persist.Adapter.EFCore.Extensions 5 | { 6 | public static class PolicyStoreExtension 7 | { 8 | internal static void LoadPolicyFromPersistPolicy(this IPolicyStore store, IEnumerable persistPolicies) 9 | where TPersistPolicy : class, IPersistPolicy 10 | { 11 | foreach (var policy in persistPolicies) 12 | { 13 | if (string.IsNullOrWhiteSpace(policy.Section)) 14 | { 15 | policy.Section = policy.Type.Substring(0, 1); 16 | } 17 | var requiredCount = store.GetRequiredValuesCount(policy.Section, policy.Type); 18 | var values = Policy.ValuesFrom(policy, requiredCount); 19 | store.AddPolicy(policy.Section, policy.Type, values); 20 | } 21 | } 22 | 23 | internal static void ReadPolicyFromCasbinModel(this ICollection persistPolicies, IPolicyStore store) 24 | where TPersistPolicy : class, IPersistPolicy, new() 25 | { 26 | var types = store.GetPolicyTypesAllSections(); 27 | foreach (var section in types) 28 | { 29 | foreach (var type in section.Value) 30 | { 31 | var scanner = store.Scan(section.Key, type); 32 | while (scanner.GetNext(out var values)) 33 | { 34 | persistPolicies.Add(PersistPolicy.Create(section.Key, type, values)); 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/PolicyTypeContextProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | #nullable enable 6 | 7 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 8 | { 9 | /// 10 | /// Test context provider that routes policy types (p, p2, etc.) to one context 11 | /// and grouping types (g, g2, etc.) to another context. 12 | /// 13 | public class PolicyTypeContextProvider : ICasbinDbContextProvider 14 | { 15 | private readonly CasbinDbContext _policyContext; 16 | private readonly CasbinDbContext _groupingContext; 17 | 18 | public PolicyTypeContextProvider( 19 | CasbinDbContext policyContext, 20 | CasbinDbContext groupingContext) 21 | { 22 | _policyContext = policyContext ?? throw new ArgumentNullException(nameof(policyContext)); 23 | _groupingContext = groupingContext ?? throw new ArgumentNullException(nameof(groupingContext)); 24 | } 25 | 26 | public DbContext GetContextForPolicyType(string policyType) 27 | { 28 | if (string.IsNullOrEmpty(policyType)) 29 | { 30 | return _policyContext; 31 | } 32 | 33 | // Route 'p' types (p, p2, p3, etc.) to policy context 34 | // Route 'g' types (g, g2, g3, etc.) to grouping context 35 | return policyType.StartsWith("p", StringComparison.OrdinalIgnoreCase) 36 | ? _policyContext 37 | : _groupingContext; 38 | } 39 | 40 | public IEnumerable GetAllContexts() 41 | { 42 | return new DbContext[] { _policyContext, _groupingContext }; 43 | } 44 | 45 | public System.Data.Common.DbConnection? GetSharedConnection() 46 | { 47 | // Return null since this provider uses separate SQLite database files 48 | // (each context has its own connection) 49 | return null; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/DefaultPersistPolicyEntityTypeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Casbin.Persist.Adapter.EFCore.Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace Casbin.Persist.Adapter.EFCore 7 | { 8 | public class DefaultPersistPolicyEntityTypeConfiguration : IEntityTypeConfiguration> 9 | where TKey : IEquatable 10 | { 11 | private readonly string _tableName; 12 | 13 | public DefaultPersistPolicyEntityTypeConfiguration(string tableName) 14 | { 15 | _tableName = tableName; 16 | } 17 | 18 | public virtual void Configure(EntityTypeBuilder> builder) 19 | { 20 | builder.ToTable(_tableName); 21 | 22 | builder.Property(p => p.Id).HasColumnName("id"); 23 | builder.Ignore(p => p.Section); 24 | builder.Property(p => p.Type).HasColumnName("ptype"); 25 | builder.Property(p => p.Value1).HasColumnName("v0"); 26 | builder.Property(p => p.Value2).HasColumnName("v1"); 27 | builder.Property(p => p.Value3).HasColumnName("v2"); 28 | builder.Property(p => p.Value4).HasColumnName("v3"); 29 | builder.Property(p => p.Value5).HasColumnName("v4"); 30 | builder.Property(p => p.Value6).HasColumnName("v5"); 31 | builder.Property(p => p.Value7).HasColumnName("v6"); 32 | builder.Property(p => p.Value8).HasColumnName("v7"); 33 | builder.Property(p => p.Value9).HasColumnName("v8"); 34 | builder.Property(p => p.Value10).HasColumnName("v9"); 35 | builder.Property(p => p.Value11).HasColumnName("v10"); 36 | builder.Property(p => p.Value12).HasColumnName("v11"); 37 | builder.Property(p => p.Value13).HasColumnName("v12"); 38 | builder.Property(p => p.Value14).HasColumnName("v13"); 39 | 40 | builder.HasIndex(p => p.Type); 41 | builder.HasIndex(p => p.Value1); 42 | builder.HasIndex(p => p.Value2); 43 | builder.HasIndex(p => p.Value3); 44 | builder.HasIndex(p => p.Value4); 45 | builder.HasIndex(p => p.Value5); 46 | builder.HasIndex(p => p.Value6); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/SingleContextProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | #nullable enable 6 | 7 | namespace Casbin.Persist.Adapter.EFCore 8 | { 9 | /// 10 | /// Default context provider that uses a single DbContext for all policy types. 11 | /// This maintains backward compatibility with the original single-context behavior. 12 | /// 13 | /// The type of the primary key 14 | public class SingleContextProvider : ICasbinDbContextProvider 15 | where TKey : IEquatable 16 | { 17 | private readonly DbContext _context; 18 | 19 | /// 20 | /// Creates a new instance of SingleContextProvider with the specified context. 21 | /// 22 | /// The DbContext to use for all policy types 23 | /// Thrown when context is null 24 | public SingleContextProvider(DbContext context) 25 | { 26 | _context = context ?? throw new ArgumentNullException(nameof(context)); 27 | } 28 | 29 | /// 30 | /// Returns the single context for any policy type. 31 | /// 32 | /// The policy type (ignored in this implementation) 33 | /// The single DbContext instance 34 | public DbContext GetContextForPolicyType(string policyType) 35 | { 36 | return _context; 37 | } 38 | 39 | /// 40 | /// Returns a collection containing only the single context. 41 | /// 42 | /// An enumerable containing the single DbContext 43 | public IEnumerable GetAllContexts() 44 | { 45 | return new[] { _context }; 46 | } 47 | 48 | /// 49 | /// Returns null since single-context scenarios don't have a shared connection 50 | /// (only one context, so the concept of "shared" doesn't apply). 51 | /// 52 | /// Always returns null 53 | public System.Data.Common.DbConnection? GetSharedConnection() 54 | { 55 | return null; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/AutoTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Casbin.Persist.Adapter.EFCore.UnitTest.Extensions; 5 | using Microsoft.EntityFrameworkCore; 6 | using Casbin.Persist.Adapter.EFCore.Entities; 7 | using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; 8 | using Xunit; 9 | 10 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 11 | { 12 | public class EFCoreAdapterTest : TestUtil, IClassFixture, IClassFixture 13 | { 14 | private readonly ModelProvideFixture _modelProvideFixture; 15 | private readonly DbContextProviderFixture _dbContextProviderFixture; 16 | 17 | public EFCoreAdapterTest(ModelProvideFixture modelProvideFixture, DbContextProviderFixture dbContextProviderFixture) 18 | { 19 | _modelProvideFixture = modelProvideFixture; 20 | _dbContextProviderFixture = dbContextProviderFixture; 21 | } 22 | 23 | private static void InitPolicy(CasbinDbContext context) 24 | { 25 | context.Clear(); 26 | context.Policies.Add(new EFCorePersistPolicy() 27 | { 28 | Type = "p", 29 | Value1 = "alice", 30 | Value2 = "data1", 31 | Value3 = "read", 32 | }); 33 | context.Policies.Add(new EFCorePersistPolicy() 34 | { 35 | Type = "p", 36 | Value1 = "bob", 37 | Value2 = "data2", 38 | Value3 = "write", 39 | }); 40 | context.Policies.Add(new EFCorePersistPolicy() 41 | { 42 | Type = "p", 43 | Value1 = "data2_admin", 44 | Value2 = "data2", 45 | Value3 = "read", 46 | }); 47 | context.Policies.Add(new EFCorePersistPolicy() 48 | { 49 | Type = "p", 50 | Value1 = "data2_admin", 51 | Value2 = "data2", 52 | Value3 = "write", 53 | }); 54 | context.Policies.Add(new EFCorePersistPolicy() 55 | { 56 | Type = "g", 57 | Value1 = "alice", 58 | Value2 = "data2_admin", 59 | }); 60 | context.SaveChanges(); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/ICasbinDbContextProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data.Common; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | #nullable enable 7 | 8 | namespace Casbin.Persist.Adapter.EFCore 9 | { 10 | /// 11 | /// Provides DbContext instances for different policy types, enabling multi-context scenarios 12 | /// where different policy types can be stored in separate schemas, tables, or databases. 13 | /// 14 | /// The type of the primary key 15 | public interface ICasbinDbContextProvider where TKey : IEquatable 16 | { 17 | /// 18 | /// Gets the DbContext that should handle the specified policy type. 19 | /// 20 | /// The policy type identifier (e.g., "p", "p2", "g", "g2") 21 | /// The DbContext instance responsible for this policy type 22 | DbContext GetContextForPolicyType(string policyType); 23 | 24 | /// 25 | /// Gets all unique DbContext instances managed by this provider. 26 | /// Used for operations that need to coordinate across all contexts (e.g., SavePolicy, LoadPolicy). 27 | /// 28 | /// An enumerable of all distinct DbContext instances 29 | IEnumerable GetAllContexts(); 30 | 31 | /// 32 | /// Gets the shared DbConnection if all contexts use the same physical connection. 33 | /// Returns null if contexts use separate connections. 34 | /// 35 | /// 36 | /// When non-null, the adapter starts transactions at the connection level 37 | /// (connection.BeginTransaction()) rather than context level, which is required 38 | /// for proper savepoint handling in PostgreSQL and other databases that require 39 | /// explicit transaction blocks before creating savepoints. 40 | /// 41 | /// Return null for scenarios where contexts use separate physical connections 42 | /// (e.g., separate SQLite database files), in which case the adapter will use 43 | /// separate transactions for each context. 44 | /// 45 | /// The shared DbConnection, or null if contexts use separate connections 46 | DbConnection? GetSharedConnection(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/CasbinDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Casbin.Persist.Adapter.EFCore.Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Casbin.Persist.Adapter.EFCore 8 | { 9 | public partial class CasbinDbContext : DbContext where TKey : IEquatable 10 | { 11 | public virtual DbSet> Policies { get; set; } 12 | private const string DefaultTableName = "casbin_rule"; 13 | 14 | private readonly IEntityTypeConfiguration> _casbinModelConfig; 15 | private readonly string _schemaName; 16 | 17 | public CasbinDbContext() 18 | { 19 | _casbinModelConfig = new DefaultPersistPolicyEntityTypeConfiguration(DefaultTableName); 20 | } 21 | 22 | public CasbinDbContext(DbContextOptions> options, string schemaName = null, string tableName = DefaultTableName) : base(options) 23 | { 24 | _casbinModelConfig = new DefaultPersistPolicyEntityTypeConfiguration(tableName); 25 | _schemaName = schemaName; 26 | } 27 | 28 | protected CasbinDbContext(DbContextOptions options, string schemaName = null, string tableName = DefaultTableName) : base(options) 29 | { 30 | _casbinModelConfig = new DefaultPersistPolicyEntityTypeConfiguration(tableName); 31 | _schemaName = schemaName; 32 | } 33 | 34 | public CasbinDbContext(DbContextOptions> options, IEntityTypeConfiguration> casbinModelConfig, string schemaName = null) : base(options) 35 | { 36 | _casbinModelConfig = casbinModelConfig; 37 | _schemaName = schemaName; 38 | } 39 | 40 | protected CasbinDbContext(DbContextOptions options, IEntityTypeConfiguration> casbinModelConfig, string schemaName = null) : base(options) 41 | { 42 | _casbinModelConfig = casbinModelConfig; 43 | _schemaName = schemaName; 44 | } 45 | 46 | protected override void OnModelCreating(ModelBuilder modelBuilder) 47 | { 48 | if (string.IsNullOrWhiteSpace(_schemaName) is false) 49 | { 50 | modelBuilder.HasDefaultSchema(_schemaName); 51 | } 52 | 53 | if (_casbinModelConfig is not null) 54 | { 55 | modelBuilder.ApplyConfiguration(_casbinModelConfig); 56 | } 57 | } 58 | 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/Casbin.Persist.Adapter.EFCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0;net7.0;net6.0;net5.0;netcoreapp3.1; 5 | 9.0 6 | 7 | 8 | 9 | Casbin.NET.Adapter.EFCore 10 | casbin.png 11 | Casbin.NET 12 | GIT 13 | https://github.com/casbin-net/efcore-adapter 14 | Apache License 2.0 15 | Apache-2.0 16 | https://github.com/casbin-net/efcore-adapter 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Casbin.Persist.Adapter.EFCore.Extensions 6 | { 7 | /// 8 | /// Extension methods for registering EFCoreAdapter with dependency injection. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// 13 | /// Adds the EFCoreAdapter to the service collection. 14 | /// The adapter will resolve the DbContext from the service provider on each operation, 15 | /// preventing issues with disposed contexts when used with long-lived services. 16 | /// 17 | /// The type of the primary key for the policy entities. 18 | /// The service collection. 19 | /// The service lifetime for the adapter. Default is Scoped. 20 | /// The service collection for chaining. 21 | public static IServiceCollection AddEFCoreAdapter( 22 | this IServiceCollection services, 23 | ServiceLifetime lifetime = ServiceLifetime.Scoped) where TKey : IEquatable 24 | { 25 | var descriptor = new ServiceDescriptor( 26 | typeof(IAdapter), 27 | sp => new EFCoreAdapter(sp), 28 | lifetime); 29 | 30 | services.TryAdd(descriptor); 31 | return services; 32 | } 33 | 34 | /// 35 | /// Adds the EFCoreAdapter with custom policy type to the service collection. 36 | /// The adapter will resolve the DbContext from the service provider on each operation, 37 | /// preventing issues with disposed contexts when used with long-lived services. 38 | /// 39 | /// The type of the primary key for the policy entities. 40 | /// The type of the persist policy entity. 41 | /// The service collection. 42 | /// The service lifetime for the adapter. Default is Scoped. 43 | /// The service collection for chaining. 44 | public static IServiceCollection AddEFCoreAdapter( 45 | this IServiceCollection services, 46 | ServiceLifetime lifetime = ServiceLifetime.Scoped) 47 | where TKey : IEquatable 48 | where TPersistPolicy : class, IEFCorePersistPolicy, new() 49 | { 50 | var descriptor = new ServiceDescriptor( 51 | typeof(IAdapter), 52 | sp => new EFCoreAdapter(sp), 53 | lifetime); 54 | 55 | services.TryAdd(descriptor); 56 | return services; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/Casbin.Persist.Adapter.EFCore.IntegrationTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0;net7.0;net6.0;net5.0;netcoreapp3.1; 5 | false 6 | 11 7 | false 8 | $(NoWarn);NU1701 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Always 60 | 61 | 62 | Always 63 | 64 | 65 | Always 66 | 67 | 68 | PreserveNewest 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Fixtures/MultiContextProviderFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures 5 | { 6 | /// 7 | /// Fixture for creating multi-context test scenarios with separate contexts for policies and groupings 8 | /// 9 | public class MultiContextProviderFixture : IDisposable 10 | { 11 | private bool _disposed; 12 | 13 | /// 14 | /// Creates a multi-context provider with separate contexts for policy and grouping rules. 15 | /// Uses separate database files with the same table name for proper isolation. 16 | /// This approach avoids SQLite transaction limitations across tables. 17 | /// 18 | /// Unique name for this test to avoid database conflicts 19 | /// A PolicyTypeContextProvider configured for testing 20 | public PolicyTypeContextProvider GetMultiContextProvider(string testName) 21 | { 22 | // Use separate database files for proper isolation 23 | var policyDbName = $"MultiContext_{testName}_policy.db"; 24 | var groupingDbName = $"MultiContext_{testName}_grouping.db"; 25 | 26 | // Create policy context with its own database and default table name 27 | var policyOptions = new DbContextOptionsBuilder>() 28 | .UseSqlite($"Data Source={policyDbName}") 29 | .Options; 30 | var policyContext = new CasbinDbContext(policyOptions); 31 | policyContext.Database.EnsureCreated(); 32 | 33 | // Create grouping context with its own database and default table name 34 | var groupingOptions = new DbContextOptionsBuilder>() 35 | .UseSqlite($"Data Source={groupingDbName}") 36 | .Options; 37 | var groupingContext = new CasbinDbContext(groupingOptions); 38 | groupingContext.Database.EnsureCreated(); 39 | 40 | return new PolicyTypeContextProvider(policyContext, groupingContext); 41 | } 42 | 43 | /// 44 | /// Gets separate contexts for direct verification in tests. 45 | /// Returns NEW context instances pointing to the same databases as the provider. 46 | /// 47 | public (CasbinDbContext policyContext, CasbinDbContext groupingContext) GetSeparateContexts(string testName) 48 | { 49 | // Use same database file names as GetMultiContextProvider 50 | var policyDbName = $"MultiContext_{testName}_policy.db"; 51 | var groupingDbName = $"MultiContext_{testName}_grouping.db"; 52 | 53 | // Create new context instances that point to the same database files 54 | var policyOptions = new DbContextOptionsBuilder>() 55 | .UseSqlite($"Data Source={policyDbName}") 56 | .Options; 57 | var policyContext = new CasbinDbContext(policyOptions); 58 | policyContext.Database.EnsureCreated(); 59 | 60 | var groupingOptions = new DbContextOptionsBuilder>() 61 | .UseSqlite($"Data Source={groupingDbName}") 62 | .Options; 63 | var groupingContext = new CasbinDbContext(groupingOptions); 64 | groupingContext.Database.EnsureCreated(); 65 | 66 | return (policyContext, groupingContext); 67 | } 68 | 69 | public void Dispose() 70 | { 71 | Dispose(true); 72 | GC.SuppressFinalize(this); 73 | } 74 | 75 | protected virtual void Dispose(bool disposing) 76 | { 77 | if (!_disposed && disposing) 78 | { 79 | // Cleanup handled by test framework 80 | _disposed = true; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/Casbin.Persist.Adapter.EFCore.UnitTest.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0;net8.0;net7.0;net6.0;net5.0;netcoreapp3.1; 5 | false 6 | 11 7 | $(NoWarn);NU1701 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | Always 69 | 70 | 71 | Always 72 | 73 | 74 | Always 75 | 76 | 77 | PreserveNewest 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/DependencyInjectionTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; 3 | using Xunit; 4 | using Casbin.Model; 5 | 6 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 7 | { 8 | public class DependencyInjectionTest : IClassFixture, IClassFixture 9 | { 10 | private readonly TestHostFixture _testHostFixture; 11 | private readonly ModelProvideFixture _modelProvideFixture; 12 | 13 | public DependencyInjectionTest(TestHostFixture testHostFixture, ModelProvideFixture modelProvideFixture) 14 | { 15 | _testHostFixture = testHostFixture; 16 | _modelProvideFixture = modelProvideFixture; 17 | } 18 | 19 | [Fact] 20 | public void ShouldResolveCasbinDbContext() 21 | { 22 | var dbContext = _testHostFixture.Services.GetService>(); 23 | Assert.NotNull(dbContext); 24 | dbContext.Database.EnsureCreated(); 25 | } 26 | 27 | [Fact] 28 | public void ShouldResolveEfCoreAdapter() 29 | { 30 | var adapter = _testHostFixture.Services.GetService(); 31 | Assert.NotNull(adapter); 32 | } 33 | 34 | [Fact] 35 | public void ShouldUseAdapterAcrossMultipleScopesWithDbContextDirectly() 36 | { 37 | // Simulate the issue where an adapter is created in one scope 38 | // but used in another scope (like with casbin-aspnetcore) 39 | IAdapter adapter; 40 | 41 | // Create adapter with DbContext in first scope 42 | using (var scope1 = _testHostFixture.Services.CreateScope()) 43 | { 44 | var dbContext = scope1.ServiceProvider.GetRequiredService>(); 45 | dbContext.Database.EnsureCreated(); 46 | adapter = new EFCoreAdapter(dbContext); 47 | } 48 | 49 | // Try to use adapter after scope is disposed - this should throw ObjectDisposedException 50 | var model = _modelProvideFixture.GetNewRbacModel(); 51 | Assert.Throws(() => adapter.LoadPolicy(model)); 52 | } 53 | 54 | [Fact] 55 | public void ShouldUseAdapterAcrossMultipleScopesWithServiceProvider() 56 | { 57 | // Create adapter with IServiceProvider - this should work across multiple scopes 58 | var adapter = new EFCoreAdapter(_testHostFixture.Services); 59 | 60 | // Ensure database is created in first scope 61 | using (var scope1 = _testHostFixture.Services.CreateScope()) 62 | { 63 | var dbContext = scope1.ServiceProvider.GetRequiredService>(); 64 | dbContext.Database.EnsureCreated(); 65 | } 66 | 67 | // Use adapter after scope is disposed - this should work with IServiceProvider 68 | var model = _modelProvideFixture.GetNewRbacModel(); 69 | adapter.LoadPolicy(model); // Should not throw 70 | } 71 | 72 | [Fact] 73 | public void ShouldResolveAdapterRegisteredWithExtensionMethod() 74 | { 75 | // The adapter registered via AddEFCoreAdapter extension should be resolvable 76 | var adapter = _testHostFixture.Services.GetService(); 77 | Assert.NotNull(adapter); 78 | 79 | // Create scope to ensure database exists 80 | using (var scope = _testHostFixture.Services.CreateScope()) 81 | { 82 | var dbContext = scope.ServiceProvider.GetRequiredService>(); 83 | dbContext.Database.EnsureCreated(); 84 | } 85 | 86 | // Should be able to use the adapter 87 | var model = _modelProvideFixture.GetNewRbacModel(); 88 | adapter.LoadPolicy(model); // Should not throw 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/SpecialPolicyTest.cs: -------------------------------------------------------------------------------- 1 | using Casbin.Persist.Adapter.EFCore.UnitTest.Extensions; 2 | using Casbin.Model; 3 | using Casbin.Persist.Adapter.EFCore.Entities; 4 | using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; 5 | using Xunit; 6 | 7 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 8 | { 9 | public class PolicyEdgeCasesTest : TestUtil, IClassFixture, 10 | IClassFixture 11 | { 12 | private readonly ModelProvideFixture _modelProvideFixture; 13 | private readonly DbContextProviderFixture _dbContextProviderFixture; 14 | 15 | public PolicyEdgeCasesTest(ModelProvideFixture modelProvideFixture, 16 | DbContextProviderFixture dbContextProviderFixture) 17 | { 18 | _modelProvideFixture = modelProvideFixture; 19 | _dbContextProviderFixture = dbContextProviderFixture; 20 | } 21 | 22 | [Fact] 23 | public void TestCommaPolicy() 24 | { 25 | var context = _dbContextProviderFixture.GetContext("CommaPolicy"); 26 | context.Clear(); 27 | var adapter = new EFCoreAdapter(context); 28 | var enforcer = new Enforcer(DefaultModel.CreateFromText( 29 | """ 30 | [request_definition] 31 | r = _ 32 | 33 | [policy_definition] 34 | p = rule, a1, a2 35 | 36 | [policy_effect] 37 | e = some(where (p.eft == allow)) 38 | 39 | [matchers] 40 | m = eval(p.rule) 41 | """ 42 | ), adapter); 43 | enforcer.AddFunction("equal", (string a1, string a2) => a1 == a2); 44 | 45 | enforcer.AddPolicy("equal(p.a1, p.a2)", "a1", "a1"); 46 | Assert.True(enforcer.Enforce("_")); 47 | 48 | enforcer.LoadPolicy(); 49 | Assert.True(enforcer.Enforce("_")); 50 | 51 | enforcer.RemovePolicy("equal(p.a1, p.a2)", "a1", "a1"); 52 | enforcer.AddPolicy("equal(p.a1, p.a2)", "a1", "a2"); 53 | Assert.False(enforcer.Enforce("_")); 54 | 55 | enforcer.LoadPolicy(); 56 | Assert.False(enforcer.Enforce("_")); 57 | } 58 | 59 | [Fact] 60 | public void TestUnexpectedPolicy() 61 | { 62 | var context = _dbContextProviderFixture.GetContext("UnexpectedPolicy"); 63 | context.Clear(); 64 | context.Policies.Add(new EFCorePersistPolicy() 65 | { 66 | Type = "p", 67 | Value1 = "a1", 68 | Value2 = "a2", 69 | Value3 = null, 70 | }); 71 | context.Policies.Add(new EFCorePersistPolicy() 72 | { 73 | Type = "p", 74 | Value1 = "a1", 75 | Value2 = "a2", 76 | Value3 = "a3", 77 | }); 78 | context.Policies.Add(new EFCorePersistPolicy() 79 | { 80 | Type = "p", 81 | Value1 = "a1", 82 | Value2 = "a2", 83 | Value3 = "a3", 84 | Value4 = "a4", 85 | }); 86 | context.Policies.Add(new EFCorePersistPolicy() 87 | { 88 | Type = "p", 89 | Value1 = "b1", 90 | Value2 = "b2", 91 | Value3 = "b3", 92 | Value4 = "b4", 93 | }); 94 | context.SaveChanges(); 95 | 96 | var adapter = new EFCoreAdapter(context); 97 | var enforcer = new Enforcer(DefaultModel.CreateFromText( 98 | """ 99 | [request_definition] 100 | r = _ 101 | 102 | [policy_definition] 103 | p = a1, a2, a3 104 | 105 | [policy_effect] 106 | e = some(where (p.eft == allow)) 107 | 108 | [matchers] 109 | m = true 110 | """), adapter); 111 | 112 | enforcer.LoadPolicy(); 113 | var policies = enforcer.GetPolicy(); 114 | 115 | TestGetPolicy(enforcer, AsList( 116 | AsList("a1", "a2", ""), 117 | AsList("a1", "a2", "a3"), 118 | AsList("b1", "b2", "b3") 119 | )); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /EFCore-Adapter.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31808.319 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Casbin.Persist.Adapter.EFCore", "Casbin.Persist.Adapter.EFCore\Casbin.Persist.Adapter.EFCore.csproj", "{A4A027E0-A0FA-40A9-A558-D6C89CB832E7}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Casbin.Persist.Adapter.EFCore.UnitTest", "Casbin.Persist.Adapter.EFCore.UnitTest\Casbin.Persist.Adapter.EFCore.UnitTest.csproj", "{0687B869-B179-4C4F-8419-B7D9B7C2DB5C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C2C3B26F-11F2-4386-B51F-D6D125DCEF06}" 11 | ProjectSection(SolutionItems) = preProject 12 | .gitignore = .gitignore 13 | .github\workflows\build.yml = .github\workflows\build.yml 14 | LICENSE = LICENSE 15 | README.md = README.md 16 | .github\workflows\release.yml = .github\workflows\release.yml 17 | .releaserc.json = .releaserc.json 18 | EndProjectSection 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Casbin.Persist.Adapter.EFCore.IntegrationTest", "Casbin.Persist.Adapter.EFCore.IntegrationTest\Casbin.Persist.Adapter.EFCore.IntegrationTest.csproj", "{3D148107-651A-492F-BF76-C417FA37B368}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Debug|x64 = Debug|x64 26 | Debug|x86 = Debug|x86 27 | Release|Any CPU = Release|Any CPU 28 | Release|x64 = Release|x64 29 | Release|x86 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|x64.ActiveCfg = Debug|Any CPU 35 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|x64.Build.0 = Debug|Any CPU 36 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|x86.ActiveCfg = Debug|Any CPU 37 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Debug|x86.Build.0 = Debug|Any CPU 38 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|x64.ActiveCfg = Release|Any CPU 41 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|x64.Build.0 = Release|Any CPU 42 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|x86.ActiveCfg = Release|Any CPU 43 | {A4A027E0-A0FA-40A9-A558-D6C89CB832E7}.Release|x86.Build.0 = Release|Any CPU 44 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|x64.ActiveCfg = Debug|Any CPU 47 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|x64.Build.0 = Debug|Any CPU 48 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|x86.ActiveCfg = Debug|Any CPU 49 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Debug|x86.Build.0 = Debug|Any CPU 50 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|x64.ActiveCfg = Release|Any CPU 53 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|x64.Build.0 = Release|Any CPU 54 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|x86.ActiveCfg = Release|Any CPU 55 | {0687B869-B179-4C4F-8419-B7D9B7C2DB5C}.Release|x86.Build.0 = Release|Any CPU 56 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|x64.ActiveCfg = Debug|Any CPU 59 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|x64.Build.0 = Debug|Any CPU 60 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|x86.ActiveCfg = Debug|Any CPU 61 | {3D148107-651A-492F-BF76-C417FA37B368}.Debug|x86.Build.0 = Debug|Any CPU 62 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|x64.ActiveCfg = Release|Any CPU 65 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|x64.Build.0 = Release|Any CPU 66 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|x86.ActiveCfg = Release|Any CPU 67 | {3D148107-651A-492F-BF76-C417FA37B368}.Release|x86.Build.0 = Release|Any CPU 68 | EndGlobalSection 69 | GlobalSection(SolutionProperties) = preSolution 70 | HideSolutionNode = FALSE 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {5781F5F2-3BA0-4FDA-BDEF-246089CA80B9} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | env: 10 | SHA: ${{ GITHUB.SHA }} 11 | REF: ${{ GITHUB.REF }} 12 | RUN_ID: ${{ GITHUB.RUN_ID }} 13 | RUN_NUMBER: ${{ GITHUB.RUN_NUMBER }} 14 | BUILD_RUN_NUMBER: build.${{ GITHUB.RUN_NUMBER }} 15 | GITHUB_TOKEN: ${{ SECRETS.GITHUB_TOKEN }} 16 | COVERALLS_REPO_TOKEN: ${{ SECRETS.COVERALLS_REPO_TOKEN }} 17 | 18 | jobs: 19 | build: 20 | runs-on: windows-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup .NET SDK 29 | uses: actions/setup-dotnet@v2 30 | with: 31 | dotnet-version: | 32 | 3.1.x 33 | 5.0.x 34 | 6.0.x 35 | 7.0.x 36 | 8.0.x 37 | 9.0.x 38 | include-prerelease: true 39 | 40 | - name: Check .NET info 41 | run: dotnet --info 42 | 43 | - name: Install dependencies 44 | run: dotnet restore 45 | 46 | - name: Build solution 47 | run: dotnet build -c Release --no-restore 48 | 49 | - name: Test solution 50 | run: dotnet test -c Release --no-build --no-restore --verbosity normal --filter "Category!=Integration" --results-directory test-results --collect:"XPlat Code Coverage" ` 51 | -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=json,cobertura,lcov,teamcity,opencover 52 | 53 | - name: Upload coverage 54 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 55 | run: | 56 | dotnet tool install coveralls.net --version 3.0.0 --tool-path tools; 57 | $CommitAuthor = git show -s --pretty=format:"%cn"; 58 | echo "Coomit author is: $CommitAuthor"; 59 | $CommitAuthorEmail = git show -s --pretty=format:"%ce"; 60 | echo "Coomit author email is: $CommitAuthorEmail"; 61 | $CommitMessage = git show -s --pretty=format:"%s"; 62 | echo "Coomit message is: $CommitMessage"; 63 | cp test-results/**/*.opencover.xml test-results 64 | tools/csmacnz.Coveralls --opencover -i test-results/coverage.opencover.xml --repoToken $env:COVERALLS_REPO_TOKEN ` 65 | --commitId $env:SHA --commitBranch $env:REF --commitAuthor "$CommitAuthor" ` 66 | --commitEmail "$CommitAuthorEmail" --commitMessage "$CommitMessage" ` 67 | --jobId $env:RUN_NUMBER --serviceName github-actions --useRelativePaths; 68 | 69 | if($LastExitCode -ne 0) 70 | { 71 | Write-Warning -Message "Can not upload coverage, laat exit code is ${LastExitCode}." 72 | $LastExitCode = 0; 73 | } 74 | 75 | - name: Upload test results artefacts 76 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: "drop-ci-test-results" 80 | path: './test-results' 81 | 82 | dry-run-semantic-release: 83 | runs-on: ubuntu-latest 84 | needs: build 85 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 86 | 87 | steps: 88 | - name: Checkout 89 | uses: actions/checkout@v3 90 | 91 | - name: Dry run semantic-release 92 | run: | 93 | export PATH=$PATH:$(yarn global bin) 94 | yarn global add semantic-release@17.4.3 95 | semantic-release --dry-run 96 | 97 | release-build-version: 98 | runs-on: windows-latest 99 | needs: build 100 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 101 | 102 | steps: 103 | - name: Checkout 104 | uses: actions/checkout@v3 105 | with: 106 | fetch-depth: 0 107 | 108 | - name: Git fetch tags 109 | run: git fetch --tags 110 | 111 | - name: Check tags 112 | run: git tag -l -n 113 | 114 | - name: Setup .NET SDK 115 | uses: actions/setup-dotnet@v2 116 | with: 117 | dotnet-version: | 118 | 3.1.x 119 | 5.0.x 120 | 6.0.x 121 | 7.0.x 122 | 8.0.x 123 | 9.0.x 124 | include-prerelease: true 125 | 126 | - name: Check .NET info 127 | run: dotnet --info 128 | 129 | - name: Install dependencies 130 | run: dotnet restore 131 | 132 | - name: Build solution 133 | run: dotnet build -c Release --no-restore 134 | 135 | - name: Pack packages 136 | run: | 137 | $LastTag = git describe --tags (git rev-list --tags --max-count=1); 138 | echo "Last tag is: $LastTag"; 139 | $Version = ($LastTag).TrimStart('v'); 140 | echo "Publishing version: $Version"; 141 | $NowBranchName = git rev-parse --abbrev-ref HEAD; 142 | echo "Now branch name: $NowBranchName"; 143 | $PackageVersion = ($LastTag).TrimStart('v') + "-" + $env:BUILD_RUN_NUMBER + "." + $NowBranchName + "." + $env:SHA.SubString(0, 7); 144 | echo "Publishing package version: ${PackageVersion}"; 145 | dotnet pack -c Release -o packages /p:PackageVersion=$PackageVersion /p:Version=$Version; 146 | 147 | - name: Upload packages artefacts 148 | uses: actions/upload-artifact@v4 149 | with: 150 | name: "drop-ci-build-packages" 151 | path: './packages' 152 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | SHA: ${{ GITHUB.SHA }} 10 | REF: ${{ GITHUB.REF }} 11 | RUN_ID: ${{ GITHUB.RUN_ID }} 12 | RUN_NUMBER: ${{ GITHUB.RUN_NUMBER }} 13 | BUILD_RUN_NUMBER: build.${{ GITHUB.RUN_NUMBER }} 14 | GITHUB_TOKEN: ${{ SECRETS.GITHUB_TOKEN }} 15 | NUGET_API_TOKEN: ${{ SECRETS.NUGET_API_KEY }} 16 | COVERALLS_REPO_TOKEN: ${{ SECRETS.COVERALLS_REPO_TOKEN }} 17 | 18 | jobs: 19 | build: 20 | runs-on: windows-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Setup .NET SDK 29 | uses: actions/setup-dotnet@v2 30 | with: 31 | dotnet-version: | 32 | 3.1.x 33 | 5.0.x 34 | 6.0.x 35 | 7.0.x 36 | 8.0.x 37 | 9.0.x 38 | include-prerelease: true 39 | 40 | - name: Check .NET info 41 | run: dotnet --info 42 | 43 | - name: Install dependencies 44 | run: dotnet restore 45 | 46 | - name: Build solution 47 | run: dotnet build -c Release --no-restore 48 | 49 | - name: Test solution 50 | run: dotnet test -c Release --no-build --no-restore --verbosity normal --filter "Category!=Integration" --results-directory test-results --collect:"XPlat Code Coverage" ` 51 | -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=json,cobertura,lcov,teamcity,opencover 52 | 53 | - name: Upload coverage 54 | run: | 55 | dotnet tool install coveralls.net --version 3.0.0 --tool-path tools; 56 | $CommitAuthor = git show -s --pretty=format:"%cn"; 57 | echo "Coomit author is: $CommitAuthor"; 58 | $CommitAuthorEmail = git show -s --pretty=format:"%ce"; 59 | echo "Coomit author email is: $CommitAuthorEmail"; 60 | $CommitMessage = git show -s --pretty=format:"%s"; 61 | echo "Coomit message is: $CommitMessage"; 62 | cp test-results/**/*.opencover.xml test-results 63 | tools/csmacnz.Coveralls --opencover -i test-results/coverage.opencover.xml --repoToken $env:COVERALLS_REPO_TOKEN ` 64 | --commitId $env:SHA --commitBranch $env:REF --commitAuthor "$CommitAuthor" ` 65 | --commitEmail "$CommitAuthorEmail" --commitMessage "$CommitMessage" ` 66 | --jobId $env:RUN_NUMBER --serviceName github-actions --useRelativePaths; 67 | 68 | if($LastExitCode -ne 0) 69 | { 70 | Write-Warning -Message "Can not upload coverage, laat exit code is ${LastExitCode}." 71 | $LastExitCode = 0; 72 | } 73 | 74 | - name: Upload test results artefacts 75 | uses: actions/upload-artifact@v4 76 | with: 77 | name: "drop-ci-test-results" 78 | path: './test-results' 79 | 80 | run-semantic-release: 81 | runs-on: ubuntu-latest 82 | needs: build 83 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 84 | 85 | steps: 86 | - name: Checkout 87 | uses: actions/checkout@v3 88 | 89 | - name: Run semantic-release 90 | run: | 91 | export PATH=$PATH:$(yarn global bin) 92 | yarn global add semantic-release@17.4.3 93 | semantic-release 94 | 95 | release: 96 | runs-on: windows-latest 97 | needs: run-semantic-release 98 | if: github.repository_owner == 'casbin-net' && github.event_name == 'push' 99 | 100 | steps: 101 | - name: Checkout 102 | uses: actions/checkout@v3 103 | with: 104 | fetch-depth: 0 105 | 106 | - name: Git fetch tags 107 | run: git fetch --tags 108 | 109 | - name: Check tags 110 | run: git tag -l -n 111 | 112 | - name: Setup .NET SDK 113 | uses: actions/setup-dotnet@v2 114 | with: 115 | dotnet-version: | 116 | 3.1.x 117 | 5.0.x 118 | 6.0.x 119 | 7.0.x 120 | 8.0.x 121 | 9.0.x 122 | include-prerelease: true 123 | 124 | - name: Check .NET info 125 | run: dotnet --info 126 | 127 | - name: Install dependencies 128 | run: dotnet restore 129 | 130 | - name: Build solution 131 | run: dotnet build -c Release --no-restore 132 | 133 | - name: Pack packages 134 | run: | 135 | $LastTag = git describe --tags (git rev-list --tags --max-count=1); 136 | echo "Last tag is: $LastTag"; 137 | $Version = ($LastTag).TrimStart('v'); 138 | echo "Publishing version: $Version"; 139 | dotnet pack -c Release -o packages /p:PackageVersion=$Version /p:Version=$Version; 140 | 141 | - name: Upload packages artefacts 142 | uses: actions/upload-artifact@v4 143 | with: 144 | name: "drop-ci-packages" 145 | path: './packages' 146 | 147 | - name: Add github nuget source 148 | run: dotnet nuget add source https://nuget.pkg.github.com/casbin-net/index.json --name github.com --username casbin-net --password $env:GITHUB_TOKEN 149 | 150 | - name: Push packages to github.com 151 | run: dotnet nuget push .\packages\*.nupkg -s github.com --skip-duplicate; 152 | 153 | - name: Push packages to nuget.org 154 | run: dotnet nuget push .\packages\*.nupkg -s nuget.org -k $env:NUGET_API_TOKEN --skip-duplicate 155 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/TestUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Xunit; 5 | 6 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 7 | { 8 | public class TestUtil 9 | { 10 | internal List AsList(params T[] values) 11 | { 12 | return values.ToList(); 13 | } 14 | internal List AsList(params string[] values) 15 | { 16 | return values.ToList(); 17 | } 18 | 19 | private static bool SetEquals(List a, IEnumerable b) 20 | { 21 | if (a == null) 22 | a = new List(); 23 | var c = new List(); 24 | if (b != null) 25 | c = b.ToList(); 26 | if (a.Count != c.Count) 27 | return false; 28 | a.Sort(); 29 | c.Sort(); 30 | for (int index = 0; index < a.Count; ++index) 31 | { 32 | if (!a[index].Equals(c[index])) 33 | return false; 34 | } 35 | return true; 36 | } 37 | 38 | private static bool ArrayEquals(List a, IEnumerable b) 39 | { 40 | if (a == null || b == null) 41 | return false; 42 | if (a.Count != b.Count()) 43 | return false; 44 | var c = b.ToList(); 45 | for (int index = 0; index < a.Count; ++index) 46 | { 47 | if (!a[index].Equals(c[index])) 48 | return false; 49 | } 50 | return true; 51 | } 52 | 53 | private static bool Array2DEquals(List> a, IEnumerable> b) 54 | { 55 | if (a == null) 56 | a = new List>(); 57 | var c = new List>(); 58 | if (b != null) 59 | c = b.ToList(); 60 | if (a.Count != c.Count) 61 | return false; 62 | for (int index = 0; index < a.Count; ++index) 63 | { 64 | if (!ArrayEquals(a[index], c[index])) 65 | return false; 66 | } 67 | return true; 68 | } 69 | 70 | internal static void TestEnforce(Enforcer e, String sub, Object obj, String act, Boolean res) 71 | { 72 | Assert.Equal(res, e.Enforce(sub, obj, act)); 73 | } 74 | 75 | internal static void TestEnforceWithoutUsers(Enforcer e, String obj, String act, Boolean res) 76 | { 77 | Assert.Equal(res, e.Enforce(obj, act)); 78 | } 79 | 80 | internal static void TestDomainEnforce(Enforcer e, String sub, String dom, String obj, String act, Boolean res) 81 | { 82 | Assert.Equal(res, e.Enforce(sub, dom, obj, act)); 83 | } 84 | 85 | internal static void TestGetPolicy(Enforcer e, List> res) 86 | { 87 | IEnumerable> myRes = e.GetPolicy(); 88 | Assert.True(Array2DEquals(res, myRes)); 89 | } 90 | 91 | internal static void TestGetFilteredPolicy(Enforcer e, int fieldIndex, List> res, params string[] fieldValues) 92 | { 93 | IEnumerable> myRes = e.GetFilteredPolicy(fieldIndex, fieldValues); 94 | Assert.True(Array2DEquals(res, myRes)); 95 | } 96 | 97 | internal static void TestGetGroupingPolicy(Enforcer e, List> res) 98 | { 99 | IEnumerable> myRes = e.GetGroupingPolicy(); 100 | Assert.Equal(res, myRes); 101 | } 102 | 103 | internal static void TestGetFilteredGroupingPolicy(Enforcer e, int fieldIndex, List> res, params string[] fieldValues) 104 | { 105 | IEnumerable> myRes = e.GetFilteredGroupingPolicy(fieldIndex, fieldValues); 106 | Assert.Equal(res, myRes); 107 | } 108 | 109 | internal static void TestHasPolicy(Enforcer e, List policy, Boolean res) 110 | { 111 | Boolean myRes = e.HasPolicy(policy); 112 | Assert.Equal(res, myRes); 113 | } 114 | 115 | internal static void TestHasGroupingPolicy(Enforcer e, List policy, Boolean res) 116 | { 117 | Boolean myRes = e.HasGroupingPolicy(policy); 118 | Assert.Equal(res, myRes); 119 | } 120 | 121 | internal static void TestGetRoles(Enforcer e, String name, List res) 122 | { 123 | IEnumerable myRes = e.GetRolesForUser(name); 124 | string message = "Roles for " + name + ": " + myRes + ", supposed to be " + res; 125 | Assert.True(SetEquals(res, myRes), message); 126 | } 127 | 128 | internal static void TestGetUsers(Enforcer e, String name, List res) 129 | { 130 | IEnumerable myRes = e.GetUsersForRole(name); 131 | var message = "Users for " + name + ": " + myRes + ", supposed to be " + res; 132 | Assert.True(SetEquals(res, myRes),message); 133 | } 134 | 135 | internal static void TestHasRole(Enforcer e, String name, String role, Boolean res) 136 | { 137 | Boolean myRes = e.HasRoleForUser(name, role); 138 | Assert.Equal(res, myRes); 139 | } 140 | 141 | internal static void TestGetPermissions(Enforcer e, String name, List> res) 142 | { 143 | IEnumerable> myRes = e.GetPermissionsForUser(name); 144 | var message = "Permissions for " + name + ": " + myRes + ", supposed to be " + res; 145 | Assert.True(Array2DEquals(res, myRes)); 146 | } 147 | 148 | internal static void TestHasPermission(Enforcer e, String name, List permission, Boolean res) 149 | { 150 | Boolean myRes = e.HasPermissionForUser(name, permission); 151 | Assert.Equal(res, myRes); 152 | } 153 | 154 | internal static void TestGetRolesInDomain(Enforcer e, String name, String domain, List res) 155 | { 156 | IEnumerable myRes = e.GetRolesForUserInDomain(name, domain); 157 | var message = "Roles for " + name + " under " + domain + ": " + myRes + ", supposed to be " + res; 158 | Assert.True(SetEquals(res, myRes), message); 159 | } 160 | 161 | internal static void TestGetPermissionsInDomain(Enforcer e, String name, String domain, List> res) 162 | { 163 | IEnumerable> myRes = e.GetPermissionsForUserInDomain(name, domain); 164 | Assert.True(Array2DEquals(res, myRes), "Permissions for " + name + " under " + domain + ": " + myRes + ", supposed to be " + res); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | coverage.opencover.xml 12 | *.sqlite3 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | [Tt]ools/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | **/Properties/launchSettings.json 59 | 60 | # StyleCop 61 | StyleCopReport.xml 62 | 63 | # Files built by Visual Studio 64 | *_i.c 65 | *_p.c 66 | *_i.h 67 | *.ilk 68 | *.meta 69 | *.obj 70 | *.iobj 71 | *.pch 72 | *.pdb 73 | *.ipdb 74 | *.pgc 75 | *.pgd 76 | *.rsp 77 | *.sbr 78 | *.tlb 79 | *.tli 80 | *.tlh 81 | *.tmp 82 | *.tmp_proj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | 259 | # Microsoft Fakes 260 | FakesAssemblies/ 261 | 262 | # GhostDoc plugin setting file 263 | *.GhostDoc.xml 264 | 265 | # Node.js Tools for Visual Studio 266 | .ntvs_analysis.dat 267 | node_modules/ 268 | 269 | # Visual Studio 6 build log 270 | *.plg 271 | 272 | # Visual Studio 6 workspace options file 273 | *.opt 274 | 275 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 276 | *.vbw 277 | 278 | # Visual Studio LightSwitch build output 279 | **/*.HTMLClient/GeneratedArtifacts 280 | **/*.DesktopClient/GeneratedArtifacts 281 | **/*.DesktopClient/ModelManifest.xml 282 | **/*.Server/GeneratedArtifacts 283 | **/*.Server/ModelManifest.xml 284 | _Pvt_Extensions 285 | 286 | # Paket dependency manager 287 | .paket/paket.exe 288 | paket-files/ 289 | 290 | # FAKE - F# Make 291 | .fake/ 292 | 293 | # JetBrains Rider 294 | .idea/ 295 | *.sln.iml 296 | 297 | # CodeRush 298 | .cr/ 299 | 300 | # Python Tools for Visual Studio (PTVS) 301 | __pycache__/ 302 | *.pyc 303 | 304 | # Cake - Uncomment if you are using it 305 | # tools/** 306 | # !tools/packages.config 307 | 308 | # Tabs Studio 309 | *.tss 310 | 311 | # Telerik's JustMock configuration file 312 | *.jmconfig 313 | 314 | # BizTalk build output 315 | *.btp.cs 316 | *.btm.cs 317 | *.odx.cs 318 | *.xsd.cs 319 | 320 | # OpenCover UI analysis results 321 | OpenCover/ 322 | 323 | # Azure Stream Analytics local run output 324 | ASALocalRun/ 325 | 326 | # MSBuild Binary and Structured Log 327 | *.binlog 328 | 329 | # NVidia Nsight GPU debugger configuration file 330 | *.nvuser 331 | 332 | # MFractors (Xamarin productivity tool) working folder 333 | .mfractor/ 334 | .vscode/ 335 | nul 336 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EF Core Adapter 2 | 3 | [![Build Status](https://github.com/casbin-net/efcore-adapter/workflows/Build/badge.svg)](https://github.com/casbin-net/efcore-adapter/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/casbin-net/EFCore-Adapter/badge.svg?branch=master)](https://coveralls.io/github/casbin-net/EFCore-Adapter?branch=master) 5 | [![Nuget](https://img.shields.io/nuget/v/Casbin.NET.Adapter.EFCore.svg)](https://www.nuget.org/packages/Casbin.NET.Adapter.EFCore/) 6 | [![Release](https://img.shields.io/github/release/casbin-net/efcore-adapter.svg)](https://github.com/casbin-net/efcore-adapter/releases/latest) 7 | [![Nuget](https://img.shields.io/nuget/dt/Casbin.NET.Adapter.EFCore.svg)](https://www.nuget.org/packages/Casbin.NET.Adapter.EFCore/) 8 | [![Discord](https://img.shields.io/discord/1022748306096537660?logo=discord&label=discord&color=5865F2)](https://discord.gg/S5UjpzGZjN) 9 | 10 | EF Core Adapter is the [EF Core](https://docs.microsoft.com/en-gb/ef/) adapter for [Casbin](https://github.com/casbin/casbin). With this library, Casbin can load policy from EF Core supported database or save policy to it. 11 | 12 | The current version supported all databases which EF Core supported, there is a part list: 13 | 14 | - SQL Server 2012 onwards 15 | - SQLite 3.7 onwards 16 | - Azure Cosmos DB SQL API 17 | - PostgreSQL 18 | - MySQL, MariaDB 19 | - Oracle DB 20 | - Db2, Informix 21 | - And more... 22 | 23 | You can see all the list at [Database Providers](https://docs.microsoft.com/en-gb/ef/core/providers). 24 | 25 | ## Installation 26 | ``` 27 | dotnet add package Casbin.NET.Adapter.EFCore 28 | ``` 29 | 30 | ## Supported Frameworks 31 | 32 | The adapter supports the following .NET target frameworks: 33 | - .NET 9.0 34 | - .NET 8.0 35 | - .NET 7.0 36 | - .NET 6.0 37 | - .NET 5.0 38 | - .NET Core 3.1 39 | 40 | ## Simple Example 41 | 42 | ```csharp 43 | using Casbin.Adapter.EFCore; 44 | using Microsoft.EntityFrameworkCore; 45 | using NetCasbin; 46 | 47 | namespace ConsoleAppExample 48 | { 49 | public class Program 50 | { 51 | public static void Main(string[] args) 52 | { 53 | // You should build a DbContextOptions for CasbinDbContext. 54 | // The example use the SQLite database named "casbin_example.sqlite3". 55 | var options = new DbContextOptionsBuilder>() 56 | .UseSqlite("Data Source=casbin_example.sqlite3") 57 | .Options; 58 | var context = new CasbinDbContext(options); 59 | 60 | // If it doesn't exist, you can use this to create it automatically. 61 | context.Database.EnsureCreated(); 62 | 63 | // Initialize a EF Core adapter and use it in a Casbin enforcer: 64 | var efCoreAdapter = new EFCoreAdapter(context); 65 | var e = new Enforcer("examples/rbac_model.conf", efCoreAdapter); 66 | 67 | // Load the policy from DB. 68 | e.LoadPolicy(); 69 | 70 | // Check the permission. 71 | e.Enforce("alice", "data1", "read"); 72 | 73 | // Modify the policy. 74 | // e.AddPolicy(...) 75 | // e.RemovePolicy(...) 76 | 77 | // Save the policy back to DB. 78 | e.SavePolicy(); 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | ## Using with Dependency Injection 85 | 86 | When using the adapter with dependency injection (e.g., in ASP.NET Core), you should use the `IServiceProvider` constructor or the extension method to avoid issues with disposed DbContext instances. 87 | 88 | ### Recommended Approach (Using Extension Method) 89 | 90 | ```csharp 91 | using Casbin.Persist.Adapter.EFCore; 92 | using Casbin.Persist.Adapter.EFCore.Extensions; 93 | using Microsoft.EntityFrameworkCore; 94 | using Microsoft.Extensions.DependencyInjection; 95 | 96 | // Register services 97 | services.AddDbContext>(options => 98 | options.UseSqlServer(connectionString)); 99 | 100 | // Register the adapter using the extension method 101 | services.AddEFCoreAdapter(); 102 | 103 | // The adapter will resolve the DbContext from the service provider on each operation, 104 | // preventing issues with disposed contexts when used with long-lived services. 105 | ``` 106 | 107 | ### Alternative Approach (Using IServiceProvider Constructor) 108 | 109 | ```csharp 110 | // In your startup configuration 111 | services.AddDbContext>(options => 112 | options.UseSqlServer(connectionString)); 113 | 114 | services.AddCasbinAuthorization(options => 115 | { 116 | options.DefaultModelPath = "model.conf"; 117 | 118 | // Use the IServiceProvider constructor 119 | options.DefaultEnforcerFactory = (sp, model) => 120 | new Enforcer(model, new EFCoreAdapter(sp)); 121 | }); 122 | ``` 123 | 124 | This approach resolves the DbContext from the service provider on each database operation, ensuring that: 125 | - The adapter works correctly with scoped DbContext instances 126 | - No `ObjectDisposedException` is thrown when the adapter outlives the scope that created it 127 | - The adapter can be used in long-lived services like singletons 128 | 129 | ## Multi-Context Support 130 | 131 | The adapter supports storing different policy types in separate database contexts, allowing you to: 132 | - Store policies (p, p2, etc.) and groupings (g, g2, etc.) in different schemas and/or tables 133 | - Each Context can control both schema AND table independently 134 | - Separate data for multi-tenant or compliance scenarios 135 | 136 | ### Quick Example 137 | 138 | ```csharp 139 | // Create ONE shared connection object 140 | var sharedConnection = new SqlConnection(connectionString); 141 | 142 | // Create contexts with shared connection 143 | var policyContext = new CasbinDbContext( 144 | new DbContextOptionsBuilder>() 145 | .UseSqlServer(sharedConnection).Options, // Shared connection 146 | schemaName: "policies"); 147 | 148 | var groupingContext = new CasbinDbContext( 149 | new DbContextOptionsBuilder>() 150 | .UseSqlServer(sharedConnection).Options, // Same connection 151 | schemaName: "groupings"); 152 | 153 | // Create a provider that routes policy types to contexts 154 | var provider = new PolicyTypeContextProvider(policyContext, groupingContext); 155 | 156 | // Use the provider with the adapter 157 | var adapter = new EFCoreAdapter(provider); 158 | var enforcer = new Enforcer("rbac_model.conf", adapter); 159 | 160 | // All operations work transparently across contexts 161 | enforcer.AddPolicy("alice", "data1", "read"); // → policyContext 162 | enforcer.AddGroupingPolicy("alice", "admin"); // → groupingContext 163 | enforcer.SavePolicy(); // Atomic across both 164 | ``` 165 | 166 | > **⚠️ Transaction Integrity Requirements** 167 | > 168 | > For atomic multi-context operations: 169 | > 1. **Share DbConnection:** All contexts must use the **same `DbConnection` object** (reference equality) 170 | > 2. **Disable AutoSave:** Use `enforcer.EnableAutoSave(false)` and call `SavePolicyAsync()` to batch commit 171 | > 3. **Supported databases:** PostgreSQL, MySQL, SQL Server, SQLite (same file) 172 | > 173 | > **Why disable AutoSave?** With `EnableAutoSave(true)` (default), each policy operation commits immediately and independently. If a later operation fails, earlier operations remain committed. With `EnableAutoSave(false)`, all changes stay in memory until `SavePolicyAsync()` commits them atomically across all contexts using a shared connection-level transaction. 174 | > 175 | > - ✅ **Atomic:** Same `DbConnection` object + `EnableAutoSave(false)` + `SavePolicyAsync()` 176 | > - ❌ **Not Atomic:** AutoSave ON, separate `DbConnection` objects, different databases 177 | > 178 | > See detailed explanation in [EnableAutoSave and Transaction Atomicity](MULTI_CONTEXT_USAGE_GUIDE.md#enableautosave-and-transaction-atomicity). 179 | 180 | ### Documentation 181 | 182 | - **[Multi-Context Usage Guide](MULTI_CONTEXT_USAGE_GUIDE.md)** - Complete step-by-step guide with examples 183 | - **[Multi-Context Design](MULTI_CONTEXT_DESIGN.md)** - Detailed design documentation and limitations 184 | - **[Integration Tests Setup](Casbin.Persist.Adapter.EFCore.UnitTest/Integration/README.md)** - How to run transaction integrity tests locally 185 | 186 | ## Getting Help 187 | 188 | - [Casbin.NET](https://github.com/casbin/Casbin.NET) 189 | 190 | ## License 191 | 192 | This project is under Apache 2.0 License. See the [LICENSE](LICENSE) file for the full license text. -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/Integration/TransactionIntegrityTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Npgsql; 4 | using Xunit; 5 | 6 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Integration 7 | { 8 | /// 9 | /// Test fixture for transaction integrity tests using PostgreSQL. 10 | /// Creates three separate schemas to simulate multi-context scenarios. 11 | /// 12 | /// Prerequisites: 13 | /// - PostgreSQL running on localhost:5432 14 | /// - Database "casbin_integration_test" must exist 15 | /// - Default credentials: postgres/postgres4all! (or update ConnectionString) 16 | /// 17 | public class TransactionIntegrityTestFixture : IAsyncLifetime 18 | { 19 | // Schema names for three-way context split 20 | public const string PoliciesSchema = "casbin_policies"; 21 | public const string GroupingsSchema = "casbin_groupings"; 22 | public const string RolesSchema = "casbin_roles"; 23 | 24 | // Connection string to local PostgreSQL 25 | public string ConnectionString { get; private set; } 26 | 27 | public TransactionIntegrityTestFixture() 28 | { 29 | // Use local PostgreSQL for integration tests 30 | // Database must exist before running tests 31 | ConnectionString = "Host=localhost;Database=casbin_integration_test;Username=postgres;Password=postgres4all!"; 32 | } 33 | 34 | public async Task InitializeAsync() 35 | { 36 | try 37 | { 38 | // Create schemas 39 | await CreateSchemasAsync(); 40 | 41 | // Run migrations for all three schemas 42 | await RunMigrationsAsync(); 43 | } 44 | catch (Exception ex) 45 | { 46 | throw new InvalidOperationException( 47 | $"Failed to initialize TransactionIntegrityTestFixture. " + 48 | $"Ensure PostgreSQL is running and database 'casbin_integration_test' exists. " + 49 | $"Connection string: {ConnectionString}", ex); 50 | } 51 | } 52 | 53 | public async Task DisposeAsync() 54 | { 55 | // Clean up test schemas 56 | // TEMPORARILY DISABLED: Comment out to leave tables for inspection 57 | // await DropSchemasAsync(); 58 | await Task.CompletedTask; 59 | } 60 | 61 | private async Task CreateSchemasAsync() 62 | { 63 | await using var connection = new NpgsqlConnection(ConnectionString); 64 | await connection.OpenAsync(); 65 | 66 | await using var cmd = connection.CreateCommand(); 67 | 68 | // Create policies schema 69 | cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS {PoliciesSchema}"; 70 | await cmd.ExecuteNonQueryAsync(); 71 | 72 | // Create groupings schema 73 | cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS {GroupingsSchema}"; 74 | await cmd.ExecuteNonQueryAsync(); 75 | 76 | // Create roles schema 77 | cmd.CommandText = $"CREATE SCHEMA IF NOT EXISTS {RolesSchema}"; 78 | await cmd.ExecuteNonQueryAsync(); 79 | } 80 | 81 | /// 82 | /// Runs migrations for all schemas. Public so tests can restore tables after dropping them. 83 | /// 84 | public async Task RunMigrationsAsync() 85 | { 86 | await using var connection = new NpgsqlConnection(ConnectionString); 87 | await connection.OpenAsync(); 88 | 89 | foreach (var schemaName in new[] { PoliciesSchema, GroupingsSchema, RolesSchema }) 90 | { 91 | await using var cmd = connection.CreateCommand(); 92 | cmd.CommandText = $@" 93 | CREATE TABLE IF NOT EXISTS {schemaName}.casbin_rule ( 94 | id SERIAL PRIMARY KEY, 95 | ptype VARCHAR(254) NOT NULL, 96 | v0 VARCHAR(254), 97 | v1 VARCHAR(254), 98 | v2 VARCHAR(254), 99 | v3 VARCHAR(254), 100 | v4 VARCHAR(254), 101 | v5 VARCHAR(254), 102 | v6 VARCHAR(254), 103 | v7 VARCHAR(254), 104 | v8 VARCHAR(254), 105 | v9 VARCHAR(254), 106 | v10 VARCHAR(254), 107 | v11 VARCHAR(254), 108 | v12 VARCHAR(254), 109 | v13 VARCHAR(254) 110 | ); 111 | CREATE INDEX IF NOT EXISTS ix_casbin_rule_ptype ON {schemaName}.casbin_rule (ptype); 112 | "; 113 | await cmd.ExecuteNonQueryAsync(); 114 | } 115 | } 116 | 117 | private async Task DropSchemasAsync() 118 | { 119 | await using var connection = new NpgsqlConnection(ConnectionString); 120 | await connection.OpenAsync(); 121 | 122 | await using var cmd = connection.CreateCommand(); 123 | 124 | // Drop tables first, then schemas 125 | foreach (var schema in new[] { PoliciesSchema, GroupingsSchema, RolesSchema }) 126 | { 127 | cmd.CommandText = $"DROP TABLE IF EXISTS {schema}.casbin_rule CASCADE"; 128 | await cmd.ExecuteNonQueryAsync(); 129 | 130 | cmd.CommandText = $"DROP SCHEMA IF EXISTS {schema} CASCADE"; 131 | await cmd.ExecuteNonQueryAsync(); 132 | } 133 | } 134 | 135 | /// 136 | /// Clears all policies from all schemas. Call before each test. 137 | /// 138 | public async Task ClearAllPoliciesAsync() 139 | { 140 | await using var connection = new NpgsqlConnection(ConnectionString); 141 | await connection.OpenAsync(); 142 | 143 | await using var cmd = connection.CreateCommand(); 144 | 145 | foreach (var schema in new[] { PoliciesSchema, GroupingsSchema, RolesSchema }) 146 | { 147 | cmd.CommandText = $"DELETE FROM {schema}.casbin_rule"; 148 | try 149 | { 150 | await cmd.ExecuteNonQueryAsync(); 151 | } 152 | catch (NpgsqlException) 153 | { 154 | // Table might not exist yet, ignore 155 | } 156 | } 157 | } 158 | 159 | /// 160 | /// Counts policies of a specific type in a schema 161 | /// 162 | public async Task CountPoliciesInSchemaAsync(string schemaName, string policyType = null) 163 | { 164 | await using var connection = new NpgsqlConnection(ConnectionString); 165 | await connection.OpenAsync(); 166 | 167 | await using var cmd = connection.CreateCommand(); 168 | if (policyType == null) 169 | { 170 | cmd.CommandText = $"SELECT COUNT(*) FROM {schemaName}.casbin_rule"; 171 | } 172 | else 173 | { 174 | cmd.CommandText = $"SELECT COUNT(*) FROM {schemaName}.casbin_rule WHERE ptype = @ptype"; 175 | cmd.Parameters.AddWithValue("@ptype", policyType); 176 | } 177 | 178 | try 179 | { 180 | var result = await cmd.ExecuteScalarAsync(); 181 | return Convert.ToInt32(result); 182 | } 183 | catch (NpgsqlException) 184 | { 185 | // Table might not exist 186 | return 0; 187 | } 188 | } 189 | 190 | /// 191 | /// Inserts a policy directly into the database (for conflict simulation) 192 | /// 193 | public async Task InsertPolicyDirectlyAsync(string schemaName, string ptype, params string[] values) 194 | { 195 | await using var connection = new NpgsqlConnection(ConnectionString); 196 | await connection.OpenAsync(); 197 | 198 | await using var cmd = connection.CreateCommand(); 199 | cmd.CommandText = $@" 200 | INSERT INTO {schemaName}.casbin_rule 201 | (ptype, v0, v1, v2, v3, v4, v5) 202 | VALUES (@ptype, @v0, @v1, @v2, @v3, @v4, @v5)"; 203 | 204 | cmd.Parameters.AddWithValue("@ptype", ptype); 205 | for (int i = 0; i < 6; i++) 206 | { 207 | var value = i < values.Length ? values[i] : (object)DBNull.Value; 208 | cmd.Parameters.AddWithValue($"@v{i}", value); 209 | } 210 | 211 | await cmd.ExecuteNonQueryAsync(); 212 | } 213 | 214 | /// 215 | /// Drops a table in a schema (for failure simulation) 216 | /// 217 | public async Task DropTableAsync(string schemaName) 218 | { 219 | await using var connection = new NpgsqlConnection(ConnectionString); 220 | await connection.OpenAsync(); 221 | 222 | await using var cmd = connection.CreateCommand(); 223 | cmd.CommandText = $"DROP TABLE IF EXISTS {schemaName}.casbin_rule CASCADE"; 224 | await cmd.ExecuteNonQueryAsync(); 225 | } 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/Integration/README.md: -------------------------------------------------------------------------------- 1 | # Integration Tests for Multi-Context Transaction Integrity 2 | 3 | This directory contains integration tests that verify the transaction integrity guarantees of the multi-context EFCore adapter feature. 4 | 5 | ## Separate Test Project 6 | 7 | These integration tests are in a **separate test project** (`Casbin.Persist.Adapter.EFCore.IntegrationTest`) to enable sequential framework execution. 8 | 9 | **Why separate?** 10 | - Integration tests run frameworks **sequentially** (one at a time) to avoid PostgreSQL database conflicts 11 | - Unit tests continue running frameworks **in parallel** for faster execution 12 | - .NET 9+ runs multi-targeted tests in parallel by default - this separation allows different configurations 13 | 14 | **Project Settings:** 15 | - `false` - Frameworks execute sequentially 16 | - Shares single PostgreSQL database: `casbin_integration_test` 17 | - Uses `DisableParallelization = true` on test collection for within-framework sequencing 18 | 19 | ## Purpose 20 | 21 | These tests prove that when multiple `DbContext` instances share the same `DbConnection` object, operations across contexts are **atomic** - they either all succeed or all fail together. 22 | 23 | ## Prerequisites 24 | 25 | ### 1. PostgreSQL Installation 26 | 27 | You need PostgreSQL running locally on your development machine. 28 | 29 | **Install PostgreSQL:** 30 | - **Windows**: Download from [postgresql.org](https://www.postgresql.org/download/windows/) 31 | - **macOS**: `brew install postgresql@17` (or use [Postgres.app](https://postgresapp.com/)) 32 | - **Linux**: `sudo apt-get install postgresql` (Debian/Ubuntu) or equivalent 33 | 34 | ### 2. Database Setup 35 | 36 | Create the test database: 37 | 38 | ```bash 39 | # Connect to PostgreSQL (default superuser is 'postgres') 40 | psql -U postgres 41 | 42 | # Create the test database 43 | CREATE DATABASE casbin_integration_test; 44 | 45 | # Exit psql 46 | \q 47 | ``` 48 | 49 | Alternatively, use a one-liner: 50 | 51 | ```bash 52 | psql -U postgres -c "CREATE DATABASE casbin_integration_test;" 53 | ``` 54 | 55 | ### 3. Connection Credentials 56 | 57 | The tests use these default credentials: 58 | - **Host**: `localhost:5432` 59 | - **Database**: `casbin_integration_test` 60 | - **Username**: `postgres` 61 | - **Password**: `postgres4all!` 62 | 63 | **If your PostgreSQL uses different credentials**, update the connection string in [TransactionIntegrityTestFixture.cs](TransactionIntegrityTestFixture.cs): 64 | 65 | ```csharp 66 | ConnectionString = "Host=localhost;Database=casbin_integration_test;Username=YOUR_USER;Password=YOUR_PASSWORD"; 67 | ``` 68 | 69 | ## Running the Tests 70 | 71 | ### Run All Integration Tests 72 | 73 | ```bash 74 | dotnet test --filter "Category=Integration" 75 | ``` 76 | 77 | ### Run a Specific Test 78 | 79 | ```bash 80 | dotnet test --filter "FullyQualifiedName~SavePolicy_WithSharedConnection_ShouldWriteToAllContextsAtomically" 81 | ``` 82 | 83 | ### Run with Specific Framework 84 | 85 | ```bash 86 | dotnet test --filter "Category=Integration" -f net9.0 87 | ``` 88 | 89 | ## Test Architecture 90 | 91 | ### Test Fixture 92 | 93 | The [TransactionIntegrityTestFixture](TransactionIntegrityTestFixture.cs) automatically: 94 | 1. Creates 3 PostgreSQL schemas: `casbin_policies`, `casbin_groupings`, `casbin_roles` 95 | 2. Creates tables in each schema 96 | 3. Clears all data before each test 97 | 4. Cleans up schemas after all tests complete 98 | 99 | ### Test Organization 100 | 101 | The integration tests are organized into 3 test classes: 102 | 103 | | Test Class | Tests | Purpose | 104 | |------------|-------|---------| 105 | | `TransactionIntegrityTests` | 7 | Multi-context transaction atomicity and rollback | 106 | | `AutoSaveTests` | 10 | Casbin.NET AutoSave behavior verification | 107 | | `SchemaDistributionTests` | 2 | Schema routing with shared connections | 108 | 109 | **Total:** 19 integration tests 110 | 111 | The tests use a three-way context provider that routes: 112 | - **p policies** → `casbin_policies` schema 113 | - **g groupings** → `casbin_groupings` schema 114 | - **g2 roles** → `casbin_roles` schema 115 | 116 | This simulates real-world multi-context scenarios where different policy types are stored separately for compliance, multi-tenancy, or organizational requirements. 117 | 118 | ## Test Coverage 119 | 120 | | Test | What It Proves | 121 | |------|----------------| 122 | | `SavePolicy_WithSharedConnection_ShouldWriteToAllContextsAtomically` | Policies written to 3 schemas in a single atomic transaction | 123 | | `MultiContextSetup_WithSharedConnection_ShouldShareSamePhysicalConnection` | Reference equality confirms DbConnection object sharing | 124 | | `SavePolicy_WhenTableMissingInOneContext_ShouldRollbackAllContexts` | Severe failures cause complete rollback | 125 | | `MultipleSaveOperations_WithSharedConnection_ShouldMaintainDataConsistency` | Multiple operations maintain consistency over time | 126 | | `SavePolicy_WithSeparateConnections_ShouldNotBeAtomic` | **Negative test**: Proves separate connections are NOT atomic | 127 | | `SavePolicy_ShouldReflectDatabaseStateNotCasbinMemory` | Tests verify actual database state, not just Casbin memory | 128 | 129 | ### SchemaDistributionTests 130 | 131 | **File:** [SchemaDistributionTests.cs](SchemaDistributionTests.cs) 132 | **Test Count:** 2 133 | **Status:** ✅ All Passing 134 | 135 | **Purpose:** 136 | 137 | These tests verify that `CasbinDbContext.HasDefaultSchema()` correctly routes policies to their designated schemas when using shared connections, ensuring schema isolation is maintained. 138 | 139 | **Test Coverage:** 140 | 141 | | Test | Purpose | Status | 142 | |------|---------|--------| 143 | | `SavePolicy_SeparateConnections_ShouldDistributeAcrossSchemas` | Baseline behavior with separate connections | ✅ Passing | 144 | | `SavePolicy_SharedConnection_ShouldDistributeAcrossSchemas` | Schema routing with shared connection | ✅ Passing | 145 | 146 | **What They Test:** 147 | 148 | 1. **Schema Routing:** 149 | - `p` policies → `casbin_policies` schema 150 | - `g` policies → `casbin_groupings` schema 151 | - `g2` policies → `casbin_roles` schema 152 | 153 | 2. **Shared Connection Impact:** 154 | - Verifies `HasDefaultSchema()` returns correct schema name per context 155 | - Confirms shared connection doesn't break schema isolation 156 | - Validates multi-context routing works correctly 157 | 158 | 3. **Database Verification:** 159 | - Direct SQL queries to each schema 160 | - Counts policies by type in each schema 161 | - Asserts correct distribution (e.g., only `p` types in `casbin_policies` schema) 162 | 163 | **Why This Matters:** 164 | 165 | When using a shared connection for atomic transactions, each context must still route to its correct schema. These tests prove that sharing a connection object doesn't accidentally merge contexts or route to wrong schemas. 166 | 167 | **Running the Tests:** 168 | 169 | ```bash 170 | # Run both SchemaDistributionTests 171 | dotnet test -f net6.0 --filter "FullyQualifiedName~SchemaDistributionTests" --verbosity normal 172 | 173 | # Run specific test 174 | dotnet test -f net6.0 --filter "FullyQualifiedName~SavePolicy_SharedConnection_ShouldDistributeAcrossSchemas" --verbosity normal 175 | ``` 176 | 177 | ### AutoSaveTests 178 | 179 | **File:** [AutoSaveTests.cs](AutoSaveTests.cs) 180 | **Test Count:** 10 181 | **Status:** ✅ All Passing 182 | 183 | **Purpose:** 184 | 185 | These tests verify the Casbin Enforcer's `EnableAutoSave` behavior in multi-context scenarios and prove that `EnableAutoSave(false)` is required for atomic rollback testing. 186 | 187 | **Key Tests:** 188 | 189 | | Test | What It Proves | Status | 190 | |------|----------------|--------| 191 | | `TestPolicyAutoSaveOn` / `TestPolicyAutoSaveOnAsync` | AutoSave ON commits immediately | ✅ Passing | 192 | | `TestPolicyAutoSaveOff` | AutoSave OFF defers until SavePolicy | ✅ Passing | 193 | | `TestGroupingPolicyAutoSaveOn` | Grouping policies also commit immediately | ✅ Passing | 194 | | `TestGroupingPolicyAutoSaveOff` | Grouping policies defer with AutoSave OFF | ✅ Passing | 195 | | `TestAutoSaveOn_MultiContext_IndividualCommits` | Multi-context: operations commit independently | ✅ Passing | 196 | | `TestAutoSaveOff_MultiContext_RollbackOnFailure` | Multi-context: atomic rollback with AutoSave OFF | ✅ Passing | 197 | 198 | **Why AutoSave Testing Matters:** 199 | 200 | The rollback tests in `TransactionIntegrityTests` require `enforcer.EnableAutoSave(false)` (lines 302, 370) because: 201 | - With AutoSave ON: Policies commit immediately when `AddPolicyAsync()` is called 202 | - With AutoSave OFF: Policies stay in memory until `SavePolicyAsync()` is called 203 | - Atomic rollback testing requires all policies to be part of the same transaction 204 | 205 | **See:** [MULTI_CONTEXT_USAGE_GUIDE.md - EnableAutoSave and Transaction Atomicity](../../MULTI_CONTEXT_USAGE_GUIDE.md#enableautosave-and-transaction-atomicity) for detailed explanation. 206 | 207 | ## Why These Tests Are Excluded from CI/CD 208 | 209 | These tests are marked with `[Trait("Category", "Integration")]` and **excluded from CI/CD** because: 210 | 211 | 1. **Pipeline Ownership**: The CI/CD pipeline is not owned by this project's maintainers 212 | 2. **External Dependency**: Requires a PostgreSQL instance with specific configuration 213 | 3. **Local Verification**: These tests are for **local verification only** - they prove the documented transaction guarantees work correctly 214 | 215 | ## Troubleshooting 216 | 217 | ### Error: "could not connect to server" 218 | 219 | PostgreSQL is not running. Start it: 220 | - **Windows**: Open Services → Start "postgresql-x64-XX" 221 | - **macOS (Homebrew)**: `brew services start postgresql@17` 222 | - **Linux**: `sudo systemctl start postgresql` 223 | 224 | ### Error: "database 'casbin_integration_test' does not exist" 225 | 226 | Create the database: 227 | ```bash 228 | psql -U postgres -c "CREATE DATABASE casbin_integration_test;" 229 | ``` 230 | 231 | ### Error: "password authentication failed for user 'postgres'" 232 | 233 | Either: 234 | 1. Update your PostgreSQL password: `ALTER USER postgres PASSWORD 'postgres';` 235 | 2. Or update the connection string in [TransactionIntegrityTestFixture.cs](TransactionIntegrityTestFixture.cs) to match your credentials 236 | 237 | ### Error: "relation 'casbin_rule' does not exist" 238 | 239 | The test fixture should create tables automatically. If this fails: 240 | 1. Ensure the database exists 241 | 2. Ensure the user has CREATE privileges: `GRANT ALL PRIVILEGES ON DATABASE casbin_integration_test TO postgres;` 242 | 3. Try manually creating schemas: `CREATE SCHEMA casbin_policies;` etc. 243 | 244 | ## Verification of Transaction Guarantees 245 | 246 | ### Critical Rollback Tests 247 | 248 | The **most critical tests** are the rollback verification tests: 249 | - `SavePolicy_WhenTableDroppedInOneContext_ShouldRollbackAllContexts` 250 | - `SavePolicy_WhenTableMissingInOneContext_ShouldRollbackAllContexts` 251 | 252 | **Key Implementation Detail:** 253 | 254 | These tests call `enforcer.EnableAutoSave(false)` immediately after creating the enforcer (lines 302, 370 in `TransactionIntegrityTests.cs`). This is **critical** because: 255 | 256 | - **With AutoSave ON (default):** `AddPolicyAsync()` commits immediately to database. When `SavePolicyAsync()` is called later and fails, it only rolls back DELETE operations, not the earlier INSERT operations that already committed. 257 | 258 | - **With AutoSave OFF:** Policies stay in-memory until `SavePolicyAsync()` is called. When the transaction fails, ALL operations (INSERT and DELETE) roll back atomically. 259 | 260 | **Code Reference:** See lines 302, 370 in [TransactionIntegrityTests.cs](TransactionIntegrityTests.cs) 261 | 262 | ## See Also 263 | 264 | - [MULTI_CONTEXT_DESIGN.md](../../MULTI_CONTEXT_DESIGN.md) - Technical design and architecture 265 | - [MULTI_CONTEXT_USAGE_GUIDE.md](../../MULTI_CONTEXT_USAGE_GUIDE.md) - User-facing usage guide 266 | - [Main README](../../README.md) - Project overview 267 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore/EFCoreAdapter.Internal.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Casbin.Model; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Infrastructure; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | 10 | // ReSharper disable InconsistentNaming 11 | // ReSharper disable MemberCanBeProtected.Global 12 | // ReSharper disable MemberCanBePrivate.Global 13 | 14 | namespace Casbin.Persist.Adapter.EFCore 15 | { 16 | public partial class EFCoreAdapter : IAdapter, IFilteredAdapter 17 | where TDbContext : DbContext 18 | where TPersistPolicy : class, IEFCorePersistPolicy, new() 19 | where TKey : IEquatable 20 | { 21 | private void InternalAddPolicy(string section, string policyType, IPolicyValues values) 22 | { 23 | var context = GetContextForPolicyType(policyType); 24 | InternalAddPolicy(context, section, policyType, values); 25 | } 26 | 27 | private void InternalAddPolicy(DbContext context, string section, string policyType, IPolicyValues values) 28 | { 29 | var dbSet = GetCasbinRuleDbSetForPolicyType(context, policyType); 30 | var persistPolicy = PersistPolicy.Create(section, policyType, values); 31 | persistPolicy = OnAddPolicy(section, policyType, values, persistPolicy); 32 | dbSet.Add(persistPolicy); 33 | } 34 | 35 | private async ValueTask InternalAddPolicyAsync(string section, string policyType, IPolicyValues values) 36 | { 37 | var context = GetContextForPolicyType(policyType); 38 | await InternalAddPolicyAsync(context, section, policyType, values); 39 | } 40 | 41 | private async ValueTask InternalAddPolicyAsync(DbContext context, string section, string policyType, IPolicyValues values) 42 | { 43 | var dbSet = GetCasbinRuleDbSetForPolicyType(context, policyType); 44 | var persistPolicy = PersistPolicy.Create(section, policyType, values); 45 | persistPolicy = OnAddPolicy(section, policyType, values, persistPolicy); 46 | await dbSet.AddAsync(persistPolicy); 47 | } 48 | 49 | private void InternalUpdatePolicy(string section, string policyType, IPolicyValues oldValues , IPolicyValues newValues) 50 | { 51 | var context = GetContextForPolicyType(policyType); 52 | InternalUpdatePolicy(context, section, policyType, oldValues, newValues); 53 | } 54 | 55 | private void InternalUpdatePolicy(DbContext context, string section, string policyType, IPolicyValues oldValues , IPolicyValues newValues) 56 | { 57 | InternalRemovePolicy(context, section, policyType, oldValues); 58 | InternalAddPolicy(context, section, policyType, newValues); 59 | } 60 | 61 | private ValueTask InternalUpdatePolicyAsync(string section, string policyType, IPolicyValues oldValues , IPolicyValues newValues) 62 | { 63 | var context = GetContextForPolicyType(policyType); 64 | return InternalUpdatePolicyAsync(context, section, policyType, oldValues, newValues); 65 | } 66 | 67 | private async ValueTask InternalUpdatePolicyAsync(DbContext context, string section, string policyType, IPolicyValues oldValues , IPolicyValues newValues) 68 | { 69 | InternalRemovePolicy(context, section, policyType, oldValues); 70 | await InternalAddPolicyAsync(context, section, policyType, newValues); 71 | } 72 | 73 | private void InternalAddPolicies(string section, string policyType, IReadOnlyList valuesList) 74 | { 75 | var context = GetContextForPolicyType(policyType); 76 | InternalAddPolicies(context, section, policyType, valuesList); 77 | } 78 | 79 | private void InternalAddPolicies(DbContext context, string section, string policyType, IReadOnlyList valuesList) 80 | { 81 | var dbSet = GetCasbinRuleDbSetForPolicyType(context, policyType); 82 | var persistPolicies = valuesList. 83 | Select(v => PersistPolicy.Create(section, policyType, v)); 84 | persistPolicies = OnAddPolicies(section, policyType, valuesList, persistPolicies); 85 | dbSet.AddRange(persistPolicies); 86 | } 87 | 88 | private async ValueTask InternalAddPoliciesAsync(string section, string policyType, IReadOnlyList valuesList) 89 | { 90 | var context = GetContextForPolicyType(policyType); 91 | await InternalAddPoliciesAsync(context, section, policyType, valuesList); 92 | } 93 | 94 | private async ValueTask InternalAddPoliciesAsync(DbContext context, string section, string policyType, IReadOnlyList valuesList) 95 | { 96 | var dbSet = GetCasbinRuleDbSetForPolicyType(context, policyType); 97 | var persistPolicies = valuesList.Select(v => 98 | PersistPolicy.Create(section, policyType, v)); 99 | persistPolicies = OnAddPolicies(section, policyType, valuesList, persistPolicies); 100 | await dbSet.AddRangeAsync(persistPolicies); 101 | } 102 | 103 | private void InternalUpdatePolicies(string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) 104 | { 105 | var context = GetContextForPolicyType(policyType); 106 | InternalUpdatePolicies(context, section, policyType, oldValuesList, newValuesList); 107 | } 108 | 109 | private void InternalUpdatePolicies(DbContext context, string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) 110 | { 111 | InternalRemovePolicies(context, section, policyType, oldValuesList); 112 | InternalAddPolicies(context, section, policyType, newValuesList); 113 | } 114 | 115 | private ValueTask InternalUpdatePoliciesAsync(string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) 116 | { 117 | var context = GetContextForPolicyType(policyType); 118 | return InternalUpdatePoliciesAsync(context, section, policyType, oldValuesList, newValuesList); 119 | } 120 | 121 | private async ValueTask InternalUpdatePoliciesAsync(DbContext context, string section, string policyType, IReadOnlyList oldValuesList, IReadOnlyList newValuesList) 122 | { 123 | InternalRemovePolicies(context, section, policyType, oldValuesList); 124 | await InternalAddPoliciesAsync(context, section, policyType, newValuesList); 125 | } 126 | 127 | private void InternalRemovePolicy(string section, string policyType, IPolicyValues values) 128 | { 129 | var context = GetContextForPolicyType(policyType); 130 | InternalRemovePolicy(context, section, policyType, values); 131 | } 132 | 133 | private void InternalRemovePolicy(DbContext context, string section, string policyType, IPolicyValues values) 134 | { 135 | InternalRemoveFilteredPolicy(context, section, policyType, 0, values); 136 | } 137 | 138 | private void InternalRemovePolicies(string section, string policyType, IReadOnlyList valuesList) 139 | { 140 | var context = GetContextForPolicyType(policyType); 141 | InternalRemovePolicies(context, section, policyType, valuesList); 142 | } 143 | 144 | private void InternalRemovePolicies(DbContext context, string section, string policyType, IReadOnlyList valuesList) 145 | { 146 | foreach (var value in valuesList) 147 | { 148 | InternalRemovePolicy(context, section, policyType, value); 149 | } 150 | } 151 | 152 | private void InternalRemoveFilteredPolicy(string section, string policyType, int fieldIndex, IPolicyValues fieldValues) 153 | { 154 | var context = GetContextForPolicyType(policyType); 155 | InternalRemoveFilteredPolicy(context, section, policyType, fieldIndex, fieldValues); 156 | } 157 | 158 | private void InternalRemoveFilteredPolicy(DbContext context, string section, string policyType, int fieldIndex, IPolicyValues fieldValues) 159 | { 160 | var dbSet = GetCasbinRuleDbSetForPolicyType(context, policyType); 161 | var filter = new PolicyFilter(policyType, fieldIndex, fieldValues); 162 | var persistPolicies = filter.Apply(dbSet); 163 | persistPolicies = OnRemoveFilteredPolicy(section, policyType, fieldIndex, fieldValues, persistPolicies); 164 | dbSet.RemoveRange(persistPolicies); 165 | } 166 | 167 | #region Helper methods 168 | 169 | /// 170 | /// Gets or caches the DbSet for a specific context and policy type 171 | /// 172 | private DbSet GetCasbinRuleDbSetForPolicyType(DbContext context, string policyType) 173 | { 174 | var key = (context, policyType); 175 | if (!_persistPoliciesByContext.TryGetValue(key, out var dbSet)) 176 | { 177 | dbSet = GetCasbinRuleDbSet(context, policyType); 178 | _persistPoliciesByContext[key] = dbSet; 179 | } 180 | return dbSet; 181 | } 182 | 183 | /// 184 | /// Gets the context responsible for handling a specific policy type 185 | /// 186 | private DbContext GetContextForPolicyType(string policyType) 187 | { 188 | return _contextProvider.GetContextForPolicyType(policyType); 189 | } 190 | 191 | #endregion 192 | 193 | #region virtual method 194 | 195 | /// 196 | /// Gets the DbSet for policies from the specified context (backward compatible) 197 | /// 198 | [Obsolete("Use GetCasbinRuleDbSet(DbContext, string) instead. This method will be removed in a future major version.", false)] 199 | protected virtual DbSet GetCasbinRuleDbSet(TDbContext dbContext) 200 | { 201 | return GetCasbinRuleDbSet((DbContext)dbContext, null); 202 | } 203 | 204 | /// 205 | /// Gets the DbSet for policies from the specified context with optional policy type routing 206 | /// 207 | protected virtual DbSet GetCasbinRuleDbSet(DbContext dbContext, string policyType) 208 | { 209 | return dbContext.Set(); 210 | } 211 | 212 | protected virtual IQueryable OnLoadPolicy(IPolicyStore store, IQueryable policies) 213 | { 214 | return policies; 215 | } 216 | 217 | protected virtual IEnumerable OnSavePolicy(IPolicyStore store, IEnumerable policies) 218 | { 219 | return policies; 220 | } 221 | 222 | protected virtual TPersistPolicy OnAddPolicy(string section, string policyType, IPolicyValues values, TPersistPolicy policy) 223 | { 224 | return policy; 225 | } 226 | 227 | protected virtual IEnumerable OnAddPolicies(string section, string policyType, 228 | IEnumerable> addList, IEnumerable policies) 229 | { 230 | return policies; 231 | } 232 | 233 | protected virtual IQueryable OnRemoveFilteredPolicy(string section, string policyType, int fieldIndex, 234 | IPolicyValues fieldValues, IQueryable policies) 235 | { 236 | return policies; 237 | } 238 | 239 | #endregion 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/BackwardCompatibilityTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Casbin.Model; 4 | using Casbin.Persist.Adapter.EFCore.Entities; 5 | using Casbin.Persist.Adapter.EFCore.UnitTest.Extensions; 6 | using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; 7 | using Microsoft.EntityFrameworkCore; 8 | using Xunit; 9 | 10 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 11 | { 12 | /// 13 | /// Tests to ensure backward compatibility with existing single-context behavior. 14 | /// These tests verify that the multi-context changes don't break existing usage patterns. 15 | /// 16 | public class BackwardCompatibilityTest : TestUtil, 17 | IClassFixture, 18 | IClassFixture 19 | { 20 | private readonly ModelProvideFixture _modelProvideFixture; 21 | private readonly DbContextProviderFixture _dbContextProviderFixture; 22 | 23 | public BackwardCompatibilityTest( 24 | ModelProvideFixture modelProvideFixture, 25 | DbContextProviderFixture dbContextProviderFixture) 26 | { 27 | _modelProvideFixture = modelProvideFixture; 28 | _dbContextProviderFixture = dbContextProviderFixture; 29 | } 30 | 31 | [Fact] 32 | public void TestSingleContextConstructorStillWorks() 33 | { 34 | // Arrange - Using original constructor pattern 35 | using var context = _dbContextProviderFixture.GetContext("SingleContextConstructor"); 36 | context.Clear(); 37 | 38 | // Act - Create adapter using single-context constructor (original API) 39 | var adapter = new EFCoreAdapter(context); 40 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 41 | 42 | // Add policies 43 | enforcer.AddPolicy("alice", "data1", "read"); 44 | enforcer.AddGroupingPolicy("alice", "admin"); 45 | 46 | // Assert - All policies should be in single context 47 | Assert.Equal(2, context.Policies.Count()); 48 | 49 | var policies = context.Policies.ToList(); 50 | Assert.Contains(policies, p => p.Type == "p" && p.Value1 == "alice"); 51 | Assert.Contains(policies, p => p.Type == "g" && p.Value1 == "alice"); 52 | } 53 | 54 | [Fact] 55 | public async Task TestSingleContextAsyncOperationsStillWork() 56 | { 57 | // Arrange 58 | await using var context = _dbContextProviderFixture.GetContext("SingleContextAsync"); 59 | context.Clear(); 60 | 61 | var adapter = new EFCoreAdapter(context); 62 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 63 | 64 | // Act 65 | await enforcer.AddPolicyAsync("alice", "data1", "read"); 66 | await enforcer.AddGroupingPolicyAsync("alice", "admin"); 67 | 68 | // Assert 69 | Assert.Equal(2, await context.Policies.CountAsync()); 70 | } 71 | 72 | [Fact] 73 | public void TestSingleContextLoadAndSave() 74 | { 75 | // Arrange 76 | using var context = _dbContextProviderFixture.GetContext("SingleContextLoadSave"); 77 | context.Clear(); 78 | 79 | var adapter = new EFCoreAdapter(context); 80 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 81 | 82 | // Act - Add and save 83 | enforcer.AddPolicy("alice", "data1", "read"); 84 | enforcer.AddGroupingPolicy("alice", "admin"); 85 | enforcer.SavePolicy(); 86 | 87 | // Create new enforcer and load 88 | var newEnforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 89 | newEnforcer.LoadPolicy(); 90 | 91 | // Assert 92 | TestGetPolicy(newEnforcer, AsList( 93 | AsList("alice", "data1", "read") 94 | )); 95 | 96 | TestGetGroupingPolicy(newEnforcer, AsList( 97 | AsList("alice", "admin") 98 | )); 99 | } 100 | 101 | [Fact] 102 | public void TestSingleContextWithExistingTests() 103 | { 104 | // This test mimics the pattern from EFCoreAdapterTest.cs to ensure compatibility 105 | using var context = _dbContextProviderFixture.GetContext("ExistingPattern"); 106 | context.Clear(); 107 | 108 | // Initialize with data (like InitPolicy in EFCoreAdapterTest.cs) 109 | context.Policies.AddRange(new[] 110 | { 111 | new EFCorePersistPolicy { Type = "p", Value1 = "alice", Value2 = "data1", Value3 = "read" }, 112 | new EFCorePersistPolicy { Type = "p", Value1 = "bob", Value2 = "data2", Value3 = "write" }, 113 | new EFCorePersistPolicy { Type = "g", Value1 = "alice", Value2 = "data2_admin" } 114 | }); 115 | context.SaveChanges(); 116 | 117 | var adapter = new EFCoreAdapter(context); 118 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 119 | 120 | // Act - Load policy 121 | enforcer.LoadPolicy(); 122 | 123 | // Assert - Should match expected behavior 124 | TestGetPolicy(enforcer, AsList( 125 | AsList("alice", "data1", "read"), 126 | AsList("bob", "data2", "write") 127 | )); 128 | 129 | TestGetGroupingPolicy(enforcer, AsList( 130 | AsList("alice", "data2_admin") 131 | )); 132 | } 133 | 134 | [Fact] 135 | public void TestSingleContextRemoveOperations() 136 | { 137 | // Arrange 138 | using var context = _dbContextProviderFixture.GetContext("SingleContextRemove"); 139 | context.Clear(); 140 | 141 | var adapter = new EFCoreAdapter(context); 142 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 143 | 144 | enforcer.AddPolicy("alice", "data1", "read"); 145 | enforcer.AddPolicy("bob", "data2", "write"); 146 | 147 | // Act 148 | enforcer.RemovePolicy("alice", "data1", "read"); 149 | 150 | // Assert 151 | Assert.Single(context.Policies); 152 | var remaining = context.Policies.First(); 153 | Assert.Equal("bob", remaining.Value1); 154 | } 155 | 156 | [Fact] 157 | public void TestSingleContextUpdateOperations() 158 | { 159 | // Arrange 160 | using var context = _dbContextProviderFixture.GetContext("SingleContextUpdate"); 161 | context.Clear(); 162 | 163 | var adapter = new EFCoreAdapter(context); 164 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 165 | 166 | enforcer.AddPolicy("alice", "data1", "read"); 167 | 168 | // Act 169 | enforcer.UpdatePolicy( 170 | AsList("alice", "data1", "read"), 171 | AsList("alice", "data1", "write") 172 | ); 173 | 174 | // Assert 175 | Assert.Single(context.Policies); 176 | var policy = context.Policies.First(); 177 | Assert.Equal("write", policy.Value3); 178 | } 179 | 180 | [Fact] 181 | public void TestSingleContextBatchOperations() 182 | { 183 | // Arrange 184 | using var context = _dbContextProviderFixture.GetContext("SingleContextBatch"); 185 | context.Clear(); 186 | 187 | var adapter = new EFCoreAdapter(context); 188 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 189 | 190 | // Act - Add multiple 191 | enforcer.AddPolicies(new[] 192 | { 193 | AsList("alice", "data1", "read"), 194 | AsList("bob", "data2", "write"), 195 | AsList("charlie", "data3", "read") 196 | }); 197 | 198 | // Assert 199 | Assert.Equal(3, context.Policies.Count()); 200 | 201 | // Act - Remove multiple 202 | enforcer.RemovePolicies(new[] 203 | { 204 | AsList("alice", "data1", "read"), 205 | AsList("bob", "data2", "write") 206 | }); 207 | 208 | // Assert 209 | Assert.Single(context.Policies); 210 | } 211 | 212 | [Fact] 213 | public void TestSingleContextFilteredLoading() 214 | { 215 | // Arrange 216 | using var context = _dbContextProviderFixture.GetContext("SingleContextFiltered"); 217 | context.Clear(); 218 | 219 | context.Policies.AddRange(new[] 220 | { 221 | new EFCorePersistPolicy { Type = "p", Value1 = "alice", Value2 = "data1", Value3 = "read" }, 222 | new EFCorePersistPolicy { Type = "p", Value1 = "bob", Value2 = "data2", Value3 = "write" }, 223 | new EFCorePersistPolicy { Type = "g", Value1 = "alice", Value2 = "admin" } 224 | }); 225 | context.SaveChanges(); 226 | 227 | var adapter = new EFCoreAdapter(context); 228 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 229 | 230 | // Act - Load only alice's policies 231 | enforcer.LoadFilteredPolicy(new SimpleFieldFilter("p", 0, Policy.ValuesFrom(AsList("alice", "", "")))); 232 | 233 | // Assert 234 | Assert.True(adapter.IsFiltered); 235 | TestGetPolicy(enforcer, AsList( 236 | AsList("alice", "data1", "read") 237 | )); 238 | } 239 | 240 | [Fact] 241 | public void TestSingleContextProviderWrapping() 242 | { 243 | // Arrange - Create adapter with explicit SingleContextProvider 244 | using var context = _dbContextProviderFixture.GetContext("ProviderWrapping"); 245 | context.Clear(); 246 | 247 | var provider = new SingleContextProvider(context); 248 | var adapter = new EFCoreAdapter(provider); 249 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 250 | 251 | // Act 252 | enforcer.AddPolicy("alice", "data1", "read"); 253 | 254 | // Assert - Should behave identically to direct context constructor 255 | Assert.Single(context.Policies); 256 | Assert.Equal("alice", context.Policies.First().Value1); 257 | } 258 | 259 | [Fact] 260 | public void TestSingleContextProviderGetAllContexts() 261 | { 262 | // Arrange 263 | using var context = _dbContextProviderFixture.GetContext("ProviderGetAll"); 264 | var provider = new SingleContextProvider(context); 265 | 266 | // Act 267 | var contexts = provider.GetAllContexts().ToList(); 268 | 269 | // Assert 270 | Assert.Single(contexts); 271 | Assert.Same(context, contexts[0]); 272 | } 273 | 274 | [Fact] 275 | public void TestSingleContextProviderGetContextForPolicyType() 276 | { 277 | // Arrange 278 | using var context = _dbContextProviderFixture.GetContext("ProviderGetForType"); 279 | var provider = new SingleContextProvider(context); 280 | 281 | // Act & Assert - All policy types should return same context 282 | Assert.Same(context, provider.GetContextForPolicyType("p")); 283 | Assert.Same(context, provider.GetContextForPolicyType("p2")); 284 | Assert.Same(context, provider.GetContextForPolicyType("g")); 285 | Assert.Same(context, provider.GetContextForPolicyType("g2")); 286 | Assert.Same(context, provider.GetContextForPolicyType(null)); 287 | Assert.Same(context, provider.GetContextForPolicyType("")); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.IntegrationTest/Integration/SchemaDistributionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Casbin; 6 | using Casbin.Model; 7 | using Npgsql; 8 | using Microsoft.EntityFrameworkCore; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | #nullable enable 13 | 14 | namespace Casbin.Persist.Adapter.EFCore.UnitTest.Integration 15 | { 16 | /// 17 | /// Tests to verify whether HasDefaultSchema() properly distributes policies across PostgreSQL schemas, 18 | /// both with separate connections and with shared connections. 19 | /// 20 | /// Purpose: Determine if explicit SET search_path is necessary or if EF Core's HasDefaultSchema() 21 | /// generates schema-qualified SQL that works correctly with shared connections. 22 | /// 23 | [Trait("Category", "Integration")] 24 | [Collection("IntegrationTests")] 25 | public class SchemaDistributionTests : IClassFixture, IAsyncLifetime 26 | { 27 | private readonly TransactionIntegrityTestFixture _fixture; 28 | private readonly ITestOutputHelper _output; 29 | private const string ModelPath = "examples/multi_context_model.conf"; 30 | 31 | public SchemaDistributionTests(TransactionIntegrityTestFixture fixture, ITestOutputHelper output) 32 | { 33 | _fixture = fixture; 34 | _output = output; 35 | } 36 | 37 | public Task InitializeAsync() => _fixture.ClearAllPoliciesAsync(); 38 | public Task DisposeAsync() => _fixture.RunMigrationsAsync(); 39 | 40 | #region Helper: Derived Context Classes 41 | 42 | /// 43 | /// Derived context for policies schema 44 | /// 45 | public class TestCasbinDbContext1 : CasbinDbContext 46 | { 47 | public TestCasbinDbContext1( 48 | DbContextOptions> options, 49 | string schemaName, 50 | string tableName) 51 | : base(options, schemaName, tableName) 52 | { 53 | } 54 | } 55 | 56 | /// 57 | /// Derived context for groupings schema 58 | /// 59 | public class TestCasbinDbContext2 : CasbinDbContext 60 | { 61 | public TestCasbinDbContext2( 62 | DbContextOptions> options, 63 | string schemaName, 64 | string tableName) 65 | : base(options, schemaName, tableName) 66 | { 67 | } 68 | } 69 | 70 | /// 71 | /// Derived context for roles schema 72 | /// 73 | public class TestCasbinDbContext3 : CasbinDbContext 74 | { 75 | public TestCasbinDbContext3( 76 | DbContextOptions> options, 77 | string schemaName, 78 | string tableName) 79 | : base(options, schemaName, tableName) 80 | { 81 | } 82 | } 83 | 84 | #endregion 85 | 86 | #region Helper: Three-Context Provider 87 | 88 | /// 89 | /// Provider that routes policy types to three separate contexts 90 | /// 91 | private class ThreeWayContextProvider : ICasbinDbContextProvider 92 | { 93 | private readonly CasbinDbContext _policyContext; 94 | private readonly CasbinDbContext _groupingContext; 95 | private readonly CasbinDbContext _roleContext; 96 | private readonly System.Data.Common.DbConnection? _sharedConnection; 97 | 98 | public ThreeWayContextProvider( 99 | CasbinDbContext policyContext, 100 | CasbinDbContext groupingContext, 101 | CasbinDbContext roleContext, 102 | System.Data.Common.DbConnection? sharedConnection) 103 | { 104 | _policyContext = policyContext; 105 | _groupingContext = groupingContext; 106 | _roleContext = roleContext; 107 | _sharedConnection = sharedConnection; 108 | } 109 | 110 | public DbContext GetContextForPolicyType(string policyType) 111 | { 112 | return policyType switch 113 | { 114 | "p" => _policyContext, // p policies → casbin_policies schema 115 | "g" => _groupingContext, // g groupings → casbin_groupings schema 116 | "g2" => _roleContext, // g2 roles → casbin_roles schema 117 | _ => _policyContext 118 | }; 119 | } 120 | 121 | public IEnumerable GetAllContexts() 122 | { 123 | return new[] { _policyContext, _groupingContext, _roleContext }; 124 | } 125 | 126 | public System.Data.Common.DbConnection? GetSharedConnection() 127 | { 128 | return _sharedConnection; 129 | } 130 | } 131 | 132 | #endregion 133 | 134 | #region Test 1: Separate Connections (Control/Baseline) 135 | 136 | /// 137 | /// BASELINE TEST: Proves that HasDefaultSchema() correctly distributes policies across schemas 138 | /// when contexts use SEPARATE connections (no shared connection). 139 | /// 140 | /// This is the baseline that should work regardless of any SET search_path logic. 141 | /// 142 | [Fact] 143 | public async Task SavePolicy_SeparateConnections_ShouldDistributeAcrossSchemas() 144 | { 145 | _output.WriteLine("=== TEST: Separate Connections - Schema Distribution ==="); 146 | 147 | // Create three contexts with SEPARATE connection strings (no shared connection) 148 | var policyOptions = new DbContextOptionsBuilder>() 149 | .UseNpgsql(_fixture.ConnectionString) // Connection #1 150 | .Options; 151 | var policyContext = new TestCasbinDbContext1(policyOptions, TransactionIntegrityTestFixture.PoliciesSchema, "casbin_rule"); 152 | 153 | var groupingOptions = new DbContextOptionsBuilder>() 154 | .UseNpgsql(_fixture.ConnectionString) // Connection #2 155 | .Options; 156 | var groupingContext = new TestCasbinDbContext2(groupingOptions, TransactionIntegrityTestFixture.GroupingsSchema, "casbin_rule"); 157 | 158 | var roleOptions = new DbContextOptionsBuilder>() 159 | .UseNpgsql(_fixture.ConnectionString) // Connection #3 160 | .Options; 161 | var roleContext = new TestCasbinDbContext3(roleOptions, TransactionIntegrityTestFixture.RolesSchema, "casbin_rule"); 162 | 163 | _output.WriteLine("Created three contexts with SEPARATE connections"); 164 | 165 | // Verify they are different connection objects 166 | var conn1 = policyContext.Database.GetDbConnection(); 167 | var conn2 = groupingContext.Database.GetDbConnection(); 168 | var conn3 = roleContext.Database.GetDbConnection(); 169 | 170 | Assert.False(ReferenceEquals(conn1, conn2), "Connections 1 and 2 should be different objects"); 171 | Assert.False(ReferenceEquals(conn2, conn3), "Connections 2 and 3 should be different objects"); 172 | _output.WriteLine("Verified: Contexts use DIFFERENT DbConnection objects"); 173 | 174 | try 175 | { 176 | // Create provider and adapter 177 | // Pass null since these contexts use separate connections 178 | var provider = new ThreeWayContextProvider(policyContext, groupingContext, roleContext, null); 179 | var adapter = new EFCoreAdapter(provider); 180 | 181 | // Create enforcer without loading policy (tables might be empty) 182 | var model = DefaultModel.CreateFromFile(ModelPath); 183 | var enforcer = new Enforcer(model); 184 | enforcer.SetAdapter(adapter); 185 | 186 | // Add policies to in-memory model (not persisted yet) 187 | enforcer.AddPolicy("alice", "data1", "read"); // → casbin_policies 188 | enforcer.AddGroupingPolicy("alice", "admin"); // → casbin_groupings 189 | enforcer.AddNamedGroupingPolicy("g2", "admin", "role-superuser"); // → casbin_roles 190 | 191 | _output.WriteLine("Added policies to in-memory model:"); 192 | _output.WriteLine(" p policy → casbin_policies"); 193 | _output.WriteLine(" g policy → casbin_groupings"); 194 | _output.WriteLine(" g2 policy → casbin_roles"); 195 | 196 | // Save to database 197 | await enforcer.SavePolicyAsync(); 198 | _output.WriteLine("Called SavePolicyAsync()"); 199 | 200 | // Verify distribution across schemas 201 | var policiesCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.PoliciesSchema); 202 | var groupingsCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.GroupingsSchema); 203 | var rolesCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.RolesSchema); 204 | 205 | _output.WriteLine($"Schema distribution:"); 206 | _output.WriteLine($" casbin_policies: {policiesCount} policy"); 207 | _output.WriteLine($" casbin_groupings: {groupingsCount} policy"); 208 | _output.WriteLine($" casbin_roles: {rolesCount} policy"); 209 | 210 | // CRITICAL ASSERTION: Policies should be distributed across all three schemas 211 | Assert.Equal(1, policiesCount); 212 | Assert.Equal(1, groupingsCount); 213 | Assert.Equal(1, rolesCount); 214 | 215 | _output.WriteLine("✓ BASELINE TEST PASSED: HasDefaultSchema() distributes policies correctly with separate connections"); 216 | } 217 | finally 218 | { 219 | await policyContext.DisposeAsync(); 220 | await groupingContext.DisposeAsync(); 221 | await roleContext.DisposeAsync(); 222 | } 223 | } 224 | 225 | #endregion 226 | 227 | #region Test 2: Shared Connection (Critical Test) 228 | 229 | /// 230 | /// CRITICAL TEST: Determines if HasDefaultSchema() correctly distributes policies across schemas 231 | /// when contexts share a SINGLE DbConnection object. 232 | /// 233 | /// If this test FAILS: SET search_path approach is necessary 234 | /// If this test PASSES: SET search_path approach is NOT necessary 235 | /// 236 | [Fact] 237 | public async Task SavePolicy_SharedConnection_ShouldDistributeAcrossSchemas() 238 | { 239 | _output.WriteLine("=== TEST: Shared Connection - Schema Distribution ==="); 240 | 241 | // Create ONE shared connection (CRITICAL for this test) 242 | var sharedConnection = new NpgsqlConnection(_fixture.ConnectionString); 243 | await sharedConnection.OpenAsync(); 244 | _output.WriteLine("Opened shared connection for all three contexts"); 245 | 246 | try 247 | { 248 | // Create three contexts using SAME connection object 249 | var policyOptions = new DbContextOptionsBuilder>() 250 | .UseNpgsql(sharedConnection) // ✅ Shared connection 251 | .Options; 252 | var policyContext = new TestCasbinDbContext1(policyOptions, TransactionIntegrityTestFixture.PoliciesSchema, "casbin_rule"); 253 | 254 | var groupingOptions = new DbContextOptionsBuilder>() 255 | .UseNpgsql(sharedConnection) // ✅ Same connection 256 | .Options; 257 | var groupingContext = new TestCasbinDbContext2(groupingOptions, TransactionIntegrityTestFixture.GroupingsSchema, "casbin_rule"); 258 | 259 | var roleOptions = new DbContextOptionsBuilder>() 260 | .UseNpgsql(sharedConnection) // ✅ Same connection 261 | .Options; 262 | var roleContext = new TestCasbinDbContext3(roleOptions, TransactionIntegrityTestFixture.RolesSchema, "casbin_rule"); 263 | 264 | _output.WriteLine("Created three contexts sharing the same connection"); 265 | 266 | // Verify reference equality 267 | var conn1 = policyContext.Database.GetDbConnection(); 268 | var conn2 = groupingContext.Database.GetDbConnection(); 269 | var conn3 = roleContext.Database.GetDbConnection(); 270 | 271 | Assert.True(ReferenceEquals(conn1, conn2), "Connections 1 and 2 should be the SAME object"); 272 | Assert.True(ReferenceEquals(conn2, conn3), "Connections 2 and 3 should be the SAME object"); 273 | _output.WriteLine("Verified: All contexts share the SAME DbConnection object (reference equality)"); 274 | 275 | // Create provider and adapter 276 | // Pass sharedConnection since all contexts share it 277 | var provider = new ThreeWayContextProvider(policyContext, groupingContext, roleContext, sharedConnection); 278 | var adapter = new EFCoreAdapter(provider); 279 | 280 | // Create enforcer without loading policy (tables might be empty) 281 | var model = DefaultModel.CreateFromFile(ModelPath); 282 | var enforcer = new Enforcer(model); 283 | enforcer.SetAdapter(adapter); 284 | 285 | // Add policies to in-memory model (not persisted yet) 286 | enforcer.AddPolicy("bob", "data2", "write"); // → casbin_policies 287 | enforcer.AddGroupingPolicy("bob", "developer"); // → casbin_groupings 288 | enforcer.AddNamedGroupingPolicy("g2", "developer", "role-contributor"); // → casbin_roles 289 | 290 | _output.WriteLine("Added policies to in-memory model:"); 291 | _output.WriteLine(" p policy → should go to casbin_policies"); 292 | _output.WriteLine(" g policy → should go to casbin_groupings"); 293 | _output.WriteLine(" g2 policy → should go to casbin_roles"); 294 | 295 | // Save to database 296 | await enforcer.SavePolicyAsync(); 297 | _output.WriteLine("Called SavePolicyAsync()"); 298 | 299 | // Verify distribution across schemas 300 | var policiesCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.PoliciesSchema); 301 | var groupingsCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.GroupingsSchema); 302 | var rolesCount = await _fixture.CountPoliciesInSchemaAsync(TransactionIntegrityTestFixture.RolesSchema); 303 | 304 | _output.WriteLine($"Schema distribution:"); 305 | _output.WriteLine($" casbin_policies: {policiesCount} policy"); 306 | _output.WriteLine($" casbin_groupings: {groupingsCount} policy"); 307 | _output.WriteLine($" casbin_roles: {rolesCount} policy"); 308 | 309 | // CRITICAL ASSERTION: Policies should be distributed across all three schemas 310 | // If all policies end up in ONE schema, HasDefaultSchema() does NOT work with shared connections 311 | // and we NEED the SET search_path approach 312 | 313 | if (policiesCount == 1 && groupingsCount == 1 && rolesCount == 1) 314 | { 315 | _output.WriteLine("✓✓✓ SHARED CONNECTION TEST PASSED!"); 316 | _output.WriteLine("HasDefaultSchema() correctly distributes policies even with shared connection"); 317 | _output.WriteLine("CONCLUSION: SET search_path approach is NOT necessary"); 318 | } 319 | else 320 | { 321 | _output.WriteLine("✗✗✗ SHARED CONNECTION TEST FAILED!"); 322 | _output.WriteLine($"Expected distribution: (1, 1, 1), Got: ({policiesCount}, {groupingsCount}, {rolesCount})"); 323 | _output.WriteLine("CONCLUSION: SET search_path approach IS necessary for shared connections"); 324 | } 325 | 326 | Assert.Equal(1, policiesCount); 327 | Assert.Equal(1, groupingsCount); 328 | Assert.Equal(1, rolesCount); 329 | 330 | await policyContext.DisposeAsync(); 331 | await groupingContext.DisposeAsync(); 332 | await roleContext.DisposeAsync(); 333 | } 334 | finally 335 | { 336 | await sharedConnection.DisposeAsync(); 337 | } 338 | } 339 | 340 | #endregion 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /Casbin.Persist.Adapter.EFCore.UnitTest/MultiContextTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Casbin.Model; 6 | using Casbin.Persist.Adapter.EFCore.Entities; 7 | using Casbin.Persist.Adapter.EFCore.UnitTest.Extensions; 8 | using Casbin.Persist.Adapter.EFCore.UnitTest.Fixtures; 9 | using Microsoft.EntityFrameworkCore; 10 | using Xunit; 11 | 12 | namespace Casbin.Persist.Adapter.EFCore.UnitTest 13 | { 14 | /// 15 | /// Tests for multi-context functionality where different policy types 16 | /// can be stored in separate database contexts/tables/schemas. 17 | /// 18 | public class MultiContextTest : TestUtil, 19 | IClassFixture, 20 | IClassFixture 21 | { 22 | private readonly ModelProvideFixture _modelProvideFixture; 23 | private readonly MultiContextProviderFixture _multiContextProviderFixture; 24 | 25 | public MultiContextTest( 26 | ModelProvideFixture modelProvideFixture, 27 | MultiContextProviderFixture multiContextProviderFixture) 28 | { 29 | _modelProvideFixture = modelProvideFixture; 30 | _multiContextProviderFixture = multiContextProviderFixture; 31 | } 32 | 33 | [Fact] 34 | public void TestMultiContextAddPolicy() 35 | { 36 | // Arrange 37 | var provider = _multiContextProviderFixture.GetMultiContextProvider("AddPolicy"); 38 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("AddPolicy"); 39 | 40 | policyContext.Clear(); 41 | groupingContext.Clear(); 42 | 43 | var adapter = new EFCoreAdapter(provider); 44 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 45 | 46 | // Act - Add policy rules (should go to policy context) 47 | enforcer.AddPolicy("alice", "data1", "read"); 48 | enforcer.AddPolicy("bob", "data2", "write"); 49 | 50 | // Add grouping rules (should go to grouping context) 51 | enforcer.AddGroupingPolicy("alice", "admin"); 52 | 53 | // Assert - Verify policies are in the correct contexts 54 | Assert.Equal(2, policyContext.Policies.Count()); 55 | Assert.Equal(1, groupingContext.Policies.Count()); 56 | 57 | // Verify policy data 58 | var alicePolicy = policyContext.Policies.FirstOrDefault(p => p.Value1 == "alice"); 59 | Assert.NotNull(alicePolicy); 60 | Assert.Equal("p", alicePolicy.Type); 61 | Assert.Equal("data1", alicePolicy.Value2); 62 | Assert.Equal("read", alicePolicy.Value3); 63 | 64 | // Verify grouping data 65 | var aliceGrouping = groupingContext.Policies.FirstOrDefault(p => p.Value1 == "alice"); 66 | Assert.NotNull(aliceGrouping); 67 | Assert.Equal("g", aliceGrouping.Type); 68 | Assert.Equal("admin", aliceGrouping.Value2); 69 | } 70 | 71 | [Fact] 72 | public async Task TestMultiContextAddPolicyAsync() 73 | { 74 | // Arrange 75 | var provider = _multiContextProviderFixture.GetMultiContextProvider("AddPolicyAsync"); 76 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("AddPolicyAsync"); 77 | 78 | policyContext.Clear(); 79 | groupingContext.Clear(); 80 | 81 | var adapter = new EFCoreAdapter(provider); 82 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 83 | 84 | // Act 85 | await enforcer.AddPolicyAsync("alice", "data1", "read"); 86 | await enforcer.AddPolicyAsync("bob", "data2", "write"); 87 | await enforcer.AddGroupingPolicyAsync("alice", "admin"); 88 | 89 | // Assert 90 | Assert.Equal(2, await policyContext.Policies.CountAsync()); 91 | Assert.Equal(1, await groupingContext.Policies.CountAsync()); 92 | } 93 | 94 | [Fact] 95 | public void TestMultiContextRemovePolicy() 96 | { 97 | // Arrange 98 | var provider = _multiContextProviderFixture.GetMultiContextProvider("RemovePolicy"); 99 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("RemovePolicy"); 100 | 101 | policyContext.Clear(); 102 | groupingContext.Clear(); 103 | 104 | // Pre-populate data 105 | policyContext.Policies.Add(new EFCorePersistPolicy 106 | { 107 | Type = "p", 108 | Value1 = "alice", 109 | Value2 = "data1", 110 | Value3 = "read" 111 | }); 112 | policyContext.SaveChanges(); 113 | 114 | groupingContext.Policies.Add(new EFCorePersistPolicy 115 | { 116 | Type = "g", 117 | Value1 = "alice", 118 | Value2 = "admin" 119 | }); 120 | groupingContext.SaveChanges(); 121 | 122 | var adapter = new EFCoreAdapter(provider); 123 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 124 | enforcer.LoadPolicy(); 125 | 126 | // Act 127 | enforcer.RemovePolicy("alice", "data1", "read"); 128 | enforcer.RemoveGroupingPolicy("alice", "admin"); 129 | 130 | // Assert 131 | Assert.Equal(0, policyContext.Policies.Count()); 132 | Assert.Equal(0, groupingContext.Policies.Count()); 133 | } 134 | 135 | [Fact] 136 | public void TestMultiContextLoadPolicy() 137 | { 138 | // Arrange 139 | var provider = _multiContextProviderFixture.GetMultiContextProvider("LoadPolicy"); 140 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("LoadPolicy"); 141 | 142 | policyContext.Clear(); 143 | groupingContext.Clear(); 144 | 145 | // Add test data to policy context 146 | policyContext.Policies.AddRange(new[] 147 | { 148 | new EFCorePersistPolicy { Type = "p", Value1 = "alice", Value2 = "data1", Value3 = "read" }, 149 | new EFCorePersistPolicy { Type = "p", Value1 = "bob", Value2 = "data2", Value3 = "write" } 150 | }); 151 | policyContext.SaveChanges(); 152 | 153 | // Add test data to grouping context 154 | groupingContext.Policies.AddRange(new[] 155 | { 156 | new EFCorePersistPolicy { Type = "g", Value1 = "alice", Value2 = "admin" }, 157 | new EFCorePersistPolicy { Type = "g", Value1 = "bob", Value2 = "user" } 158 | }); 159 | groupingContext.SaveChanges(); 160 | 161 | var adapter = new EFCoreAdapter(provider); 162 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 163 | 164 | // Act 165 | enforcer.LoadPolicy(); 166 | 167 | // Assert - Verify all policies loaded from both contexts 168 | TestGetPolicy(enforcer, AsList( 169 | AsList("alice", "data1", "read"), 170 | AsList("bob", "data2", "write") 171 | )); 172 | 173 | TestGetGroupingPolicy(enforcer, AsList( 174 | AsList("alice", "admin"), 175 | AsList("bob", "user") 176 | )); 177 | } 178 | 179 | [Fact] 180 | public async Task TestMultiContextLoadPolicyAsync() 181 | { 182 | // Arrange 183 | var provider = _multiContextProviderFixture.GetMultiContextProvider("LoadPolicyAsync"); 184 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("LoadPolicyAsync"); 185 | 186 | policyContext.Clear(); 187 | groupingContext.Clear(); 188 | 189 | policyContext.Policies.AddRange(new[] 190 | { 191 | new EFCorePersistPolicy { Type = "p", Value1 = "alice", Value2 = "data1", Value3 = "read" } 192 | }); 193 | await policyContext.SaveChangesAsync(); 194 | 195 | groupingContext.Policies.Add(new EFCorePersistPolicy { Type = "g", Value1 = "alice", Value2 = "admin" }); 196 | await groupingContext.SaveChangesAsync(); 197 | 198 | var adapter = new EFCoreAdapter(provider); 199 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 200 | 201 | // Act 202 | await enforcer.LoadPolicyAsync(); 203 | 204 | // Assert 205 | Assert.Single(enforcer.GetPolicy()); 206 | Assert.Single(enforcer.GetGroupingPolicy()); 207 | } 208 | 209 | [Fact] 210 | public void TestMultiContextSavePolicy() 211 | { 212 | // Arrange 213 | var provider = _multiContextProviderFixture.GetMultiContextProvider("SavePolicy"); 214 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("SavePolicy"); 215 | 216 | policyContext.Clear(); 217 | groupingContext.Clear(); 218 | 219 | var adapter = new EFCoreAdapter(provider); 220 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 221 | 222 | // Add policies via enforcer 223 | enforcer.AddPolicy("alice", "data1", "read"); 224 | enforcer.AddPolicy("bob", "data2", "write"); 225 | enforcer.AddGroupingPolicy("alice", "admin"); 226 | 227 | // Act - Save should distribute policies to correct contexts 228 | enforcer.SavePolicy(); 229 | 230 | // Assert - Verify data is in correct contexts 231 | Assert.Equal(2, policyContext.Policies.Count()); 232 | Assert.Equal(1, groupingContext.Policies.Count()); 233 | 234 | // Verify we can reload from both contexts 235 | var newEnforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 236 | newEnforcer.LoadPolicy(); 237 | 238 | TestGetPolicy(newEnforcer, AsList( 239 | AsList("alice", "data1", "read"), 240 | AsList("bob", "data2", "write") 241 | )); 242 | 243 | TestGetGroupingPolicy(newEnforcer, AsList( 244 | AsList("alice", "admin") 245 | )); 246 | } 247 | 248 | [Fact] 249 | public async Task TestMultiContextSavePolicyAsync() 250 | { 251 | // Arrange 252 | var provider = _multiContextProviderFixture.GetMultiContextProvider("SavePolicyAsync"); 253 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("SavePolicyAsync"); 254 | 255 | policyContext.Clear(); 256 | groupingContext.Clear(); 257 | 258 | var adapter = new EFCoreAdapter(provider); 259 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 260 | 261 | enforcer.AddPolicy("alice", "data1", "read"); 262 | enforcer.AddGroupingPolicy("alice", "admin"); 263 | 264 | // Act 265 | await enforcer.SavePolicyAsync(); 266 | 267 | // Assert 268 | Assert.Equal(1, await policyContext.Policies.CountAsync()); 269 | Assert.Equal(1, await groupingContext.Policies.CountAsync()); 270 | } 271 | 272 | [Fact] 273 | public void TestMultiContextBatchOperations() 274 | { 275 | // Arrange 276 | var provider = _multiContextProviderFixture.GetMultiContextProvider("BatchOperations"); 277 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("BatchOperations"); 278 | 279 | policyContext.Clear(); 280 | groupingContext.Clear(); 281 | 282 | var adapter = new EFCoreAdapter(provider); 283 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 284 | 285 | // Act - Add multiple policies at once 286 | enforcer.AddPolicies(new[] 287 | { 288 | AsList("alice", "data1", "read"), 289 | AsList("bob", "data2", "write"), 290 | AsList("charlie", "data3", "read") 291 | }); 292 | 293 | // Assert 294 | Assert.Equal(3, policyContext.Policies.Count()); 295 | 296 | // Act - Remove multiple policies 297 | enforcer.RemovePolicies(new[] 298 | { 299 | AsList("alice", "data1", "read"), 300 | AsList("bob", "data2", "write") 301 | }); 302 | 303 | // Assert 304 | Assert.Equal(1, policyContext.Policies.Count()); 305 | Assert.Equal("charlie", policyContext.Policies.First().Value1); 306 | } 307 | 308 | [Fact] 309 | public void TestMultiContextLoadFilteredPolicy() 310 | { 311 | // Arrange 312 | var provider = _multiContextProviderFixture.GetMultiContextProvider("LoadFilteredPolicy"); 313 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("LoadFilteredPolicy"); 314 | 315 | policyContext.Clear(); 316 | groupingContext.Clear(); 317 | 318 | // Add multiple policies 319 | policyContext.Policies.AddRange(new[] 320 | { 321 | new EFCorePersistPolicy { Type = "p", Value1 = "alice", Value2 = "data1", Value3 = "read" }, 322 | new EFCorePersistPolicy { Type = "p", Value1 = "bob", Value2 = "data2", Value3 = "write" } 323 | }); 324 | policyContext.SaveChanges(); 325 | 326 | groupingContext.Policies.Add(new EFCorePersistPolicy { Type = "g", Value1 = "alice", Value2 = "admin" }); 327 | groupingContext.SaveChanges(); 328 | 329 | var adapter = new EFCoreAdapter(provider); 330 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 331 | 332 | // Act - Load only alice's policies 333 | enforcer.LoadFilteredPolicy(new SimpleFieldFilter("p", 0, Policy.ValuesFrom(AsList("alice", "", "")))); 334 | 335 | // Assert 336 | TestGetPolicy(enforcer, AsList( 337 | AsList("alice", "data1", "read") 338 | )); 339 | 340 | // Bob's policy should not be loaded 341 | Assert.DoesNotContain(enforcer.GetPolicy(), p => p.Contains("bob")); 342 | } 343 | 344 | /// 345 | /// Verifies that UpdatePolicy operations work across multiple contexts without throwing exceptions. 346 | /// 347 | /// NOTE: This is NOT a transaction rollback test. This test uses separate SQLite database files 348 | /// (policy.db and grouping.db), making atomic cross-context transactions impossible. 349 | /// 350 | /// For actual transaction integrity and rollback verification across multiple contexts, 351 | /// see Integration/TransactionIntegrityTests.cs (PostgreSQL tests with shared connections). 352 | /// 353 | [Fact] 354 | public void TestMultiContextUpdatePolicyNoException() 355 | { 356 | // Arrange 357 | var provider = _multiContextProviderFixture.GetMultiContextProvider("UpdatePolicyNoException"); 358 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("UpdatePolicyNoException"); 359 | 360 | policyContext.Clear(); 361 | groupingContext.Clear(); 362 | 363 | var adapter = new EFCoreAdapter(provider); 364 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 365 | 366 | // Add initial data 367 | enforcer.AddPolicy("alice", "data1", "read"); 368 | enforcer.AddGroupingPolicy("alice", "admin"); 369 | 370 | // Act & Assert - UpdatePolicy should complete without throwing exceptions 371 | enforcer.UpdatePolicy( 372 | AsList("alice", "data1", "read"), 373 | AsList("alice", "data1", "write") 374 | ); 375 | 376 | // Verify the update was applied successfully 377 | Assert.True(enforcer.HasPolicy("alice", "data1", "write")); 378 | Assert.False(enforcer.HasPolicy("alice", "data1", "read")); 379 | } 380 | 381 | [Fact] 382 | public void TestMultiContextProviderGetAllContexts() 383 | { 384 | // Arrange 385 | var provider = _multiContextProviderFixture.GetMultiContextProvider("GetAllContexts"); 386 | 387 | // Act 388 | var contexts = provider.GetAllContexts().ToList(); 389 | 390 | // Assert 391 | Assert.Equal(2, contexts.Count); 392 | Assert.All(contexts, ctx => Assert.NotNull(ctx)); 393 | } 394 | 395 | [Fact] 396 | public void TestMultiContextProviderGetContextForPolicyType() 397 | { 398 | // Arrange 399 | var provider = _multiContextProviderFixture.GetMultiContextProvider("GetContextForType"); 400 | 401 | // Act & Assert 402 | var pContext = provider.GetContextForPolicyType("p"); 403 | var p2Context = provider.GetContextForPolicyType("p2"); 404 | var gContext = provider.GetContextForPolicyType("g"); 405 | var g2Context = provider.GetContextForPolicyType("g2"); 406 | 407 | // All 'p' types should route to same context 408 | Assert.Same(pContext, p2Context); 409 | 410 | // All 'g' types should route to same context 411 | Assert.Same(gContext, g2Context); 412 | 413 | // 'p' and 'g' types should route to different contexts 414 | Assert.NotSame(pContext, gContext); 415 | } 416 | 417 | [Fact] 418 | public void TestDbSetCachingByPolicyType() 419 | { 420 | // This test verifies that the DbSet cache uses (context, policyType) as the composite key 421 | // rather than just context. This prevents the bug where different policy types would 422 | // incorrectly share the same cached DbSet. 423 | 424 | // Arrange 425 | var provider = _multiContextProviderFixture.GetMultiContextProvider("DbSetCaching"); 426 | var (policyContext, groupingContext) = _multiContextProviderFixture.GetSeparateContexts("DbSetCaching"); 427 | 428 | policyContext.Clear(); 429 | groupingContext.Clear(); 430 | 431 | // Create a custom adapter that tracks GetCasbinRuleDbSet calls 432 | var callTracker = new Dictionary(); 433 | var adapter = new DbSetCachingTestAdapter(provider, callTracker); 434 | var enforcer = new Enforcer(_modelProvideFixture.GetNewRbacModel(), adapter); 435 | 436 | // Act - Add policies of different types 437 | enforcer.AddPolicy("alice", "data1", "read"); // Type 'p' - first call should invoke GetCasbinRuleDbSet 438 | enforcer.AddPolicy("bob", "data2", "write"); // Type 'p' - should use cached DbSet 439 | enforcer.AddGroupingPolicy("alice", "admin"); // Type 'g' - different type, should invoke GetCasbinRuleDbSet 440 | enforcer.AddGroupingPolicy("bob", "user"); // Type 'g' - should use cached DbSet 441 | 442 | // Assert - Verify GetCasbinRuleDbSet was called once per unique (context, policyType) combination 443 | // If the cache key was only 'context', it would be called once and return wrong DbSet for 'g' 444 | Assert.Equal(1, callTracker["p"]); // Called once for 'p', then cached 445 | Assert.Equal(1, callTracker["g"]); // Called once for 'g', then cached 446 | 447 | // Verify data went to correct contexts 448 | Assert.Equal(2, policyContext.Policies.Count()); 449 | Assert.Equal(2, groupingContext.Policies.Count()); 450 | 451 | // Verify policy types are correct 452 | Assert.All(policyContext.Policies, p => Assert.Equal("p", p.Type)); 453 | Assert.All(groupingContext.Policies, g => Assert.Equal("g", g.Type)); 454 | } 455 | } 456 | 457 | /// 458 | /// Test adapter that tracks how many times GetCasbinRuleDbSet is called per policy type. 459 | /// This is used to verify the DbSet caching behavior. 460 | /// 461 | internal class DbSetCachingTestAdapter : EFCoreAdapter 462 | { 463 | private readonly Dictionary _callTracker; 464 | 465 | public DbSetCachingTestAdapter( 466 | ICasbinDbContextProvider contextProvider, 467 | Dictionary callTracker) 468 | : base(contextProvider) 469 | { 470 | _callTracker = callTracker; 471 | } 472 | 473 | protected override DbSet> GetCasbinRuleDbSet(DbContext dbContext, string policyType) 474 | { 475 | // Track that this method was called for this policy type 476 | // Only track non-null policy types (null is used for general operations) 477 | if (policyType != null) 478 | { 479 | if (!_callTracker.ContainsKey(policyType)) 480 | { 481 | _callTracker[policyType] = 0; 482 | } 483 | _callTracker[policyType]++; 484 | } 485 | 486 | // Call base implementation to get the actual DbSet 487 | return base.GetCasbinRuleDbSet(dbContext, policyType); 488 | } 489 | } 490 | } 491 | -------------------------------------------------------------------------------- /MULTI_CONTEXT_USAGE_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Multi-Context Support Usage Guide 2 | 3 | ## Overview 4 | 5 | Multi-context support allows you to store different Casbin policy types in separate database locations while maintaining a unified authorization model. 6 | 7 | **Use cases:** 8 | - Store policy rules (p, p2) and role assignments (g, g2) in separate schemas 9 | - Apply different retention policies per policy type 10 | - Separate concerns in multi-tenant systems 11 | 12 | **How it works:** 13 | - Each `CasbinDbContext` targets a different schema, table, or database 14 | - A context provider routes policy types to the appropriate context 15 | - The adapter automatically coordinates operations across all contexts 16 | 17 | ## Quick Start 18 | 19 | ### Step 1: Create Database Contexts 20 | 21 | Create separate `CasbinDbContext` instances that **share the same physical DbConnection object**. 22 | 23 | **⚠️ CRITICAL - Shared Connection Requirement:** 24 | 25 | For atomic transactions across contexts, you MUST pass the **same DbConnection object instance** to all contexts. EF Core's `UseTransaction()` requires reference equality of connection objects, not just matching connection strings. 26 | 27 | **✅ CORRECT: Share physical DbConnection object** 28 | 29 | ```csharp 30 | using Microsoft.EntityFrameworkCore; 31 | using Microsoft.Data.SqlClient; // or Npgsql.NpgsqlConnection, etc. 32 | using Casbin.Persist.Adapter.EFCore; 33 | 34 | // Create ONE shared connection object 35 | string connectionString = "Server=localhost;Database=CasbinDB;Trusted_Connection=True;"; 36 | var sharedConnection = new SqlConnection(connectionString); 37 | 38 | // Pass SAME connection instance to both contexts 39 | var policyContext = new CasbinDbContext( 40 | new DbContextOptionsBuilder>() 41 | .UseSqlServer(sharedConnection) // ← Shared connection object 42 | .Options, 43 | schemaName: "policies"); 44 | policyContext.Database.EnsureCreated(); 45 | 46 | var groupingContext = new CasbinDbContext( 47 | new DbContextOptionsBuilder>() 48 | .UseSqlServer(sharedConnection) // ← Same connection object 49 | .Options, 50 | schemaName: "groupings"); 51 | groupingContext.Database.EnsureCreated(); 52 | ``` 53 | 54 | **❌ WRONG: This will NOT provide atomic transactions** 55 | 56 | ```csharp 57 | // Each .UseSqlServer(connectionString) creates a DIFFERENT DbConnection object 58 | var policyContext = new CasbinDbContext( 59 | new DbContextOptionsBuilder>() 60 | .UseSqlServer(connectionString) // ← Creates DbConnection #1 61 | .Options); 62 | 63 | var groupingContext = new CasbinDbContext( 64 | new DbContextOptionsBuilder>() 65 | .UseSqlServer(connectionString) // ← Creates DbConnection #2 (different object!) 66 | .Options); 67 | 68 | // These contexts have different connection objects, so they CANNOT share transactions 69 | ``` 70 | 71 | **Other configuration options:** 72 | 73 | | Option | Use Case | Example | 74 | |--------|----------|---------| 75 | | **Different schemas** | SQL Server, PostgreSQL | `schemaName: "policies"` vs `schemaName: "groupings"` | 76 | | **Different tables** | Any database | `tableName: "casbin_policy"` vs `tableName: "casbin_grouping"` | 77 | | **Separate databases** | Testing only | `UseSqlite("policy.db")` vs `UseSqlite("grouping.db")` ⚠️ Not atomic | 78 | 79 | ### Step 2: Implement Context Provider 80 | 81 | Create a provider that routes policy types to contexts: 82 | 83 | ```csharp 84 | using System; 85 | using System.Collections.Generic; 86 | using Microsoft.EntityFrameworkCore; 87 | using Casbin.Persist.Adapter.EFCore; 88 | 89 | public class PolicyTypeContextProvider : ICasbinDbContextProvider 90 | { 91 | private readonly CasbinDbContext _policyContext; 92 | private readonly CasbinDbContext _groupingContext; 93 | 94 | public PolicyTypeContextProvider( 95 | CasbinDbContext policyContext, 96 | CasbinDbContext groupingContext) 97 | { 98 | _policyContext = policyContext ?? throw new ArgumentNullException(nameof(policyContext)); 99 | _groupingContext = groupingContext ?? throw new ArgumentNullException(nameof(groupingContext)); 100 | } 101 | 102 | public DbContext GetContextForPolicyType(string policyType) 103 | { 104 | if (string.IsNullOrEmpty(policyType)) 105 | return _policyContext; 106 | 107 | // Route: p/p2/p3 → policyContext, g/g2/g3 → groupingContext 108 | return policyType.StartsWith("p", StringComparison.OrdinalIgnoreCase) 109 | ? _policyContext 110 | : _groupingContext; 111 | } 112 | 113 | public IEnumerable GetAllContexts() 114 | { 115 | return new DbContext[] { _policyContext, _groupingContext }; 116 | } 117 | } 118 | ``` 119 | 120 | **Policy type routing:** 121 | 122 | | Policy Type | Context | Description | 123 | |-------------|---------|-------------| 124 | | `p`, `p2`, `p3`, ... | policyContext | Permission rules | 125 | | `g`, `g2`, `g3`, ... | groupingContext | Role/group assignments | 126 | 127 | ### Step 3-4: Create Adapter and Enforcer 128 | 129 | ```csharp 130 | // Create provider 131 | var provider = new PolicyTypeContextProvider(policyContext, groupingContext); 132 | 133 | // Create adapter with multi-context support 134 | var adapter = new EFCoreAdapter(provider); 135 | 136 | // Create enforcer (multi-context behavior is transparent) 137 | var enforcer = new Enforcer("path/to/model.conf", adapter); 138 | enforcer.LoadPolicy(); 139 | ``` 140 | 141 | ### Step 5: Use Normally 142 | 143 | ```csharp 144 | // Add policies (automatically routed to correct contexts) 145 | enforcer.AddPolicy("alice", "data1", "read"); // → policyContext 146 | enforcer.AddGroupingPolicy("alice", "admin"); // → groupingContext 147 | 148 | // Save (coordinated across both contexts) 149 | enforcer.SavePolicy(); 150 | 151 | // Check permissions (combines data from both contexts) 152 | bool allowed = enforcer.Enforce("alice", "data1", "read"); 153 | ``` 154 | 155 | ### Complete Example 156 | 157 | ```csharp 158 | using Microsoft.EntityFrameworkCore; 159 | using Microsoft.Data.SqlClient; 160 | using NetCasbin; 161 | using Casbin.Persist.Adapter.EFCore; 162 | 163 | public class Program 164 | { 165 | public static void Main() 166 | { 167 | // 1. Create shared connection object 168 | string connectionString = "Server=localhost;Database=CasbinDB;Trusted_Connection=True;"; 169 | var sharedConnection = new SqlConnection(connectionString); 170 | 171 | // 2. Create contexts with shared connection 172 | var policyContext = new CasbinDbContext( 173 | new DbContextOptionsBuilder>() 174 | .UseSqlServer(sharedConnection).Options, // ← Shared connection 175 | schemaName: "policies"); 176 | policyContext.Database.EnsureCreated(); 177 | 178 | var groupingContext = new CasbinDbContext( 179 | new DbContextOptionsBuilder>() 180 | .UseSqlServer(sharedConnection).Options, // ← Same connection 181 | schemaName: "groupings"); 182 | groupingContext.Database.EnsureCreated(); 183 | 184 | // 3. Create provider (use implementation from Step 2) 185 | var provider = new PolicyTypeContextProvider(policyContext, groupingContext); 186 | 187 | // 4. Create adapter and enforcer 188 | var adapter = new EFCoreAdapter(provider); 189 | var enforcer = new Enforcer("rbac_model.conf", adapter); 190 | 191 | // 5. Use enforcer (atomic transactions across both contexts) 192 | enforcer.AddPolicy("alice", "data1", "read"); 193 | enforcer.AddGroupingPolicy("alice", "admin"); 194 | enforcer.SavePolicy(); 195 | 196 | bool allowed = enforcer.Enforce("alice", "data1", "read"); 197 | Console.WriteLine($"Alice can read data1: {allowed}"); 198 | 199 | // 6. Cleanup 200 | sharedConnection.Dispose(); 201 | } 202 | } 203 | ``` 204 | 205 | ## Configuration Reference 206 | 207 | ### Async Operations 208 | 209 | All operations have async variants: 210 | 211 | ```csharp 212 | await enforcer.AddPolicyAsync("alice", "data1", "read"); 213 | await enforcer.AddGroupingPolicyAsync("alice", "admin"); 214 | await enforcer.SavePolicyAsync(); 215 | await enforcer.LoadPolicyAsync(); 216 | ``` 217 | 218 | ### Filtered Loading 219 | 220 | Load subsets of policies across all contexts by implementing `IPolicyFilter`: 221 | 222 | ```csharp 223 | using Casbin.Model; 224 | using Casbin.Persist; 225 | 226 | // Create a custom filter for specific field values 227 | public class SimpleFieldFilter : IPolicyFilter 228 | { 229 | private readonly PolicyFilter _policyFilter; 230 | 231 | public SimpleFieldFilter(string policyType, int fieldIndex, IPolicyValues values) 232 | { 233 | _policyFilter = new PolicyFilter(policyType, fieldIndex, values); 234 | } 235 | 236 | public IQueryable Apply(IQueryable policies) where T : IPersistPolicy 237 | { 238 | return _policyFilter.Apply(policies); 239 | } 240 | } 241 | 242 | // Use the filter to load only Alice's p policies 243 | enforcer.LoadFilteredPolicy( 244 | new SimpleFieldFilter("p", 0, Policy.ValuesFrom(new[] { "alice", "", "" })) 245 | ); 246 | ``` 247 | 248 | For more complex filtering scenarios (e.g., domain-based filtering), implement `IPolicyFilter` directly: 249 | 250 | ```csharp 251 | public class DomainFilter : IPolicyFilter 252 | { 253 | private readonly string _domain; 254 | 255 | public DomainFilter(string domain) => _domain = domain; 256 | 257 | public IQueryable Apply(IQueryable policies) where T : IPersistPolicy 258 | { 259 | return policies.Where(p => 260 | (p.Type == "p" && p.Value2 == _domain) || // Filter p policies by domain 261 | (p.Type == "g" && p.Value3 == _domain) // Filter g policies by domain 262 | ); 263 | } 264 | } 265 | 266 | // Load policies for a specific domain 267 | enforcer.LoadFilteredPolicy(new DomainFilter("tenant-123")); 268 | ``` 269 | 270 | ### Dependency Injection 271 | 272 | For ASP.NET Core applications with shared connection: 273 | 274 | ```csharp 275 | // Register shared connection as singleton 276 | services.AddSingleton(sp => 277 | { 278 | var connectionString = Configuration.GetConnectionString("Casbin"); 279 | return new SqlConnection(connectionString); 280 | }); 281 | 282 | // Register context provider with shared connection 283 | services.AddSingleton>(sp => 284 | { 285 | var sharedConnection = sp.GetRequiredService(); 286 | 287 | var policyCtx = new CasbinDbContext( 288 | new DbContextOptionsBuilder>() 289 | .UseSqlServer(sharedConnection).Options, // Shared connection 290 | schemaName: "policies"); 291 | 292 | var groupingCtx = new CasbinDbContext( 293 | new DbContextOptionsBuilder>() 294 | .UseSqlServer(sharedConnection).Options, // Same connection 295 | schemaName: "groupings"); 296 | 297 | return new PolicyTypeContextProvider(policyCtx, groupingCtx); 298 | }); 299 | 300 | services.AddSingleton(sp => 301 | { 302 | var provider = sp.GetRequiredService>(); 303 | return new EFCoreAdapter(provider); 304 | }); 305 | 306 | services.AddSingleton(sp => 307 | { 308 | var adapter = sp.GetRequiredService(); 309 | return new Enforcer("rbac_model.conf", adapter); 310 | }); 311 | ``` 312 | 313 | ### Connection Lifetime Management 314 | 315 | **Important:** When using shared connections, you are responsible for connection lifetime: 316 | 317 | **In simple applications:** 318 | ```csharp 319 | // Create connection 320 | var connection = new SqlConnection(connectionString); 321 | 322 | // Use for contexts/adapter/enforcer 323 | // ... (create contexts, adapter, enforcer) 324 | 325 | // Dispose when done 326 | connection.Dispose(); 327 | ``` 328 | 329 | **With using statement:** 330 | ```csharp 331 | using (var connection = new SqlConnection(connectionString)) 332 | { 333 | // Create contexts with shared connection 334 | var policyCtx = new CasbinDbContext(...); 335 | var groupingCtx = new CasbinDbContext(...); 336 | 337 | // Create and use enforcer 338 | var provider = new PolicyTypeContextProvider(policyCtx, groupingCtx); 339 | var adapter = new EFCoreAdapter(provider); 340 | var enforcer = new Enforcer("model.conf", adapter); 341 | 342 | enforcer.LoadPolicy(); 343 | // ... use enforcer 344 | 345 | } // Connection disposed automatically 346 | ``` 347 | 348 | **In DI scenarios:** 349 | 350 | The DbConnection is registered as a singleton and will be disposed when the application shuts down. No manual disposal needed in request handlers. 351 | 352 | ## Transaction Behavior 353 | 354 | ### Shared Connection Requirements 355 | 356 | **For atomic transactions across contexts, all contexts MUST share the same DbConnection object instance.** 357 | 358 | **How atomic transactions work:** 359 | 1. You create ONE DbConnection object and pass it to all contexts 360 | 2. Adapter detects shared connection via `CanShareTransaction()` (reference equality check) 361 | 3. Adapter uses `UseTransaction()` to enlist all contexts in one transaction 362 | 4. Database ensures atomic commit/rollback across both contexts 363 | 364 | **✅ CORRECT Example:** 365 | 366 | Already shown in Step 1 - create shared DbConnection and pass to all contexts. 367 | 368 | ### EnableAutoSave and Transaction Atomicity 369 | 370 | The Casbin Enforcer's `EnableAutoSave` setting fundamentally affects transaction atomicity in multi-context scenarios. 371 | 372 | #### Understanding AutoSave Modes 373 | 374 | **EnableAutoSave(true) - Immediate Commits (Default)** 375 | 376 | When AutoSave is enabled (the default), each `AddPolicy`/`RemovePolicy`/`UpdatePolicy` operation commits immediately to the database. 377 | 378 | **Behavior:** 379 | - Each individual operation is fully atomic (succeeds or fails completely) 380 | - Each operation creates its own implicit database transaction 381 | - **No atomicity across multiple operations:** 382 | - If you execute 3 operations sequentially and the 3rd fails, the first 2 remain committed 383 | - Earlier operations cannot be rolled back when later operations fail 384 | - Each operation is independent 385 | 386 | **Use Cases:** 387 | - Real-time policy updates where each change is independent 388 | - Single-context usage where cross-context atomicity isn't required 389 | - Scenarios where you can tolerate some operations committing while others don't 390 | 391 | **Example - Independent Commits:** 392 | ```csharp 393 | var enforcer = new Enforcer(model, adapter); 394 | enforcer.EnableAutoSave(true); // Default behavior 395 | 396 | // Each operation commits immediately and independently: 397 | await enforcer.AddPolicyAsync("alice", "data1", "read"); // ← Commits to DB now 398 | await enforcer.AddGroupingPolicyAsync("alice", "admin"); // ← Commits to DB now 399 | await enforcer.AddNamedGroupingPolicyAsync("g2", "admin", "super"); // ← If this fails... 400 | 401 | // ⚠️ The first 2 operations are already committed and CANNOT be rolled back 402 | ``` 403 | 404 | **EnableAutoSave(false) - Batched Atomic Commits** 405 | 406 | When AutoSave is disabled, all operations stay in memory until `enforcer.SavePolicyAsync()` is called. 407 | 408 | **Behavior:** 409 | - Operations stored in Casbin's in-memory policy store (not database) 410 | - When `SavePolicyAsync()` is called with shared connection: 411 | - All contexts enlisted in single connection-level transaction 412 | - All operations commit atomically (all-or-nothing) 413 | - If any operation fails, entire transaction rolls back 414 | - **Full atomicity across all operations** 415 | 416 | **Use Cases:** 417 | - Multi-context scenarios requiring atomicity 418 | - Batch policy updates that must succeed or fail together 419 | - Critical operations where partial application is unacceptable 420 | - Production systems with ACID requirements 421 | 422 | **Example - Atomic Batch Commit:** 423 | ```csharp 424 | var enforcer = new Enforcer(model, adapter); 425 | enforcer.EnableAutoSave(false); // Disable AutoSave for atomicity 426 | 427 | // All operations stay in memory (not committed yet): 428 | await enforcer.AddPolicyAsync("alice", "data1", "read"); // In memory only 429 | await enforcer.AddGroupingPolicyAsync("alice", "admin"); // In memory only 430 | await enforcer.AddNamedGroupingPolicyAsync("g2", "admin", "super"); // In memory only 431 | 432 | // Commit all operations atomically (all-or-nothing): 433 | await enforcer.SavePolicyAsync(); // ← All 3 commit together OR all 3 roll back 434 | 435 | // ✅ Either all 3 policies exist in database, or none do 436 | ``` 437 | 438 | #### Recommendation for Multi-Context Atomicity 439 | 440 | > **💡 Best Practice** 441 | > 442 | > When using multiple contexts and you need all policy changes to succeed or fail together: 443 | > 444 | > 1. **Disable AutoSave:** `enforcer.EnableAutoSave(false)` 445 | > 2. **Use shared connection:** Ensure all contexts share the same `DbConnection` object (see above) 446 | > 3. **Batch commit:** Call `await enforcer.SavePolicyAsync()` to commit atomically 447 | > 448 | > This ensures all policy changes across all contexts are committed atomically or rolled back together. 449 | 450 | #### Real-World Example: Authorization Setup 451 | 452 | **Scenario:** Setting up a new user with permissions and role assignments. 453 | 454 | **Without Atomicity (AutoSave ON - Default):** 455 | ```csharp 456 | // AutoSave is ON by default 457 | await enforcer.AddPolicyAsync("bob", "data1", "read"); // ✓ Committed to policies schema 458 | await enforcer.AddPolicyAsync("bob", "data1", "write"); // ✓ Committed to policies schema 459 | await enforcer.AddGroupingPolicyAsync("bob", "admin"); // ✗ FAILS - network error 460 | 461 | // Problem: Bob has partial permissions (read/write) but no admin role 462 | // Result: Inconsistent authorization state 463 | ``` 464 | 465 | **With Atomicity (AutoSave OFF):** 466 | ```csharp 467 | enforcer.EnableAutoSave(false); // Require explicit save 468 | 469 | await enforcer.AddPolicyAsync("bob", "data1", "read"); // In memory 470 | await enforcer.AddPolicyAsync("bob", "data1", "write"); // In memory 471 | await enforcer.AddGroupingPolicyAsync("bob", "admin"); // In memory 472 | 473 | try 474 | { 475 | await enforcer.SavePolicyAsync(); // Atomic commit - all or nothing 476 | // ✓ Success: All 3 policies committed 477 | } 478 | catch (Exception ex) 479 | { 480 | // ✓ Failure: All 3 policies rolled back automatically 481 | // Result: Bob has no permissions (consistent state) 482 | Console.WriteLine($"Setup failed: {ex.Message}"); 483 | } 484 | ``` 485 | 486 | #### Technical Details 487 | 488 | **How AutoSave Affects Transaction Coordination:** 489 | 490 | With **AutoSave ON**, the Casbin Enforcer immediately calls the adapter's methods for each operation. The adapter has no opportunity to coordinate transactions because it receives operations one at a time. 491 | 492 | **Call Flow (AutoSave ON):** 493 | ``` 494 | User: enforcer.AddPolicyAsync() 495 | → Enforcer: Immediately calls adapter.AddPolicyAsync() 496 | → Adapter: context.SaveChangesAsync() → Database (committed) 497 | → Returns to user 498 | ``` 499 | 500 | With **AutoSave OFF**, operations accumulate in memory. Only when `SavePolicyAsync()` is called does the adapter receive all policies at once, enabling atomic transaction coordination. 501 | 502 | **Call Flow (AutoSave OFF):** 503 | ``` 504 | User: enforcer.AddPolicyAsync() 505 | → Enforcer: Stores in memory, does NOT call adapter 506 | → Returns to user 507 | 508 | User: enforcer.SavePolicyAsync() 509 | → Enforcer: Calls adapter.SavePolicyAsync() with ALL policies 510 | → Adapter: Starts shared transaction 511 | → Adapter: Enlists all contexts in transaction 512 | → Adapter: Commits/clears all contexts 513 | → Adapter: Commits transaction atomically 514 | → Returns to user 515 | ``` 516 | 517 | **For More Details:** See [Integration Test README](Casbin.Persist.Adapter.EFCore.UnitTest/Integration/README.md) for test evidence of this behavior, particularly the rollback tests that require `EnableAutoSave(false)`. 518 | 519 | ### Context Factory Pattern (Recommended) 520 | 521 | ```csharp 522 | public class CasbinContextFactory : IDisposable 523 | { 524 | private readonly DbConnection _sharedConnection; 525 | 526 | public CasbinContextFactory(IConfiguration configuration) 527 | { 528 | var connectionString = configuration.GetConnectionString("Casbin"); 529 | _sharedConnection = new SqlConnection(connectionString); // Create shared connection once 530 | } 531 | 532 | public CasbinDbContext CreateContext(string schemaName) 533 | { 534 | var options = new DbContextOptionsBuilder>() 535 | .UseSqlServer(_sharedConnection) // ← Share same connection object 536 | .Options; 537 | return new CasbinDbContext(options, schemaName: schemaName); 538 | } 539 | 540 | public void Dispose() 541 | { 542 | _sharedConnection?.Dispose(); 543 | } 544 | } 545 | 546 | // Usage 547 | using var factory = new CasbinContextFactory(configuration); 548 | var policyContext = factory.CreateContext("policies"); 549 | var groupingContext = factory.CreateContext("groupings"); 550 | // Both contexts share the same physical connection object 551 | ``` 552 | 553 | ### Database Compatibility 554 | 555 | | Database | Atomic Transactions | Connection Requirement | Notes | 556 | |----------|-------------------|----------------------|-------| 557 | | **SQL Server** | ✅ Yes | Same DbConnection object | Works with different schemas/tables | 558 | | **PostgreSQL** | ✅ Yes | Same DbConnection object | Works with different schemas/tables | 559 | | **MySQL** | ✅ Yes | Same DbConnection object | Works with different schemas/tables | 560 | | **SQLite** | ✅ Yes | Same DbConnection object | Works with different tables in same file | 561 | 562 | **Note:** "Same database" requires **same DbConnection object instance**, not just matching connection strings. 563 | 564 | ### Responsibility Matrix 565 | 566 | | Task | Your Responsibility | Adapter Responsibility | 567 | |------|-------------------|----------------------| 568 | | Create shared DbConnection object | ✅ YES | ❌ NO | 569 | | Pass same connection to all contexts | ✅ YES | ❌ NO | 570 | | Manage connection lifetime | ✅ YES | ❌ NO | 571 | | Use context factory pattern | ✅ YES (recommended) | ❌ NO | 572 | | Call `UseTransaction()` | ❌ NO | ✅ YES (internal) | 573 | | Detect shared connection (reference equality) | ❌ NO | ✅ YES | 574 | | Coordinate commit/rollback | ❌ NO | ✅ YES | 575 | 576 | ### When Separate Connections Are Acceptable 577 | 578 | **Non-atomic behavior (individual transactions per context) may be acceptable for:** 579 | - Testing and development 580 | - Read-heavy workloads with eventual consistency 581 | - Non-critical data 582 | 583 | **Not acceptable for:** 584 | - Production ACID requirements (financial, authorization) 585 | - Compliance/audit scenarios 586 | - Multi-tenant SaaS with strict data integrity 587 | 588 | ## Troubleshooting 589 | 590 | ### "No such table" errors 591 | 592 | **Cause:** Database tables not created. 593 | 594 | **Solution:** 595 | ```csharp 596 | policyContext.Database.EnsureCreated(); 597 | groupingContext.Database.EnsureCreated(); 598 | ``` 599 | 600 | ### Partial data committed on failure 601 | 602 | **Cause:** Using separate database connections (e.g., different SQLite files). 603 | 604 | **Solution:** Use same database with different schemas/tables: 605 | ```csharp 606 | // Instead of separate files 607 | .UseSqlite("Data Source=policy.db") 608 | .UseSqlite("Data Source=grouping.db") 609 | 610 | // Use same file with different tables 611 | .UseSqlite("Data Source=casbin.db") // Both use same file 612 | // Configure different table names 613 | ``` 614 | 615 | ### Transaction warnings in logs 616 | 617 | **Cause:** Adapter detected different connection strings and fell back to individual transactions. 618 | 619 | **Solution:** Ensure all contexts use the same connection string variable (see [Context Factory Pattern](#context-factory-pattern-recommended)). 620 | 621 | ## See Also 622 | 623 | - [MULTI_CONTEXT_DESIGN.md](MULTI_CONTEXT_DESIGN.md) - Technical architecture and implementation details 624 | - [Casbin.NET Documentation](https://casbin.org/docs/overview) - Casbin concepts and model syntax 625 | - [ICasbinDbContextProvider Interface](Casbin.Persist.Adapter.EFCore/ICasbinDbContextProvider.cs) - Interface definition 626 | --------------------------------------------------------------------------------