├── 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 | [](https://github.com/casbin-net/efcore-adapter/actions)
4 | [](https://coveralls.io/github/casbin-net/EFCore-Adapter?branch=master)
5 | [](https://www.nuget.org/packages/Casbin.NET.Adapter.EFCore/)
6 | [](https://github.com/casbin-net/efcore-adapter/releases/latest)
7 | [](https://www.nuget.org/packages/Casbin.NET.Adapter.EFCore/)
8 | [](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