├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ ├── copy-docs.yml │ ├── dependabot-clone.yml │ ├── nuget-push-public.yml │ ├── release-dryrun.yml │ ├── release.yml │ └── tidy.yml ├── .gitignore ├── .releaserc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml ├── Finbuckle.MultiTenant.sln ├── LICENSE ├── NUGET_README.md ├── README.md ├── SECURITY.md ├── docs ├── Authentication.md ├── ConfigurationAndUsage.md ├── CoreConcepts.md ├── EFCore.md ├── GettingStarted.md ├── History.md ├── Identity.md ├── Index.md ├── Introduction.md ├── Options.md ├── Stores.md ├── Strategies.md └── WhatsNew.md ├── finbuckle-128x128.png ├── release.sh ├── samples ├── README.md └── v9 │ ├── net8 │ └── webapi │ │ ├── AppTenantInfo.cs │ │ ├── Program.cs │ │ ├── Properties │ │ └── launchSettings.json │ │ ├── README.md │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ └── webapi.csproj │ └── net9 │ └── webapi │ ├── AppTenantInfo.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── README.md │ ├── appsettings.Development.json │ ├── appsettings.json │ └── webapi.csproj ├── src ├── Directory.Build.props ├── Finbuckle.MultiTenant.AspNetCore │ ├── AssemblyInfo.cs │ ├── Extensions │ │ ├── ApplicationBuilderExtensions.cs │ │ ├── EndpointConventionBuilderExtensions.cs │ │ ├── HttpContextExtensions.cs │ │ └── MultiTenantBuilderExtensions.cs │ ├── Finbuckle.MultiTenant.AspNetCore.csproj │ ├── Internal │ │ ├── MultiTenantAuthenticationSchemeProvider.cs │ │ ├── MultiTenantAuthenticationService.cs │ │ └── MultiTenantMiddleware.cs │ ├── Options │ │ ├── BasePathStrategyOptions.cs │ │ ├── MultiTenantAuthenticationOptions.cs │ │ └── ShortCircuitWhenOptions.cs │ ├── Routing │ │ ├── ExcludeFromMultiTenantResolutionAttribute.cs │ │ └── IExcludeFromMultiTenantResolutionMetadata.cs │ ├── Strategies │ │ ├── BasePathStrategy.cs │ │ ├── ClaimStrategy.cs │ │ ├── HeaderStrategy.cs │ │ ├── HostStrategy.cs │ │ ├── RemoteAuthenticationCallbackStrategy.cs │ │ ├── RouteStrategy.cs │ │ └── SessionStrategy.cs │ └── packages.lock.json ├── Finbuckle.MultiTenant.EntityFrameworkCore │ ├── AssemblyInfo.cs │ ├── Constants.cs │ ├── Enums.cs │ ├── Extensions │ │ ├── EntityTypeBuilderExtensions.cs │ │ ├── EntityTypeExtensions.cs │ │ ├── ModelBuilderExtensions.cs │ │ ├── ModelExtensions.cs │ │ ├── MultiTenantBuilderExtensions.cs │ │ ├── MultiTenantDbContextExtensions.cs │ │ └── MultiTenantEntityTypeBuilderExtensions.cs │ ├── Finbuckle.MultiTenant.EntityFrameworkCore.csproj │ ├── IMultiTenantDbContext.cs │ ├── MultiTenantDbContext.cs │ ├── MultiTenantEntityTypeBuilder.cs │ ├── MultiTenantIdentityDbContext.cs │ ├── Stores │ │ └── EFCoreStore │ │ │ ├── EFCoreStore.cs │ │ │ └── EFCoreStoreDbContext.cs │ └── packages.lock.json └── Finbuckle.MultiTenant │ ├── Abstractions │ ├── IMultiTenantContext.cs │ ├── IMultiTenantContextAccessor.cs │ ├── IMultiTenantContextSetter.cs │ ├── IMultiTenantStore.cs │ ├── IMultiTenantStrategy.cs │ ├── ITenantInfo.cs │ └── ITenantResolver.cs │ ├── AssemblyInfo.cs │ ├── DependencyInjection │ ├── MultiTenantBuilderExtensions.cs │ ├── OptionsBuilderExtensions.cs │ └── ServiceCollectionExtensions.cs │ ├── Events │ ├── MultiTenantEvents.cs │ ├── StoreResolveCompletedContext.cs │ ├── StrategyResolveCompletedContext.cs │ └── TenantResolveCompletedContext.cs │ ├── Finbuckle.MultiTenant.csproj │ ├── Internal │ ├── AsyncLocalMultiTenantContextAccessor.cs │ ├── Constants.cs │ ├── StaticMultiTenantContextAccessor.cs │ └── TypeExtensions.cs │ ├── MultiTenantAttribute.cs │ ├── MultiTenantBuilder.cs │ ├── MultiTenantContext.cs │ ├── MultiTenantException.cs │ ├── MultiTenantOptions.cs │ ├── Options │ ├── MultiTenantOptionsCache.cs │ └── MultiTenantOptionsManager.cs │ ├── StoreInfo.cs │ ├── Stores │ ├── ConfigurationStore │ │ └── ConfigurationStore.cs │ ├── DistributedCacheStore │ │ └── DistributedCacheStore.cs │ ├── EchoStore │ │ └── EchoStore.cs │ ├── HttpRemoteStore │ │ ├── HttpRemoteStore.cs │ │ └── HttpRemoteStoreClient.cs │ ├── InMemoryStore │ │ ├── InMemoryStore.cs │ │ └── InMemoryStoreOptions.cs │ └── MultiTenantStoreWrapper.cs │ ├── Strategies │ ├── DelegateStrategy.cs │ ├── MultiTenantStrategyWrapper.cs │ └── StaticStrategy.cs │ ├── StrategyInfo.cs │ ├── TenantInfo.cs │ ├── TenantResolver.cs │ └── packages.lock.json └── test ├── Directory.Build.props ├── Finbuckle.MultiTenant.AspNetCore.Test ├── Extensions │ ├── HttpContextExtensionShould.cs │ └── MultiTenantBuilderExtensionsShould.cs ├── Finbuckle.MultiTenant.AspNetCore.Test.csproj ├── MultiTenantAuthenticationSchemeProviderShould.cs ├── MultiTenantMiddlewareShould.cs ├── MultiTenantRouteBuilderShould.cs ├── Routing │ └── ExcludeFromMultiTenantResolutionShould.cs └── Strategies │ ├── BasePathStrategyShould.cs │ ├── HostStrategyShould.cs │ ├── RemoteAuthenticationCallbackStrategyShould.cs │ ├── RouteStrategyShould.cs │ └── SessionStrategyShould.cs ├── Finbuckle.MultiTenant.EntityFrameworkCore.Test ├── Extensions │ ├── EntityTypeBuilderExtensions │ │ ├── EntityTypeBuilderExtensionsShould.cs │ │ ├── TestDbContext.cs │ │ └── TestIdentityDbContext.cs │ ├── EntityTypeExtensions │ │ ├── EntityTypeExtensionsShould.cs │ │ └── TestDbContext.cs │ ├── ModelBuilderExtensions │ │ ├── ModelBuilderExtensionsShould.cs │ │ └── TestDbContext.cs │ ├── ModelExtensions │ │ ├── ModelExtensionsShould.cs │ │ └── TestDbContext.cs │ ├── MultiTenantBuilderExtensions │ │ ├── MultiTenantBuilderExtensionsShould.cs │ │ └── TestEfCoreStoreDbContext.cs │ ├── MultiTenantDbContextExtensions │ │ ├── MultiTenantDbContextExtensionShould.cs │ │ └── TestDbContext.cs │ └── MultiTenantEntityTypeBuilderExtensions │ │ ├── MultiTenantEntityTypeBuilderExtensionsShould.cs │ │ └── TestDbContext.cs ├── Finbuckle.MultiTenant.EntityFrameworkCore.Test.csproj ├── MultiTenantDbContext │ ├── MultiTenantDbContextShould.cs │ └── TestEntitities.cs ├── MultiTenantEntityTypeBuilder │ ├── MultiTenantEntityTypeBuilderShould.cs │ └── TestDbContext.cs ├── MultiTenantIdentityDbContext │ ├── MultiTenanIdentitytDbContextShould.cs │ ├── TestIdentityDbContext.cs │ ├── TestIdentityDbContextAll.cs │ ├── TestIdentityDbContextTUser.cs │ └── TestIdentityDbContextTUserTRole.cs └── Stores │ └── EFCoreStoreShould.cs └── Finbuckle.MultiTenant.Test ├── ConfigurationStoreTestSettings.json ├── ConfigurationStoreTestSettings_NoDefaults.json ├── DependencyInjection ├── MultiTenantBuilderExtensionsShould.cs ├── MultiTenantBuilderShould.cs └── ServiceCollectionExtensionsShould.cs ├── Finbuckle.MultiTenant.Test.csproj ├── MultiTenantContextShould.cs ├── Options ├── MultiTenantOptionsCacheShould.cs └── MultiTenantOptionsManagerShould.cs ├── Stores ├── ConfigurationStoreShould.cs ├── DistributedCacheStoreShould.cs ├── HttpRemoteStoreClientShould.cs ├── HttpRemoteStoreShould.cs ├── IMultiTenantStoreTestBase.cs ├── InMemoryStoreShould.cs └── MultiTenantStoreWrapperShould.cs ├── Strategies ├── DelegateStrategyShould.cs └── StaticStrategyShould.cs ├── TenantInfoShould.cs └── TenantResolverShould.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.cs text 4 | *.cshtml text 5 | *.sln text 6 | *.csproj text 7 | *.md text 8 | *.json text 9 | *.props text -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI Workflow 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build-and-test: 10 | strategy: 11 | matrix: 12 | dotnet: ['8.0', '9.0'] 13 | os: [ubuntu-latest, windows-latest, macos-latest] 14 | runs-on: ${{ matrix.os }} 15 | steps: 16 | - name: checkout repo 17 | uses: actions/checkout@v4 18 | - uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: | 21 | 8 22 | 9 23 | - name: build 24 | run: dotnet build 25 | - name: test Finbuckle.MultiTenant 26 | run: dotnet test --no-build -v q -f net${{ matrix.dotnet }} 27 | working-directory: ./test/Finbuckle.MultiTenant.Test 28 | - name: test Finbuckle.MultiTenant.AspNetCore 29 | run: dotnet test --no-build -v q -f net${{ matrix.dotnet }} 30 | working-directory: ./test/Finbuckle.MultiTenant.AspNetCore.Test 31 | - name: test Finbuckle.MultiTenant.EntityFrameworkCore 32 | run: dotnet test --no-build -v q -f net${{ matrix.dotnet }} 33 | working-directory: ./test/Finbuckle.MultiTenant.EntityFrameworkCore.Test 34 | 35 | -------------------------------------------------------------------------------- /.github/workflows/copy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Copy Docs to Website 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: [published] 7 | 8 | jobs: 9 | checkout-copy-checkin: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Finbuckle.MultiTenant 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Get Current Version 17 | run: echo "tag=$(git describe --tags --abbrev=0 --match 'v*')" >> $GITHUB_ENV 18 | - name: Checkout Website 19 | uses: actions/checkout@v4 20 | with: 21 | repository: Finbuckle/Website 22 | token: ${{ secrets.workflow_pat }} 23 | path: website 24 | - name: Copy Docs 25 | run: mkdir -p website/content/docs/${{ env.tag }} && cp docs/* website/content/docs/${{ env.tag }} 26 | - name: Checkin Website 27 | working-directory: website 28 | run: | 29 | git config user.name github-actions 30 | git config user.email github-actions@github.com 31 | git add . 32 | git commit -m "chore: docs generated" 33 | git push 34 | -------------------------------------------------------------------------------- /.github/workflows/dependabot-clone.yml: -------------------------------------------------------------------------------- 1 | name: Dependabot Clone 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 1 * * *' 7 | 8 | jobs: 9 | check-and-update-dependencies: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | branch: [ main, 8.x ] 14 | steps: 15 | - name: Setup .NET 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: 9 19 | - name: Checkout Finbuckle.MultiTenant 20 | uses: actions/checkout@v4 21 | with: 22 | ref: ${{ matrix.branch }} 23 | fetch-depth: 0 24 | 25 | - name: Checkout branch for update 26 | run: git checkout -b fix/update-dependencies-${{ matrix.branch }} || git checkout fix/update-dependencies-${{ matrix.branch }} 27 | continue-on-error: true 28 | 29 | - name: Restore dependencies 30 | run: dotnet restore --force-evaluate 31 | 32 | - name: Check for changes in a packages.lock.json 33 | id: check-for-changes 34 | run: git diff --exit-code src/*/packages.lock.json || echo CHANGED=1 >> $GITHUB_ENV 35 | continue-on-error: true 36 | 37 | - name: Push changes if changed 38 | if: env.CHANGED == '1' 39 | run: | 40 | git config user.name github-actions 41 | git config user.email github-actions@github.com 42 | git add . 43 | git commit -m "fix: update dependencies" 44 | git push origin fix/update-dependencies-${{ matrix.branch }} 45 | 46 | - name: Create PR if changed 47 | if: env.CHANGED == '1' 48 | env: 49 | GH_TOKEN: ${{ github.token }} 50 | run: | 51 | gh pr create --title "Update dependencies for ${{ matrix.branch }}" --body "This PR updates project dependencies." --head fix/update-dependencies-${{ matrix.branch }} --base ${{ matrix.branch }} 52 | continue-on-error: true -------------------------------------------------------------------------------- /.github/workflows/nuget-push-public.yml: -------------------------------------------------------------------------------- 1 | name: NuGet Push Public 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | build-test-prep-deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-dotnet@v4 11 | with: 12 | dotnet-version: | 13 | 8 14 | 9 15 | - name: Create the package 16 | run: dotnet test -c Release -p:ContinuousIntegrationBuild=true && dotnet pack --no-build -c Release --output nupkgs 17 | - name: Publish the package to NuGet.org 18 | env: 19 | NUGET_KEY: ${{secrets.NUGET_KEY}} 20 | run: dotnet nuget push nupkgs/*.nupkg -k $NUGET_KEY -s https://api.nuget.org/v3/index.json --skip-duplicate -------------------------------------------------------------------------------- /.github/workflows/release-dryrun.yml: -------------------------------------------------------------------------------- 1 | name: Release Dry Run 2 | 3 | on: [workflow_dispatch] 4 | 5 | jobs: 6 | build-test-prep-release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: | 16 | 8 17 | 9 18 | - name: build and test 19 | run: | 20 | dotnet restore 21 | dotnet build -c Release --no-restore -p:ContinuousIntegrationBuild=true 22 | dotnet test -c Release --no-build 23 | - name: setup semantic-release 24 | run: | 25 | npm install -D semantic-release 26 | npm install -D @semantic-release/git 27 | npm install -D @semantic-release/changelog 28 | npm install -D @semantic-release/exec 29 | npm install -D conventional-changelog-conventionalcommits 30 | - name: run semantic-release 31 | env: 32 | GH_TOKEN: ${{ secrets.workflow_pat }} 33 | run: npx semantic-release --dry-run 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: [ workflow_dispatch ] 4 | 5 | jobs: 6 | build-test-prep-release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: 20 13 | - uses: actions/setup-dotnet@v4 14 | with: 15 | dotnet-version: | 16 | 8 17 | 9 18 | - name: build and test 19 | run: | 20 | dotnet restore 21 | dotnet build -c Release --no-restore -p:ContinuousIntegrationBuild=true 22 | dotnet test -c Release --no-build 23 | - name: setup semantic-release 24 | run: | 25 | npm install -D semantic-release 26 | npm install -D @semantic-release/git 27 | npm install -D @semantic-release/changelog 28 | npm install -D @semantic-release/exec 29 | npm install -D conventional-changelog-conventionalcommits 30 | - name: run semantic-release 31 | env: 32 | GH_TOKEN: ${{ secrets.workflow_pat }} 33 | run: npx semantic-release -------------------------------------------------------------------------------- /.github/workflows/tidy.yml: -------------------------------------------------------------------------------- 1 | name: Tidy Issues and PRs 2 | on: 3 | schedule: 4 | - cron: 30 1 * * * 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | days-before-stale: 180 13 | days-before-close: -1 14 | stale-issue-label: inactive 15 | stale-pr-label: inactive 16 | exempt-issue-labels: pinned 17 | exempt-pr-labels: pinned 18 | stale-issue-message: This issue has been labeled inactive because it has been open 180 days with no activity. Please consider closing this issue if no further action is needed. 19 | stale-pr-message: This PR has been labeled inactive because it has been open 180 days with no activity. Please consider closing this PR if no further action is needed. 20 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["main"], 3 | "plugins": [ 4 | ["@semantic-release/commit-analyzer", { 5 | "preset": "conventionalcommits" 6 | }], 7 | ["@semantic-release/release-notes-generator", { 8 | "preset": "conventionalcommits" 9 | }], 10 | "@semantic-release/github", 11 | "@semantic-release/changelog", 12 | ["@semantic-release/exec", { 13 | "prepareCmd": "./release.sh -f private -v ${nextRelease.version} -r '${nextRelease.notes}'" 14 | }], 15 | ["@semantic-release/git", { 16 | "assets": ["src/Directory.Build.props", "*.md", "docs"], 17 | "message": "chore: ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 18 | }] 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. 2 | For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct). 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are always welcome. 4 | 5 | New, updated, and improved samples, docs, and tests are always needed. 6 | 7 | For small changes and corrections please go ahead and open a pull request. 8 | 9 | For more complex changes or new features please file an issue before you open a pull request so we can discuss and plan 10 | accordingly. 11 | 12 | Regarding new features, the repository prioritizes maintainability, core functionality, and common use-cases. 13 | Additional features will be considered on a case-by-case basis. 14 | 15 | Authors are encouraged to host their own repositories for functionality outside these guidelines and to submit a pull 16 | request adding an appropriate section, description, and link to their repository in to the official docs. -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [finbuckle] -------------------------------------------------------------------------------- /NUGET_README.md: -------------------------------------------------------------------------------- 1 | ## About Finbuckle.MultiTenant 2 | 3 | Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, 4 | per-tenant app behavior, and per-tenant data isolation. 5 | See [https://www.finbuckle.com/multitenant](https://www.finbuckle.com/multitenant) for more details and documentation. 6 | 7 | See the [release history](https://www.finbuckle.com/MultiTenant/Docs/History) for all release details. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you discover a security issue in Finbuckle.MultiTenant, please report it by sending an email to security@finbuckle.com 4 | 5 | This will allow us to assess the risk, and make a fix available before we add a bug report to the GitHub repository. 6 | 7 | Thanks! 8 | -------------------------------------------------------------------------------- /docs/CoreConcepts.md: -------------------------------------------------------------------------------- 1 | # Core Concepts 2 | 3 | The library uses standard .NET Core conventions and most of the internal details are abstracted away from app code. 4 | However, there are a few important specifics to be aware of. The items below make up the foundation of the library 5 | 6 | ## `ITenantInfo` and `TenantInfo` 7 | 8 | A `TenantInfo` instance contains information about a tenant. Often this will be the "current" tenant in the context an 9 | app. These instances' type implements `ITenantInfo` which defines properties 10 | for `Id`, `Identifier`, `Name`. When calling `AddMultiTenant` the type passed into the 11 | type parameter defines the`ITenantInfo` use throughout the library and app. 12 | 13 | * `Id` is a unique id for a tenant in the app and should never change. 14 | * `Identifier` is the value used to actually resolve a tenant and should have a syntax compatible for the app (i.e. no 15 | crazy symbols in a web app where the identifier will be part of the URL). Unlike `Id`, `Identifier` can be changed if 16 | necessary. 17 | * `Name` is a display name for the tenant. 18 | 19 | `TenantInfo` is a provided basic implementation of `ITenantInfo` with only the required properties. 20 | 21 | An app can define a custom `ITenantInfo` and add custom properties as needed. We recommend keeping these 22 | classes lightweight since they are often queried. Keep heavier associated data in an external area that can be pulled in 23 | when needed via the tenant `Id`. 24 | 25 | > Previous versions of `ITenantInfo` and `TenantInfo` included a connection string property. If you still need this 26 | > simply add it to your custom `ITenantInfo` implementation. 27 | 28 | ## `MultiTenantContext` 29 | 30 | The `MultiTenantContext` contains information about the current tenant. 31 | 32 | * Implements `IMultiTenantContext` and `IMultiTenantContext` which can be obtained from dependency injection. 33 | * Includes `TenantInfo`, `StrategyInfo`, and `StoreInfo` properties with details on the current tenant, how it was 34 | determined, and from where its information was retrieved. 35 | * Can be obtained in ASP.NET Core by calling the `GetMultiTenantContext()` method on the current request's `HttpContext` 36 | object. The implementation used with ASP.NET Core middleware has read only properties. The `HttpContext` extension 37 | method `TrySetTenantInfo` can be used to manually set the current tenant, but normally the middleware handles this. 38 | * A custom implementation can be defined for advanced use cases. 39 | 40 | ## MultiTenant Strategies 41 | 42 | Responsible for determining and returning a tenant identifier string for the current request. 43 | 44 | * Several strategies are provided based on host, route, etc. See [MultiTenant Strategies](Strategies) for more 45 | information. 46 | * Custom strategies implementing `IMultiTenantStrategy` can be used as well. 47 | 48 | ## MultiTenant Stores 49 | 50 | Responsible for returning a `TenantInfo` object based on a tenant string identifier (which is usually provided by a 51 | strategy). 52 | 53 | * Has methods for adding, removing, updating, and retrieving `TenantInfo` objects. 54 | * Two implementations are provided: a basic `InMemoryTenantStore` based on `ConcurrentDictionary` 55 | and a more advanced Entity Framework Core based implementation. 56 | * Custom stores implementing `IMultiTenantStore` can be used as well. 57 | 58 | ## MultiTenantException 59 | 60 | Exception type thrown when a serious problem occurs within Finbuckle.MultiTenant. 61 | 62 | * Usually wraps an underlying exception. 63 | -------------------------------------------------------------------------------- /docs/Index.md: -------------------------------------------------------------------------------- 1 | [Introduction](Introduction) 2 | 3 | [What's New in v9.4.0](WhatsNew) 4 | 5 | [Version History](History) 6 | 7 | [Getting Started](GettingStarted) 8 | 9 | [Core Concepts](CoreConcepts) 10 | 11 | [Configuration and Usage](ConfigurationAndUsage) 12 | 13 | [MultiTenant Strategies](Strategies) 14 | 15 | [MultiTenant Stores](Stores) 16 | 17 | [Per-Tenant Options](Options) 18 | 19 | [Per-Tenant Authentication](Authentication) 20 | 21 | [Per-Tenant Data with EFCore](EFCore) 22 | 23 | [Per-Tenant Data with Identity](Identity) -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | # ![Finbuckle Logo](https://www.finbuckle.com/images/finbuckle-32x32-gh.png) Finbuckle.MultiTenant 9.4.0 2 | 3 | ## About Finbuckle.MultiTenant 4 | 5 | Finbuckle.MultiTenant is open source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant 6 | app behavior, and per-tenant data isolation. 7 | See [https://www.finbuckle.com/multitenant](https://www.finbuckle.com/multi-tenant) for more details and documentation. 8 | 9 | **This release supports .NET 9 and .NET 8.** 10 | 11 | Beginning with Finbuckle.MultiTenant 8.0.0 major version releases align with .NET major version releases and target all 12 | Microsoft supported major .NET versions at the time of initial release. 13 | 14 | New development focuses on the latest Finbuckle.MultiTenant release version while critical security and severe bug 15 | fixes will be released for prior versions which target .NET versions supported by Microsoft. 16 | 17 | In general, you should target the version of Finbuckle.MultiTenant that matches your .NET version. 18 | 19 | ## Open Source Support 20 | 21 | Your support helps keep the project going and is greatly appreciated! 22 | 23 | Finbuckle.MultiTenant is primarily supported by its [GitHub sponsors](https://github.com/sponsors/Finbuckle) and [contributors](https://github.com/Finbuckle/Finbuckle.MultiTenant/graphs/contributors). 24 | 25 | Additional support is provided by the following organizations: 26 | 27 |

28 | Digital Ocean logo 29 |

30 | 31 |

32 | GitHub logo 33 |

34 | 35 |

36 | Jetbrains logo 37 |

38 | 39 | ## License 40 | 41 | This project uses the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0). See the LICENSE file for 42 | license information. 43 | 44 | ## .NET Foundation 45 | 46 | This project is supported by the [.NET Foundation](https://dotnetfoundation.org). 47 | 48 | ## Code of Conduct 49 | 50 | This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our 51 | community. For more information see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct) 52 | or the CONTRIBUTING.md file. 53 | 54 | ## Community 55 | 56 | Check out the [GitHub repository](https://github.com/Finbuckle/Finbuckle.MultiTenant) to ask a question, make a request, 57 | or check out the code! 58 | 59 | ## Sample Projects 60 | 61 | A variety of sample projects are available in the [samples](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/main/samples) directory. -------------------------------------------------------------------------------- /docs/WhatsNew.md: -------------------------------------------------------------------------------- 1 | # What's New in v9.4.0 2 | 3 | > This page only lists release update details specific to v9.4.0. [Release update details for all releases are shown in the history page.](History) 4 | 5 | 6 | ## [9.4.0](https://github.com/Finbuckle/Finbuckle.MultiTenant/compare/v9.3.1...v9.4.0) (2025-09-14) 7 | 8 | ### Features 9 | 10 | * added EnforceMultiTenantOnTracking ([#1008](https://github.com/Finbuckle/Finbuckle.MultiTenant/issues/1008)) ([6ea8fd3](https://github.com/Finbuckle/Finbuckle.MultiTenant/commit/6ea8fd3ff85de36566b53c98d59fe998e821b116)) 11 | 12 | 13 | -------------------------------------------------------------------------------- /finbuckle-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Finbuckle/Finbuckle.MultiTenant/efb83a8e1c025f44ea9ead2eef1b531bb4663dbe/finbuckle-128x128.png -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This script is intended for use with semantic-release prepare step with 3 | # the exec plugin. 4 | 5 | while getopts v:r:f: flag 6 | do 7 | case "${flag}" in 8 | v) version=${OPTARG};; 9 | r) release_notes=${OPTARG};; 10 | f) feed_type=${OPTARG};; 11 | esac 12 | done 13 | 14 | # Update the Version property in Directory.Build.props: 15 | sed -E -i 's|.*|'"${version}"'|g' src/Directory.Build.props 16 | 17 | # Update the version in readme and docs files with elements: 18 | sed -E -i 's|.*|'"${version}"'|g' README.md docs/*.md 19 | 20 | # Set text to display whether the release is public feed or private feed: 21 | sed -E -i 's|.*|'"${feed_type}"'|g' README.md docs/*.md 22 | 23 | # Set text wherever release notes are needed in docs: 24 | perl -i -0777 -pe 's|.*|\n'"${release_notes}"'\n|s' docs/*.md 25 | 26 | # Set text for release notes in readme (header line stripped): 27 | release_notes_no_header=$(echo -e "${release_notes}" | tail -n +2) 28 | perl -i -0777 -pe 's|.*|\n'"${release_notes_no_header}"'\n|s' README.md 29 | 30 | # Copy the new changelog file to the Version History docs page: 31 | history=$(.*|\n'"${history}"'\n|s' docs/History.md -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Samples 2 | 3 | This folder contains simple examples of Finbuckle.MultiTenant and related packages in action. 4 | 5 | Samples are grouped by Finbuckle.MultiTenant version and .NET version. 6 | 7 | ## Where are the samples? 8 | 9 | Samples may not always be present in the main branch as they need to be updated to support later .NET versions. 10 | Contributions providing simple, up-to-date samples are always appreciated! 11 | 12 | The following tagged commits have legacy samples from their respective release: 13 | - [v8.0.0](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/v8.0.0/samples/) 14 | - [v6.13.1](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/v6.13.1/samples/) 15 | - [v6.2.0](https://github.com/Finbuckle/Finbuckle.MultiTenant/tree/v6.2.0/samples) -------------------------------------------------------------------------------- /samples/v9/net8/webapi/AppTenantInfo.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | 3 | public class AppTenantInfo : ITenantInfo 4 | { 5 | public string? Id { get; set; } 6 | public string? Identifier { get; set; } 7 | public string? Name { get; set; } 8 | public string? PreferredLanguage { get; set; } 9 | } -------------------------------------------------------------------------------- /samples/v9/net8/webapi/Program.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | // Learn more about configuring OpenAPI at https://aka.ms/aspnetcore/swashbuckle 7 | builder.Services.AddEndpointsApiExplorer(); 8 | builder.Services.AddSwaggerGen(); 9 | 10 | // Configure MultiTenant to use our AppTenantInfo class with the route strategy and in-memory store. 11 | var tenantList = BuildTenantList(); 12 | builder.Services.AddMultiTenant() 13 | .WithRouteStrategy() 14 | .WithInMemoryStore(options => 15 | { 16 | options.Tenants = tenantList; 17 | }); 18 | 19 | var app = builder.Build(); 20 | 21 | // Configure the HTTP request pipeline. 22 | if (app.Environment.IsDevelopment()) 23 | { 24 | app.UseSwagger(); 25 | app.UseSwaggerUI(); 26 | } 27 | 28 | app.UseHttpsRedirection(); 29 | 30 | // Add the MultiTenant middleware. 31 | app.UseMultiTenant(); 32 | 33 | // Define summaries in English, French, and German. 34 | var summaries = new Dictionary 35 | { 36 | ["en"] = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"], 37 | ["fr"] = ["Glacial", "Vivifiant", "Frais", "Froid", "Doux", "Chaud", "Clément", "Très chaud", "Accablant", "Brûlant"], 38 | ["de"] = ["Eiskalt", "Frisch", "Kühl", "Kalt", "Mild", "Warm", "Lau", "Heiß", "Schwül", "Glühend"] 39 | }; 40 | 41 | // Return weather with the summaries in the tenant's preferred language. 42 | app.MapGet("/{__tenant__}/weatherforecast", (string __tenant__, HttpContext http) => 43 | { 44 | // Note: __tenant__ parameter isn't used but required for Swashbuckle to work properly. 45 | 46 | // Get the MultiTenantContext instance. 47 | var mtc = http.GetMultiTenantContext(); 48 | 49 | // Set language to the tenant's preferred or default to english. 50 | var language = mtc.TenantInfo?.PreferredLanguage ?? "en"; 51 | 52 | var selectedSummaries = summaries[language]; 53 | var forecast = Enumerable.Range(1, 5).Select(index => 54 | new WeatherForecast 55 | ( 56 | DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 57 | Random.Shared.Next(-20, 55), 58 | selectedSummaries[Random.Shared.Next(selectedSummaries.Length)] 59 | )) 60 | .ToArray(); 61 | return forecast; 62 | }) 63 | .WithName("GetWeatherForecast") 64 | .WithOpenApi(); 65 | 66 | app.Run(); 67 | 68 | // Define tenants for the in-memory store. 69 | List BuildTenantList() => 70 | [ 71 | new AppTenantInfo 72 | { 73 | Id = "tenant-001", 74 | Identifier = "acme", 75 | Name = "Acme Corporation", 76 | PreferredLanguage = "en" 77 | }, 78 | 79 | new AppTenantInfo 80 | { 81 | Id = "tenant-002", 82 | Identifier = "globex", 83 | Name = "Globex GmbH", 84 | PreferredLanguage = "de" 85 | }, 86 | 87 | new AppTenantInfo 88 | { 89 | Id = "tenant-003", 90 | Identifier = "parisian", 91 | Name = "Parisian Foods", 92 | PreferredLanguage = "fr" 93 | } 94 | ]; 95 | 96 | record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) 97 | { 98 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 99 | } -------------------------------------------------------------------------------- /samples/v9/net8/webapi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:59077", 8 | "sslPort": 44309 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5136", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7090;http://localhost:5136", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /samples/v9/net8/webapi/README.md: -------------------------------------------------------------------------------- 1 | # webapi Sample 2 | 3 | This project demonstrates a simple multi-tenant webapi based on the default `dotnet new webapi` command in `.NET 8`. 4 | 5 | The default weather endpoint is modified to provide the summary in either English, French, or German based on the 6 | tenant's preferred language. 7 | 8 | Finbuckle.MultiTenant is configured to use the route strategy and an in-memory tenant store. A custom `AppTenantInfo` 9 | implements `ITenantInfo` and adds the `PreferredLanguage` property. Three example tenants are defined in the 10 | `BuildTenantList` method. -------------------------------------------------------------------------------- /samples/v9/net8/webapi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/v9/net8/webapi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /samples/v9/net8/webapi/webapi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/v9/net9/webapi/AppTenantInfo.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | 3 | public class AppTenantInfo : ITenantInfo 4 | { 5 | public string? Id { get; set; } 6 | public string? Identifier { get; set; } 7 | public string? Name { get; set; } 8 | public string? PreferredLanguage { get; set; } 9 | } -------------------------------------------------------------------------------- /samples/v9/net9/webapi/Program.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi 7 | builder.Services.AddOpenApi(); 8 | 9 | // Configure MultiTenant to use our AppTenantInfo class with the route strategy and in-memory store. 10 | var tenantList = BuildTenantList(); 11 | builder.Services.AddMultiTenant() 12 | .WithRouteStrategy() 13 | .WithInMemoryStore(options => 14 | { 15 | options.Tenants = tenantList; 16 | }); 17 | 18 | var app = builder.Build(); 19 | 20 | // Configure the HTTP request pipeline. 21 | if (app.Environment.IsDevelopment()) 22 | { 23 | app.MapOpenApi(); 24 | } 25 | 26 | app.UseHttpsRedirection(); 27 | 28 | // Add the MultiTenant middleware. 29 | app.UseMultiTenant(); 30 | 31 | // Define summaries in English, French, and German. 32 | var summaries = new Dictionary 33 | { 34 | ["en"] = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"], 35 | ["fr"] = ["Glacial", "Vivifiant", "Frais", "Froid", "Doux", "Chaud", "Clément", "Très chaud", "Accablant", "Brûlant"], 36 | ["de"] = ["Eiskalt", "Frisch", "Kühl", "Kalt", "Mild", "Warm", "Lau", "Heiß", "Schwül", "Glühend"] 37 | }; 38 | 39 | // Return weather with the summaries in the tenant's preferred language. 40 | app.MapGet("/{__tenant__}/weatherforecast", (string __tenant__, HttpContext http) => 41 | { 42 | // Note: __tenant__ parameter isn't used but required for OpenAPI to work properly. 43 | 44 | // Get the MultiTenantContext instance. 45 | var mtc = http.GetMultiTenantContext(); 46 | 47 | // Set language to the tenant's preferred or default to english. 48 | var language = mtc.TenantInfo?.PreferredLanguage ?? "en"; 49 | 50 | var selectedSummaries = summaries[language]; 51 | var forecast = Enumerable.Range(1, 5).Select(index => 52 | new WeatherForecast 53 | ( 54 | DateOnly.FromDateTime(DateTime.Now.AddDays(index)), 55 | Random.Shared.Next(-20, 55), 56 | selectedSummaries[Random.Shared.Next(selectedSummaries.Length)] 57 | )) 58 | .ToArray(); 59 | return forecast; 60 | }) 61 | .WithName("GetWeatherForecast"); 62 | 63 | app.Run(); 64 | 65 | // Define tenants for the in-memory store. 66 | List BuildTenantList() => 67 | [ 68 | new AppTenantInfo 69 | { 70 | Id = "tenant-001", 71 | Identifier = "acme", 72 | Name = "Acme Corporation", 73 | PreferredLanguage = "en" 74 | }, 75 | 76 | new AppTenantInfo 77 | { 78 | Id = "tenant-002", 79 | Identifier = "globex", 80 | Name = "Globex GmbH", 81 | PreferredLanguage = "de" 82 | }, 83 | 84 | new AppTenantInfo 85 | { 86 | Id = "tenant-003", 87 | Identifier = "parisian", 88 | Name = "Parisian Foods", 89 | PreferredLanguage = "fr" 90 | } 91 | ]; 92 | 93 | record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) 94 | { 95 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 96 | } -------------------------------------------------------------------------------- /samples/v9/net9/webapi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": false, 8 | "applicationUrl": "http://localhost:5271", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": false, 17 | "applicationUrl": "https://localhost:7175;http://localhost:5271", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/v9/net9/webapi/README.md: -------------------------------------------------------------------------------- 1 | # webapi Sample 2 | 3 | This project demonstrates a simple multi-tenant webapi based on the default `dotnet new webapi` command in `.NET 9`. 4 | 5 | The default weather endpoint is modified to provide the summary in either English, French, or German based on the 6 | tenant's preferred language. 7 | 8 | Finbuckle.MultiTenant is configured to use the route strategy and an in-memory tenant store. A custom `AppTenantInfo` 9 | implements `ITenantInfo` and adds the `PreferredLanguage` property. Three example tenants are defined in the 10 | `BuildTenantList` method. -------------------------------------------------------------------------------- /samples/v9/net9/webapi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /samples/v9/net9/webapi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /samples/v9/net9/webapi/webapi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9.4.0 4 | Finbuckle LLC 5 | Copyright Finbuckle LLC, Andrew White, and Contributors 6 | Apache-2.0 7 | https://www.finbuckle.com/MultiTenant 8 | https://github.com/Finbuckle/Finbuckle.MultiTenant 9 | true 10 | true 11 | snupkg 12 | finbuckle;multitenant;multitenancy;aspnet;aspnetcore;efcore 13 | finbuckle-128x128.png 14 | NUGET_README.md 15 | true 16 | true 17 | true 18 | 19 | enable 20 | true 21 | 22 | 23 | 24 | latest-Recommended 25 | CS1591 26 | $(NoWarn);CA1848 27 | $(NoWarn);CA2201 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Runtime.CompilerServices; 5 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.AspNetCore.Test")] -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Extensions/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.AspNetCore.Internal; 5 | using Microsoft.AspNetCore.Builder; 6 | 7 | //ReSharper disable once CheckNamespace 8 | namespace Finbuckle.MultiTenant; 9 | 10 | /// 11 | /// Extension methods for using Finbuckle.MultiTenant.AspNetCore. 12 | /// 13 | public static class FinbuckleMultiTenantApplicationBuilderExtensions 14 | { 15 | /// 16 | /// Use Finbuckle.MultiTenant middleware in processing the request. 17 | /// 18 | /// The IApplicationBuilder instance the extension method applies to. 19 | /// The same IApplicationBuilder passed into the method. 20 | public static IApplicationBuilder UseMultiTenant(this IApplicationBuilder builder) 21 | => builder.UseMiddleware(); 22 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Extensions/EndpointConventionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.AspNetCore.Routing; 2 | using Microsoft.AspNetCore.Builder; 3 | 4 | namespace Finbuckle.MultiTenant.AspNetCore.Extensions; 5 | 6 | public static class EndpointConventionBuilderExtensions 7 | { 8 | private static readonly ExcludeFromMultiTenantResolutionAttribute s_excludeFromMultiTenantResolutionAttribute = new(); 9 | 10 | /// 11 | /// Adds the to 12 | /// for all endpoints produced by the . 13 | /// 14 | /// The . 15 | /// A that can be used to further customize the endpoint. 16 | public static TBuilder ExcludeFromMultiTenantResolution(this TBuilder builder) where TBuilder : IEndpointConventionBuilder 17 | => builder.WithMetadata(s_excludeFromMultiTenantResolutionAttribute); 18 | 19 | /// 20 | /// Adds the to 21 | /// for all endpoints produced by the . 22 | /// 23 | /// The . 24 | /// A that can be used to further customize the endpoint. 25 | public static RouteHandlerBuilder ExcludeFromMultiTenantResolution(this RouteHandlerBuilder builder) 26 | => ExcludeFromMultiTenantResolution(builder); 27 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Extensions/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace Finbuckle.MultiTenant; 10 | 11 | /// 12 | /// Finbuckle.MultiTenant.AspNetCore extensions to HttpContext. 13 | /// 14 | public static class FinbuckleHttpContextExtensions 15 | { 16 | /// 17 | /// Returns the current MultiTenantContext. 18 | /// 19 | /// The HttpContext instance. 20 | /// The ITenantInfo implementation type. 21 | public static IMultiTenantContext GetMultiTenantContext(this HttpContext httpContext) 22 | where TTenantInfo : class, ITenantInfo, new() 23 | { 24 | if (httpContext.Items.TryGetValue(typeof(IMultiTenantContext), out var mtc) && mtc is not null) 25 | return (IMultiTenantContext)mtc; 26 | 27 | mtc = new MultiTenantContext(); 28 | httpContext.Items[typeof(IMultiTenantContext)] = mtc; 29 | 30 | return (IMultiTenantContext)mtc; 31 | } 32 | 33 | /// 34 | /// Returns the current generic TTenantInfo instance or null if there is none. 35 | /// 36 | /// The HttpContext instance. 37 | /// The ITenantInfo implementation type. 38 | public static TTenantInfo? GetTenantInfo(this HttpContext httpContext) 39 | where TTenantInfo : class, ITenantInfo, new() => 40 | httpContext.GetMultiTenantContext().TenantInfo; 41 | 42 | 43 | /// 44 | /// Sets the provided TenantInfo on the MultiTenantContext. 45 | /// Sets StrategyInfo and StoreInfo on the MultiTenant Context to null. 46 | /// Optionally resets the current dependency injection service provider. 47 | /// 48 | /// The HttpContext instance. 49 | /// The tenant info instance to set as current. 50 | /// Creates a new service provider scope if true. 51 | /// The ITenantInfo implementation type. 52 | public static void SetTenantInfo(this HttpContext httpContext, TTenantInfo tenantInfo, 53 | bool resetServiceProviderScope) 54 | where TTenantInfo : class, ITenantInfo, new() 55 | { 56 | if (resetServiceProviderScope) 57 | httpContext.RequestServices = httpContext.RequestServices.CreateScope().ServiceProvider; 58 | 59 | var multiTenantContext = new MultiTenantContext 60 | { 61 | TenantInfo = tenantInfo, 62 | StrategyInfo = null, 63 | StoreInfo = null 64 | }; 65 | 66 | var setter = httpContext.RequestServices.GetRequiredService(); 67 | setter.MultiTenantContext = multiTenantContext; 68 | 69 | httpContext.Items[typeof(IMultiTenantContext)] = multiTenantContext; 70 | } 71 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Finbuckle.MultiTenant.AspNetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | Finbuckle.MultiTenant.AspNetCore 5 | ASP.NET Core support for Finbuckle.MultiTenant. 6 | 7 | 8 | 9 | 10 | $(TargetFramework.Substring(3, 1)) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Security.Claims; 6 | using Finbuckle.MultiTenant.Abstractions; 7 | using Finbuckle.MultiTenant.Internal; 8 | using Microsoft.AspNetCore.Authentication; 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.Extensions.Options; 11 | 12 | namespace Finbuckle.MultiTenant.AspNetCore.Internal; 13 | 14 | [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] 15 | internal class MultiTenantAuthenticationService : IAuthenticationService 16 | where TTenantInfo : class, ITenantInfo, new() 17 | { 18 | private readonly IAuthenticationService _inner; 19 | private readonly IOptionsMonitor _multiTenantAuthenticationOptions; 20 | 21 | public MultiTenantAuthenticationService(IAuthenticationService inner, IOptionsMonitor multiTenantAuthenticationOptions) 22 | { 23 | this._inner = inner ?? throw new System.ArgumentNullException(nameof(inner)); 24 | this._multiTenantAuthenticationOptions = multiTenantAuthenticationOptions; 25 | } 26 | 27 | private static void AddTenantIdentifierToProperties(HttpContext context, ref AuthenticationProperties? properties) 28 | { 29 | // Add tenant identifier to the properties so on the callback we can use it to set the multitenant context. 30 | var multiTenantContext = context.GetMultiTenantContext(); 31 | if (multiTenantContext?.TenantInfo != null) 32 | { 33 | properties ??= new AuthenticationProperties(); 34 | if(!properties.Items.ContainsKey(Constants.TenantToken)) 35 | properties.Items.Add(Constants.TenantToken, multiTenantContext.TenantInfo.Identifier); 36 | } 37 | } 38 | 39 | public Task AuthenticateAsync(HttpContext context, string? scheme) 40 | => _inner.AuthenticateAsync(context, scheme); 41 | 42 | public async Task ChallengeAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) 43 | { 44 | if (_multiTenantAuthenticationOptions.CurrentValue.SkipChallengeIfTenantNotResolved) 45 | { 46 | if (context.GetMultiTenantContext()?.TenantInfo == null) 47 | return; 48 | } 49 | 50 | AddTenantIdentifierToProperties(context, ref properties); 51 | await _inner.ChallengeAsync(context, scheme, properties).ConfigureAwait(false); 52 | } 53 | 54 | public async Task ForbidAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) 55 | { 56 | AddTenantIdentifierToProperties(context, ref properties); 57 | await _inner.ForbidAsync(context, scheme, properties).ConfigureAwait(false); 58 | } 59 | 60 | public async Task SignInAsync(HttpContext context, string? scheme, ClaimsPrincipal principal, AuthenticationProperties? properties) 61 | { 62 | AddTenantIdentifierToProperties(context, ref properties); 63 | await _inner.SignInAsync(context, scheme, principal, properties).ConfigureAwait(false); 64 | } 65 | 66 | public async Task SignOutAsync(HttpContext context, string? scheme, AuthenticationProperties? properties) 67 | { 68 | AddTenantIdentifierToProperties(context, ref properties); 69 | await _inner.SignOutAsync(context, scheme, properties).ConfigureAwait(false); 70 | } 71 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Internal/MultiTenantMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Threading.Tasks; 5 | using Finbuckle.MultiTenant.Abstractions; 6 | using Finbuckle.MultiTenant.AspNetCore.Options; 7 | using Finbuckle.MultiTenant.AspNetCore.Routing; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Options; 11 | 12 | namespace Finbuckle.MultiTenant.AspNetCore.Internal; 13 | 14 | /// 15 | /// Middleware for resolving the MultiTenantContext and storing it in HttpContext. 16 | /// 17 | public class MultiTenantMiddleware 18 | { 19 | private readonly RequestDelegate next; 20 | private readonly ShortCircuitWhenOptions? options; 21 | 22 | public MultiTenantMiddleware(RequestDelegate next) 23 | { 24 | this.next = next; 25 | } 26 | 27 | public MultiTenantMiddleware(RequestDelegate next, IOptions options) 28 | { 29 | this.next = next; 30 | this.options = options.Value; 31 | } 32 | 33 | public async Task Invoke(HttpContext context) 34 | { 35 | if (context.GetEndpoint()?.Metadata.GetMetadata() is { ExcludeFromResolution: true }) 36 | { 37 | await next(context); 38 | return; 39 | } 40 | 41 | context.RequestServices.GetRequiredService(); 42 | var mtcSetter = context.RequestServices.GetRequiredService(); 43 | 44 | var resolver = context.RequestServices.GetRequiredService(); 45 | 46 | var multiTenantContext = await resolver.ResolveAsync(context).ConfigureAwait(false); 47 | mtcSetter.MultiTenantContext = multiTenantContext; 48 | context.Items[typeof(IMultiTenantContext)] = multiTenantContext; 49 | 50 | if (options?.Predicate is null || !options.Predicate(multiTenantContext)) 51 | await next(context); 52 | else if (options.RedirectTo is not null) 53 | context.Response.Redirect(options.RedirectTo.ToString()); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Options/BasePathStrategyOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.AspNetCore.Options; 5 | 6 | public class BasePathStrategyOptions 7 | { 8 | // TODO make this default to true in next major release 9 | public bool RebaseAspNetCorePathBase { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Options/MultiTenantAuthenticationOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.AspNetCore; 5 | 6 | public class MultiTenantAuthenticationOptions 7 | { 8 | public bool SkipChallengeIfTenantNotResolved { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Options/ShortCircuitWhenOptions.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | 3 | namespace Finbuckle.MultiTenant.AspNetCore.Options; 4 | 5 | public class ShortCircuitWhenOptions 6 | { 7 | private Func? _predicate; 8 | 9 | /// 10 | /// The callback that determines if the endpoint should be short circuited. 11 | /// 12 | public Func? Predicate 13 | { 14 | get 15 | { 16 | return _predicate; 17 | } 18 | set 19 | { 20 | ArgumentNullException.ThrowIfNull(value); 21 | 22 | _predicate = value; 23 | } 24 | } 25 | 26 | /// 27 | /// A to redirect the request to, if short circuited. 28 | /// 29 | public Uri? RedirectTo { get; set; } 30 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Routing/ExcludeFromMultiTenantResolutionAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Finbuckle.MultiTenant.AspNetCore.Routing; 4 | 5 | /// 6 | /// Indicates that this should be excluded from MultiTenant resolution. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = false, Inherited = true)] 9 | public class ExcludeFromMultiTenantResolutionAttribute : Attribute, IExcludeFromMultiTenantResolutionMetadata 10 | { 11 | /// 12 | public bool ExcludeFromResolution => true; 13 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Routing/IExcludeFromMultiTenantResolutionMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Finbuckle.MultiTenant.AspNetCore.Routing; 4 | 5 | /// 6 | /// Indicates whether MultiTenant resolution should occur for this . 7 | /// 8 | public interface IExcludeFromMultiTenantResolutionMetadata 9 | { 10 | /// 11 | /// Gets a value indicating whether MultiTenant resolution should 12 | /// occur for this . If , 13 | /// tenant resolution will not be executed. 14 | /// 15 | bool ExcludeFromResolution { get; } 16 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/BasePathStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 8 | 9 | public class BasePathStrategy : IMultiTenantStrategy 10 | { 11 | public Task GetIdentifierAsync(object context) 12 | { 13 | if (context is not HttpContext httpContext) 14 | return Task.FromResult(null); 15 | 16 | var path = httpContext.Request.Path; 17 | 18 | var pathSegments = 19 | path.Value?.Split('/', 2, StringSplitOptions.RemoveEmptyEntries); 20 | 21 | if (pathSegments is null || pathSegments.Length == 0) 22 | return Task.FromResult(null); 23 | 24 | string identifier = pathSegments[0]; 25 | 26 | return Task.FromResult(identifier); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/ClaimStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.Internal; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 11 | 12 | // ReSharper disable once ClassNeverInstantiated.Global 13 | public class ClaimStrategy : IMultiTenantStrategy 14 | { 15 | private readonly string _tenantKey; 16 | private readonly string? _authenticationScheme; 17 | 18 | public ClaimStrategy(string template) : this(template, null) 19 | { 20 | } 21 | 22 | public ClaimStrategy(string template, string? authenticationScheme) 23 | { 24 | ArgumentException.ThrowIfNullOrWhiteSpace(template); 25 | 26 | _tenantKey = template; 27 | _authenticationScheme = authenticationScheme; 28 | } 29 | 30 | public async Task GetIdentifierAsync(object context) 31 | { 32 | if (context is not HttpContext httpContext) 33 | return null; 34 | 35 | if (httpContext.User.Identity is { IsAuthenticated: true }) 36 | return httpContext.User.FindFirst(_tenantKey)?.Value; 37 | 38 | AuthenticationScheme? authScheme; 39 | var schemeProvider = httpContext.RequestServices.GetRequiredService(); 40 | if (_authenticationScheme is null) 41 | { 42 | authScheme = await schemeProvider.GetDefaultAuthenticateSchemeAsync().ConfigureAwait(false); 43 | } 44 | else 45 | { 46 | authScheme = 47 | (await schemeProvider.GetAllSchemesAsync().ConfigureAwait(false)).FirstOrDefault(x => x.Name == _authenticationScheme); 48 | } 49 | 50 | if (authScheme is null) 51 | { 52 | return null; 53 | } 54 | 55 | var handler = 56 | (IAuthenticationHandler)ActivatorUtilities.CreateInstance(httpContext.RequestServices, 57 | authScheme.HandlerType); 58 | await handler.InitializeAsync(authScheme, httpContext).ConfigureAwait(false); 59 | httpContext.Items[$"{Constants.TenantToken}__bypass_validate_principal__"] = "true"; // Value doesn't matter. 60 | var handlerResult = await handler.AuthenticateAsync().ConfigureAwait(false); 61 | httpContext.Items.Remove($"{Constants.TenantToken}__bypass_validate_principal__"); 62 | 63 | var identifier = handlerResult.Principal?.FindFirst(_tenantKey)?.Value; 64 | return identifier; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/HeaderStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using Finbuckle.MultiTenant.Abstractions; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 9 | 10 | [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] 11 | public class HeaderStrategy : IMultiTenantStrategy 12 | { 13 | private readonly string _headerKey; 14 | public HeaderStrategy(string headerKey) 15 | { 16 | _headerKey = headerKey; 17 | } 18 | 19 | public Task GetIdentifierAsync(object context) 20 | { 21 | if (context is not HttpContext httpContext) 22 | return Task.FromResult(null); 23 | 24 | return Task.FromResult(httpContext?.Request.Headers[_headerKey].FirstOrDefault()); 25 | } 26 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/HostStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Text.RegularExpressions; 5 | using Finbuckle.MultiTenant.Abstractions; 6 | using Finbuckle.MultiTenant.Internal; 7 | using Microsoft.AspNetCore.Http; 8 | 9 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 10 | 11 | public sealed class HostStrategy : IMultiTenantStrategy 12 | { 13 | private readonly Regex regex; 14 | 15 | public HostStrategy(string template) 16 | { 17 | // match whole domain if just "__tenant__". 18 | if (template == Constants.TenantToken) 19 | { 20 | template = template.Replace(Constants.TenantToken, "(?.+)"); 21 | } 22 | else 23 | { 24 | // Check for valid template. 25 | // Template cannot be null or whitespace. 26 | if (string.IsNullOrWhiteSpace(template)) 27 | { 28 | throw new MultiTenantException("Template cannot be null or whitespace."); 29 | } 30 | 31 | // Wildcard "*" must be only occur once in template. 32 | if (Regex.Match(template, @"\*(?=.*\*)").Success) 33 | { 34 | throw new MultiTenantException("Wildcard \"*\" must be only occur once in template."); 35 | } 36 | 37 | // Wildcard "*" must be only token in template segment. 38 | if (Regex.Match(template, @"\*[^\.]|[^\.]\*").Success) 39 | { 40 | throw new MultiTenantException("\"*\" wildcard must be only token in template segment."); 41 | } 42 | 43 | // Wildcard "?" must be only token in template segment. 44 | if (Regex.Match(template, @"\?[^\.]|[^\.]\?").Success) 45 | { 46 | throw new MultiTenantException("\"?\" wildcard must be only token in template segment."); 47 | } 48 | 49 | template = template.Trim().Replace(".", @"\."); 50 | string wildcardSegmentsPattern = @"(\.[^\.]+)*"; 51 | string singleSegmentPattern = @"[^\.]+"; 52 | if (template.Substring(template.Length - 3, 3) == @"\.*") 53 | { 54 | template = string.Concat(template.AsSpan(0, template.Length - 3), wildcardSegmentsPattern); 55 | } 56 | 57 | wildcardSegmentsPattern = @"([^\.]+\.)*"; 58 | template = template.Replace(@"*\.", wildcardSegmentsPattern); 59 | template = template.Replace("?", singleSegmentPattern); 60 | template = template.Replace(Constants.TenantToken, @"(?[^\.]+)"); 61 | } 62 | 63 | this.regex = new Regex($"^{template}$", RegexOptions.ExplicitCapture | RegexOptions.Compiled, TimeSpan.FromMilliseconds(100)); 64 | } 65 | 66 | public Task GetIdentifierAsync(object context) 67 | { 68 | if (context is not HttpContext httpContext) 69 | return Task.FromResult(null); 70 | 71 | var host = httpContext.Request.Host; 72 | 73 | if (host.HasValue == false) 74 | return Task.FromResult(null); 75 | 76 | string? identifier = null; 77 | 78 | var match = regex.Match(host.Host); 79 | 80 | if (match.Success) 81 | { 82 | identifier = match.Groups["identifier"].Value; 83 | } 84 | 85 | return Task.FromResult(identifier); 86 | } 87 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/RouteStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 8 | 9 | public class RouteStrategy : IMultiTenantStrategy 10 | { 11 | internal readonly string TenantParam; 12 | 13 | public RouteStrategy(string tenantParam) 14 | { 15 | if (string.IsNullOrWhiteSpace(tenantParam)) 16 | { 17 | throw new ArgumentException($"\"{nameof(tenantParam)}\" must not be null or whitespace", nameof(tenantParam)); 18 | } 19 | 20 | this.TenantParam = tenantParam; 21 | } 22 | 23 | public Task GetIdentifierAsync(object context) 24 | { 25 | 26 | if (context is not HttpContext httpContext) 27 | return Task.FromResult(null); 28 | 29 | httpContext.Request.RouteValues.TryGetValue(TenantParam, out var identifier); 30 | 31 | return Task.FromResult(identifier as string); 32 | } 33 | } 34 | 35 | // #endif -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.AspNetCore/Strategies/SessionStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Finbuckle.MultiTenant.AspNetCore.Strategies; 8 | 9 | public class SessionStrategy : IMultiTenantStrategy 10 | { 11 | private readonly string tenantKey; 12 | 13 | public SessionStrategy(string tenantKey) 14 | { 15 | if (string.IsNullOrWhiteSpace(tenantKey)) 16 | { 17 | throw new ArgumentException("message", nameof(tenantKey)); 18 | } 19 | 20 | this.tenantKey = tenantKey; 21 | } 22 | 23 | public Task GetIdentifierAsync(object context) 24 | { 25 | if (context is not HttpContext httpContext) 26 | return Task.FromResult(null); 27 | 28 | var identifier = httpContext.Session.GetString(tenantKey); 29 | return Task.FromResult(identifier); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Runtime.CompilerServices; 5 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.EntityFrameworkCore.Test")] -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.EntityFrameworkCore; 5 | 6 | /// 7 | /// Contains constant values for Finbuckle.MultiTenant.EntityFrameworkCore. 8 | /// 9 | public static class Constants 10 | { 11 | public static readonly string MultiTenantAnnotationName = "Finbuckle:MultiTenant"; 12 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Enums.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.EntityFrameworkCore; 5 | 6 | /// 7 | /// Determines how entities where TenantId does not match the TenantContext are handled 8 | /// when SaveChanges or SaveChangesAsync is called. 9 | /// 10 | public enum TenantMismatchMode 11 | { 12 | Throw, 13 | Ignore, 14 | Overwrite 15 | } 16 | 17 | /// 18 | /// Determines how entities with null TenantId are handled 19 | /// when SaveChanges or SaveChangesAsync is called. 20 | /// 21 | public enum TenantNotSetMode 22 | { 23 | Throw, 24 | Overwrite 25 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/EntityTypeBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 6 | using System.Linq.Expressions; 7 | using Finbuckle.MultiTenant.EntityFrameworkCore; 8 | 9 | // ReSharper disable once CheckNamespace 10 | namespace Finbuckle.MultiTenant; 11 | 12 | public static class EntityTypeBuilderExtensions 13 | { 14 | private class ExpressionVariableScope 15 | { 16 | public IMultiTenantDbContext? Context { get; } 17 | } 18 | 19 | private static LambdaExpression? GetQueryFilter(this EntityTypeBuilder builder) 20 | { 21 | return builder.Metadata.GetQueryFilter(); 22 | } 23 | 24 | /// 25 | /// Adds MultiTenant support for an entity. Call after 26 | /// to merge query filters. 27 | /// 28 | /// The typed EntityTypeBuilder instance. 29 | /// A MultiTenantEntityTypeBuilder instance. 30 | public static MultiTenantEntityTypeBuilder IsMultiTenant(this EntityTypeBuilder builder) 31 | { 32 | if (builder.Metadata.IsMultiTenant()) 33 | return new MultiTenantEntityTypeBuilder(builder); 34 | 35 | builder.HasAnnotation(Constants.MultiTenantAnnotationName, true); 36 | 37 | try 38 | { 39 | builder.Property("TenantId") 40 | .IsRequired() 41 | .HasMaxLength(Internal.Constants.TenantIdMaxLength); 42 | } 43 | catch (Exception ex) 44 | { 45 | throw new MultiTenantException($"{builder.Metadata.ClrType} unable to add TenantId property", ex); 46 | } 47 | 48 | // build expression tree for e => EF.Property(e, "TenantId") == TenantInfo.Id 49 | 50 | // where e is one of our entity types 51 | // will need this ParameterExpression for next step and for final step 52 | var entityParamExp = Expression.Parameter(builder.Metadata.ClrType, "e"); 53 | 54 | var existingQueryFilter = builder.GetQueryFilter(); 55 | 56 | // override to match existing query parameter if applicable 57 | if (existingQueryFilter != null) 58 | { 59 | entityParamExp = existingQueryFilter.Parameters.First(); 60 | } 61 | 62 | // build up expression tree for: EF.Property(e, "TenantId") 63 | var tenantIdExp = Expression.Constant("TenantId", typeof(string)); 64 | var efPropertyExp = Expression.Call(typeof(EF), nameof(EF.Property), new[] { typeof(string) }, entityParamExp, tenantIdExp); 65 | var leftExp = efPropertyExp; 66 | 67 | // build up express tree for: TenantInfo.Id 68 | // EF will magically sub the current db context in for scope.Context 69 | var scopeConstantExp = Expression.Constant(new ExpressionVariableScope()); 70 | var contextMemberInfo = typeof(ExpressionVariableScope).GetMember(nameof(ExpressionVariableScope.Context))[0]; 71 | var contextMemberAccessExp = Expression.MakeMemberAccess(scopeConstantExp, contextMemberInfo); 72 | var contextTenantInfoExp = Expression.Property(contextMemberAccessExp, nameof(IMultiTenantDbContext.TenantInfo)); 73 | var rightExp = Expression.Property(contextTenantInfoExp, nameof(IMultiTenantDbContext.TenantInfo.Id)); 74 | 75 | // build expression tree for EF.Property(e, "TenantId") == TenantInfo.Id' 76 | var predicate = Expression.Equal(leftExp, rightExp); 77 | 78 | // combine with existing filter 79 | if (existingQueryFilter != null) 80 | { 81 | predicate = Expression.AndAlso(existingQueryFilter.Body, predicate); 82 | } 83 | 84 | // build the final expression tree 85 | var delegateType = Expression.GetDelegateType(builder.Metadata.ClrType, typeof(bool)); 86 | var lambdaExp = Expression.Lambda(delegateType, predicate, entityParamExp); 87 | 88 | // set the filter 89 | builder.HasQueryFilter(lambdaExp); 90 | 91 | return new MultiTenantEntityTypeBuilder(builder); 92 | } 93 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/EntityTypeExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Finbuckle.MultiTenant; 9 | 10 | public static class EntityTypeExtensions 11 | { 12 | /// 13 | /// Whether or not the is configured as MultiTenant. 14 | /// 15 | /// The entity type to test for MultiTenant configuration. 16 | /// Returns true if the entity type has MultiTenant configuration, false if not. 17 | public static bool IsMultiTenant(this IMutableEntityType? entityType) 18 | { 19 | while (entityType != null) 20 | { 21 | var hasMultiTenantAnnotation = (bool?) entityType.FindAnnotation(Constants.MultiTenantAnnotationName)?.Value ?? false; 22 | if (hasMultiTenantAnnotation) 23 | return true; 24 | entityType = entityType.BaseType; 25 | } 26 | 27 | return false; 28 | } 29 | 30 | public static bool IsMultiTenant(this IEntityType? entityType) 31 | { 32 | while (entityType != null) 33 | { 34 | var hasMultiTenantAnnotation = (bool?) entityType.FindAnnotation(Constants.MultiTenantAnnotationName)?.Value ?? false; 35 | if (hasMultiTenantAnnotation) 36 | return true; 37 | entityType = entityType.BaseType; 38 | } 39 | 40 | return false; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/ModelBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Finbuckle.MultiTenant; 9 | 10 | public static class FinbuckleModelBuilderExtensions 11 | { 12 | /// 13 | /// Configures any entity's with the [MultiTenant] attribute. 14 | /// 15 | public static ModelBuilder ConfigureMultiTenant(this ModelBuilder modelBuilder) 16 | { 17 | // Call IsMultiTenant() to configure the types marked with the MultiTenant Data Attribute 18 | foreach (var clrType in modelBuilder.Model.GetEntityTypes() 19 | .Where(et => et.ClrType.HasMultiTenantAttribute()) 20 | .Select(et => et.ClrType)) 21 | { 22 | modelBuilder.Entity(clrType) 23 | .IsMultiTenant(); 24 | } 25 | 26 | return modelBuilder; 27 | } 28 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/ModelExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | 6 | // ReSharper disable once CheckNamespace 7 | namespace Finbuckle.MultiTenant; 8 | 9 | public static class ModelExtensions 10 | { 11 | /// 12 | /// Gets all MultiTenant entity types defined in the model. 13 | /// 14 | /// the model from which to list entities. 15 | /// MultiTenant entity types. 16 | public static IEnumerable GetMultiTenantEntityTypes(this IModel model) 17 | { 18 | return model.GetEntityTypes().Where(et => et.IsMultiTenant()); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/MultiTenantBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.EntityFrameworkCore.Stores.EFCoreStore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | // ReSharper disable once CheckNamespace 9 | namespace Finbuckle.MultiTenant; 10 | 11 | /// 12 | /// Provides builder methods for Finbuckle.MultiTenant services and configuration. 13 | /// 14 | public static class MultiTenantBuilderExtensions 15 | { 16 | /// 17 | /// Adds an EFCore based multitenant store to the application. Will also add the database context service unless it is already added. 18 | /// 19 | /// The same MultiTenantBuilder passed into the method. 20 | // ReSharper disable once InconsistentNaming 21 | public static MultiTenantBuilder WithEFCoreStore(this MultiTenantBuilder builder) 22 | where TEFCoreStoreDbContext : EFCoreStoreDbContext 23 | where TTenantInfo : class, ITenantInfo, new() 24 | { 25 | builder.Services.AddDbContext(); // Note, will not override existing context if already added. 26 | return builder.WithStore>(ServiceLifetime.Scoped); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Extensions/MultiTenantEntityTypeBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace Finbuckle.MultiTenant; 9 | 10 | public static class MultiTenantEntityTypeBuilderExtensions 11 | { 12 | /// 13 | /// Adds TenantId to all unique indexes. 14 | /// 15 | /// Thet MultiTenantEntityTypeBuilder instance. 16 | /// The MultiTenantEntityTypeBuilder instance. 17 | public static MultiTenantEntityTypeBuilder AdjustUniqueIndexes(this MultiTenantEntityTypeBuilder builder) 18 | { 19 | // Update any unique constraints to include TenantId (unless they already do) 20 | var indexes = builder.Builder.Metadata.GetIndexes() 21 | .Where(i => i.IsUnique) 22 | .Where(i => !i.Properties.Select(p => p.Name).Contains("TenantId")) 23 | .ToList(); 24 | 25 | foreach (var index in indexes.ToArray()) 26 | { 27 | builder.AdjustIndex(index); 28 | } 29 | 30 | return builder; 31 | } 32 | 33 | /// 34 | /// Adds TenantId to all indexes. 35 | /// 36 | /// Thet MultiTenantEntityTypeBuilder instance. 37 | /// The MultiTenantEntityTypeBuilder instance. 38 | public static MultiTenantEntityTypeBuilder AdjustIndexes(this MultiTenantEntityTypeBuilder builder) 39 | { 40 | // Update any unique constraints to include TenantId (unless they already do) 41 | var indexes = builder.Builder.Metadata.GetIndexes() 42 | .Where(i => !i.Properties.Select(p => p.Name).Contains("TenantId")) 43 | .ToList(); 44 | 45 | foreach (var index in indexes.ToArray()) 46 | { 47 | builder.AdjustIndex(index); 48 | } 49 | 50 | return builder; 51 | } 52 | 53 | /// 54 | /// Adds TenantId to the primary and alternate keys and adds the TenantId property to any dependent types' foreign keys. 55 | /// 56 | /// Thet MultiTenantEntityTypeBuilder instance. 57 | /// The modelBuilder for the database ontext. 58 | /// The MultiTenantEntityTypeBuilder instance. 59 | internal static MultiTenantEntityTypeBuilder AdjustKeys(this MultiTenantEntityTypeBuilder builder, ModelBuilder modelBuilder) 60 | { 61 | var keys = builder.Builder.Metadata.GetKeys(); 62 | foreach (var key in keys.ToArray()) 63 | { 64 | builder.AdjustKey(key, modelBuilder); 65 | } 66 | 67 | return builder; 68 | } 69 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Finbuckle.MultiTenant.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | Finbuckle.MultiTenant.EntityFrameworkCore 5 | Entity Framework Core support for Finbuckle.MultiTenant. 6 | 7 | 8 | 9 | 10 | $(TargetFramework.Substring(3, 1)) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/IMultiTenantDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore; 7 | 8 | public interface IMultiTenantDbContext 9 | { 10 | ITenantInfo? TenantInfo { get; } 11 | TenantMismatchMode TenantMismatchMode { get; } 12 | TenantNotSetMode TenantNotSetMode { get; } 13 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/MultiTenantEntityTypeBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Collections.Immutable; 5 | using System.Diagnostics.CodeAnalysis; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Metadata; 8 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 9 | 10 | namespace Finbuckle.MultiTenant.EntityFrameworkCore; 11 | 12 | [SuppressMessage("ReSharper", "UnusedMethodReturnValue.Global")] 13 | public class MultiTenantEntityTypeBuilder 14 | { 15 | public EntityTypeBuilder Builder { get; } 16 | 17 | public MultiTenantEntityTypeBuilder(EntityTypeBuilder builder) 18 | { 19 | Builder = builder; 20 | } 21 | 22 | /// 23 | /// Adds TenantId to the index. 24 | /// 25 | /// The index to adjust for TenantId. 26 | /// The MultiTenantEntityTypeBuilder instance. 27 | public MultiTenantEntityTypeBuilder AdjustIndex(IMutableIndex index) 28 | { 29 | // set the new unique index with TenantId preserving name and database name 30 | IndexBuilder indexBuilder; 31 | Builder.Metadata.RemoveIndex(index); 32 | if (index.Name != null) 33 | indexBuilder = Builder 34 | .HasIndex(index.Properties.Select(p => p.Name).Append("TenantId").ToArray(), index.Name) 35 | .HasDatabaseName(index.GetDatabaseName()); 36 | else 37 | indexBuilder = Builder.HasIndex(index.Properties.Select(p => p.Name).Append("TenantId").ToArray()) 38 | .HasDatabaseName(index.GetDatabaseName()); 39 | 40 | if (index.IsUnique) 41 | indexBuilder.IsUnique(); 42 | 43 | if (index.GetFilter() is string filter) 44 | { 45 | indexBuilder.HasFilter(filter); 46 | } 47 | 48 | foreach (var annotation in index.GetAnnotations()) 49 | { 50 | indexBuilder.HasAnnotation(annotation.Name, annotation.Value); 51 | } 52 | 53 | return this; 54 | } 55 | 56 | /// 57 | /// Adds TenantId to the key and adds the TenantId property to any dependent types' foreign keys. 58 | /// 59 | /// The key to adjust for TenantId. 60 | /// The modelBuilder for the DbContext. 61 | /// The MultiTenantEntityTypeBuilder<T> instance. 62 | public MultiTenantEntityTypeBuilder AdjustKey(IMutableKey key, ModelBuilder modelBuilder) 63 | { 64 | var prop = Builder.Metadata.GetProperty("TenantId"); 65 | var props = key.Properties.Append(prop).ToImmutableList(); 66 | var foreignKeys = key.GetReferencingForeignKeys().ToArray(); 67 | var annotations = key.GetAnnotations(); 68 | var newKey = key.IsPrimaryKey() ? Builder.Metadata.SetPrimaryKey(props) : Builder.Metadata.AddKey(props); 69 | 70 | foreach (var fk in foreignKeys) 71 | { 72 | var fkEntityBuilder = modelBuilder.Entity(fk.DeclaringEntityType.ClrType); 73 | var newFkProp = fkEntityBuilder.Property("TenantId").Metadata; 74 | var fkProps = fk.Properties.Append(newFkProp).ToImmutableList(); 75 | fk.SetProperties(fkProps, newKey!); 76 | } 77 | 78 | foreach (var annotation in annotations) 79 | { 80 | if (newKey?.FindAnnotation(annotation.Name) is null) 81 | { 82 | newKey?.AddAnnotation(annotation.Name, annotation.Value); 83 | } 84 | } 85 | 86 | // remove key 87 | Builder.Metadata.RemoveKey(key); 88 | 89 | return this; 90 | } 91 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Stores/EFCoreStore/EFCoreStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Stores.EFCoreStore; 8 | 9 | public class EFCoreStore : IMultiTenantStore 10 | where TEFCoreStoreDbContext : EFCoreStoreDbContext 11 | where TTenantInfo : class, ITenantInfo, new() 12 | { 13 | internal readonly TEFCoreStoreDbContext dbContext; 14 | 15 | public EFCoreStore(TEFCoreStoreDbContext dbContext) 16 | { 17 | this.dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); 18 | } 19 | 20 | public virtual async Task TryGetAsync(string id) 21 | { 22 | return await dbContext.TenantInfo.AsNoTracking() 23 | .Where(ti => ti.Id == id) 24 | .SingleOrDefaultAsync().ConfigureAwait(false); 25 | } 26 | 27 | public virtual async Task> GetAllAsync() 28 | { 29 | return await dbContext.TenantInfo.AsNoTracking().ToListAsync().ConfigureAwait(false); 30 | } 31 | 32 | public virtual async Task> GetAllAsync(int take, int skip) 33 | { 34 | return await dbContext.TenantInfo.Take(take).Skip(skip).AsNoTracking().ToListAsync().ConfigureAwait(false); 35 | } 36 | 37 | public virtual async Task TryGetByIdentifierAsync(string identifier) 38 | { 39 | return await dbContext.TenantInfo.AsNoTracking() 40 | .Where(ti => ti.Identifier == identifier) 41 | .SingleOrDefaultAsync().ConfigureAwait(false); 42 | } 43 | 44 | public virtual async Task TryAddAsync(TTenantInfo tenantInfo) 45 | { 46 | await dbContext.TenantInfo.AddAsync(tenantInfo).ConfigureAwait(false); 47 | var result = await dbContext.SaveChangesAsync().ConfigureAwait(false) > 0; 48 | dbContext.Entry(tenantInfo).State = EntityState.Detached; 49 | 50 | return result; 51 | } 52 | 53 | public virtual async Task TryRemoveAsync(string identifier) 54 | { 55 | var existing = await dbContext.TenantInfo 56 | .Where(ti => ti.Identifier == identifier) 57 | .SingleOrDefaultAsync().ConfigureAwait(false); 58 | 59 | if (existing is null) 60 | { 61 | return false; 62 | } 63 | 64 | dbContext.TenantInfo.Remove(existing); 65 | return await dbContext.SaveChangesAsync().ConfigureAwait(false) > 0; 66 | } 67 | 68 | public virtual async Task TryUpdateAsync(TTenantInfo tenantInfo) 69 | { 70 | dbContext.TenantInfo.Update(tenantInfo); 71 | var result = await dbContext.SaveChangesAsync().ConfigureAwait(false) > 0; 72 | dbContext.Entry(tenantInfo).State = EntityState.Detached; 73 | return result; 74 | } 75 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant.EntityFrameworkCore/Stores/EFCoreStore/EFCoreStoreDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Stores.EFCoreStore; 8 | 9 | public class EFCoreStoreDbContext : DbContext 10 | where TTenantInfo : class, ITenantInfo, new() 11 | { 12 | public EFCoreStoreDbContext(DbContextOptions options) : base(options) 13 | { 14 | } 15 | 16 | public DbSet TenantInfo => Set(); 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.Entity().HasKey(ti => ti.Id); 21 | modelBuilder.Entity().Property(ti => ti.Id).HasMaxLength(Internal.Constants.TenantIdMaxLength); 22 | modelBuilder.Entity().HasIndex(ti => ti.Identifier).IsUnique(); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Non-generic interface for the MultiTenantContext. 8 | /// 9 | public interface IMultiTenantContext 10 | { 11 | /// 12 | /// Information about the tenant for this context. 13 | /// 14 | ITenantInfo? TenantInfo { get; } 15 | 16 | /// 17 | /// True if a tenant has been resolved and TenantInfo is not null. 18 | /// 19 | bool IsResolved { get; } 20 | 21 | /// 22 | /// Information about the MultiTenant strategies for this context. 23 | /// 24 | StrategyInfo? StrategyInfo { get; } 25 | } 26 | 27 | 28 | 29 | /// 30 | /// Generic interface for the multi-tenant context. 31 | /// 32 | /// The ITenantInfo implementation type. 33 | public interface IMultiTenantContext : IMultiTenantContext 34 | where TTenantInfo : class, ITenantInfo, new() 35 | { 36 | /// 37 | /// Information about the tenant for this context. 38 | /// 39 | new TTenantInfo? TenantInfo { get; } 40 | 41 | /// 42 | /// Information about the MultiTenant stores for this context. 43 | /// 44 | StoreInfo? StoreInfo { get; set; } 45 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContextAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Provides access the current MultiTenantContext. 8 | /// 9 | public interface IMultiTenantContextAccessor 10 | { 11 | /// 12 | /// Gets the current MultiTenantContext. 13 | /// 14 | IMultiTenantContext MultiTenantContext { get; } 15 | } 16 | 17 | /// 18 | /// Provides access the current MultiTenantContext. 19 | /// 20 | /// The ITenantInfo implementation type. 21 | public interface IMultiTenantContextAccessor : IMultiTenantContextAccessor 22 | where TTenantInfo : class, ITenantInfo, new() 23 | { 24 | /// 25 | /// Gets the current MultiTenantContext. 26 | /// 27 | new IMultiTenantContext MultiTenantContext { get; } 28 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/IMultiTenantContextSetter.cs: -------------------------------------------------------------------------------- 1 | namespace Finbuckle.MultiTenant.Abstractions; 2 | 3 | /// 4 | /// Interface used to set the MultiTenantContext. This is an implementation detail and not intended for general use. 5 | /// 6 | public interface IMultiTenantContextSetter 7 | { 8 | IMultiTenantContext MultiTenantContext { set; } 9 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Interface definition for tenant stores. 8 | /// 9 | /// The ITenantInfo implementation type. 10 | public interface IMultiTenantStore where TTenantInfo : class, ITenantInfo, new() 11 | { 12 | /// 13 | /// Try to add the TTenantInfo to the store. 14 | /// 15 | /// New TTenantInfo instance to add. 16 | /// True if successfully added 17 | Task TryAddAsync(TTenantInfo tenantInfo); 18 | 19 | /// 20 | /// Try to update the TTenantInfo in the store. 21 | /// 22 | /// Existing TTenantInfo instance to update. 23 | /// True if successfully updated. 24 | Task TryUpdateAsync(TTenantInfo tenantInfo); 25 | 26 | /// 27 | /// Try to remove the TTenantInfo from the store. 28 | /// 29 | /// Identifier for the tenant to remove. 30 | /// True if successfully removed. 31 | Task TryRemoveAsync(string identifier); 32 | 33 | /// 34 | /// Retrieve the TTenantInfo for a given identifier. 35 | /// 36 | /// Identifier for the tenant to retrieve. 37 | /// The found TTenantInfo instance or null if none found. 38 | /// TODO make obsolete 39 | Task TryGetByIdentifierAsync(string identifier); 40 | 41 | /// 42 | /// Retrieve the TTenantInfo for a given tenant Id. 43 | /// 44 | /// TenantId for the tenant to retrieve. 45 | /// The found TTenantInfo instance or null if none found. 46 | /// TODO make obsolete 47 | Task TryGetAsync(string id); 48 | 49 | 50 | /// 51 | /// Retrieve all the TTenantInfo's from the store. 52 | /// 53 | /// An IEnumerable of all tenants in the store. 54 | Task> GetAllAsync(); 55 | 56 | /// 57 | /// Retrieve all the TTenantInfo's from the store. 58 | /// 59 | /// Number of elements to take from the list. 60 | /// Number of elements to skip from the list. 61 | /// 62 | Task> GetAllAsync(int take, int skip); 63 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/IMultiTenantStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Determines the tenant identifier. 8 | /// 9 | public interface IMultiTenantStrategy 10 | { 11 | /// 12 | /// Method for implementations to control how the identifier is determined. 13 | /// 14 | /// The context object used to determine an identifier. 15 | /// The found identifier or null. 16 | Task GetIdentifierAsync(object context); 17 | 18 | /// 19 | /// Strategy execution order priority. Low values are executed first. Equal values are executed in order of registration. 20 | /// 21 | int Priority => 0; 22 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/ITenantInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Interface for basic tenant information. 8 | /// 9 | public interface ITenantInfo 10 | { 11 | 12 | /// 13 | /// Gets or sets a unique id for the tenant. 14 | /// 15 | /// 16 | /// Unlike the Identifier, the id is never intended to be changed. 17 | /// 18 | string? Id { get; set; } 19 | 20 | /// 21 | /// Gets or sets a unique identifier for the tenant. 22 | /// 23 | /// 24 | /// The Identifier is intended for use during tenant resolution and format is determined by convention. For example 25 | /// a web based strategy may require URL friendly identifiers. Identifiers can be changed if needed. 26 | /// 27 | string? Identifier { get; set; } 28 | 29 | /// 30 | /// Gets or sets a display friendly name for the tenant. 31 | /// 32 | string? Name { get; set; } 33 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Abstractions/ITenantResolver.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Abstractions; 5 | 6 | /// 7 | /// Resolves the current tenant. 8 | /// 9 | public interface ITenantResolver 10 | { 11 | /// 12 | /// Performs tenant resolution within the given context. 13 | /// 14 | /// The context for tenant resolution. 15 | /// The MultiTenantContext. 16 | Task ResolveAsync(object context); 17 | 18 | /// 19 | /// Contains a list of MultiTenant strategies used for tenant resolution. 20 | /// 21 | public IEnumerable Strategies { get; set; } 22 | } 23 | 24 | /// 25 | /// Resolves the current tenant. 26 | /// 27 | /// The ITenantInfo implementation type. 28 | public interface ITenantResolver : ITenantResolver 29 | where TTenantInfo : class, ITenantInfo, new() 30 | { 31 | /// 32 | /// Performs tenant resolution within the given context. 33 | /// 34 | /// The context for tenant resolution. 35 | /// The MultiTenantContext. 36 | new Task> ResolveAsync(object context); 37 | 38 | /// 39 | /// Contains a list of MultiTenant stores used for tenant resolution. 40 | /// 41 | public IEnumerable> Stores { get; set; } 42 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Runtime.CompilerServices; 5 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.Test")] 6 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.AspNetCore")] 7 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.AspNetCore.Test")] 8 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.EntityFrameworkCore")] 9 | [assembly:InternalsVisibleTo("Finbuckle.MultiTenant.EntityFrameworkCore.Test")] -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Events/MultiTenantEvents.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Events; 7 | 8 | /// 9 | /// Events for successful and failed tenant resolution. 10 | /// 11 | /// The ITenantInfo implementation type. 12 | public class MultiTenantEvents 13 | where TTenantInfo : class, ITenantInfo, new() 14 | { 15 | /// 16 | /// Called after each MultiTenantStrategy has run. The resulting identifier can be modified if desired or set to null to advance to the next strategy. 17 | /// 18 | public Func OnStrategyResolveCompleted { get; set; } = context => Task.CompletedTask; 19 | 20 | /// 21 | /// Called after each MultiTenantStore has attempted to find the tenant identifier. The resulting TenantInfo can be modified if desired or set to null to advance to the next store. 22 | /// 23 | public Func, Task> OnStoreResolveCompleted { get; set; } = context => Task.CompletedTask; 24 | 25 | /// 26 | /// Called after tenant resolution has completed for all strategies and stores. The resulting MultiTenantContext can be modified if desired. 27 | /// 28 | public Func, Task> OnTenantResolveCompleted { get; set; } = context => Task.CompletedTask; 29 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Events/StoreResolveCompletedContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Events; 7 | 8 | /// 9 | /// Context for when a MultiTenantStore has attempted to look up a tenant identifier. 10 | /// 11 | /// The ITenantInfo implementation type. 12 | public class StoreResolveCompletedContext 13 | where TTenantInfo : class, ITenantInfo, new() 14 | { 15 | /// 16 | /// Gets or sets the context used for attempted tenant resolution. 17 | /// 18 | public object? Context { get; set; } 19 | 20 | /// 21 | /// The MultiTenantStore instance that was run. 22 | /// 23 | public required IMultiTenantStore Store { get; init; } 24 | 25 | /// 26 | /// The MultiTenantStrategy instance that was run. 27 | /// 28 | public required IMultiTenantStrategy Strategy { get; init; } 29 | 30 | /// 31 | /// The identifier used for tenant resolution by the store. 32 | /// 33 | public required string Identifier { get; init; } 34 | 35 | /// 36 | /// The resolved TenantInfo. Setting to null will cause the next store to run 37 | /// 38 | public TTenantInfo? TenantInfo { get; set; } 39 | 40 | /// 41 | /// Returns true if a tenant was found. 42 | /// 43 | public bool TenantFound => TenantInfo != null; 44 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Events/StrategyResolveCompletedContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Events; 7 | 8 | /// 9 | /// Context for when a MultiTenantStrategy has run. 10 | /// 11 | public class StrategyResolveCompletedContext 12 | { 13 | /// 14 | /// Gets or sets the context used for attempted tenant resolution. 15 | /// 16 | public object? Context { get; set; } 17 | 18 | /// 19 | /// The MultiTenantStrategy instance that was run. 20 | /// 21 | public required IMultiTenantStrategy Strategy { get; init; } 22 | 23 | /// 24 | /// Gets or sets the identifier found by the strategy. Setting to null will cause the next strategy to run. 25 | /// 26 | public string? Identifier { get; set; } 27 | 28 | /// 29 | /// Returns true if a tenant identifier was found. 30 | /// 31 | public bool IdentifierFound => Identifier != null; 32 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Events/TenantResolveCompletedContext.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | 3 | namespace Finbuckle.MultiTenant.Events; 4 | 5 | /// 6 | /// Context for when tenant resolution has completed. 7 | /// 8 | /// The ITenantInfo implementation type. 9 | public record TenantResolveCompletedContext 10 | where TTenantInfo : class, ITenantInfo, new() 11 | { 12 | /// 13 | /// The resolved MultiTenantContext. 14 | /// 15 | public required MultiTenantContext MultiTenantContext { get; set; } 16 | 17 | /// 18 | /// The context used to resolve the tenant. 19 | /// 20 | public required object Context { get; init; } 21 | 22 | /// 23 | /// 24 | /// 25 | public bool IsResolved => MultiTenantContext.IsResolved; 26 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Finbuckle.MultiTenant.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | Finbuckle.MultiTenant 5 | Main library package for Finbuckle.MultiTenant. 6 | 7 | 8 | 9 | 10 | $(TargetFramework.Substring(3, 1)) 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Internal/AsyncLocalMultiTenantContextAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Internal; 7 | 8 | /// 9 | /// Provides access the current MultiTenantContext via an AsyncLocal variable. 10 | /// 11 | /// The ITenantInfo implementation type. 12 | internal class AsyncLocalMultiTenantContextAccessor : IMultiTenantContextSetter, 13 | IMultiTenantContextAccessor 14 | where TTenantInfo : class, ITenantInfo, new() 15 | { 16 | private static readonly AsyncLocal> AsyncLocalContext = new(); 17 | 18 | /// 19 | public IMultiTenantContext MultiTenantContext 20 | { 21 | get => AsyncLocalContext.Value ?? (AsyncLocalContext.Value = new MultiTenantContext()); 22 | set => AsyncLocalContext.Value = value; 23 | } 24 | 25 | /// 26 | IMultiTenantContext IMultiTenantContextAccessor.MultiTenantContext => (IMultiTenantContext)MultiTenantContext; 27 | 28 | IMultiTenantContext IMultiTenantContextSetter.MultiTenantContext 29 | { 30 | set => MultiTenantContext = (IMultiTenantContext)value; 31 | } 32 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Internal/Constants.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant.Internal; 5 | 6 | internal static class Constants 7 | { 8 | public static readonly int TenantIdMaxLength = 64; 9 | public static readonly string TenantToken = "__tenant__"; 10 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Internal/StaticMultiTenantContextAccessor.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | 3 | namespace Finbuckle.MultiTenant.Internal; 4 | 5 | internal class StaticMultiTenantContextAccessor(TTenantInfo? tenantInfo) 6 | : IMultiTenantContextAccessor 7 | where TTenantInfo : class, ITenantInfo, new() 8 | { 9 | IMultiTenantContext IMultiTenantContextAccessor.MultiTenantContext => MultiTenantContext; 10 | 11 | public IMultiTenantContext MultiTenantContext { get; } = 12 | new MultiTenantContext { TenantInfo = tenantInfo }; 13 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Internal/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Reflection; 5 | 6 | namespace Finbuckle.MultiTenant.Internal; 7 | 8 | internal static class TypeExtensions 9 | { 10 | public static bool ImplementsOrInheritsUnboundGeneric(this Type source, Type unboundGeneric) 11 | { 12 | if (unboundGeneric.IsInterface) 13 | { 14 | return source.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == unboundGeneric); 15 | } 16 | 17 | Type? toCheck = source; 18 | 19 | if (unboundGeneric != toCheck) 20 | { 21 | while (toCheck != null && toCheck != typeof(object)) 22 | { 23 | var current = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; 24 | 25 | if (unboundGeneric == current) 26 | { 27 | return true; 28 | } 29 | 30 | toCheck = toCheck.BaseType; 31 | } 32 | } 33 | 34 | return false; 35 | } 36 | 37 | internal static bool HasMultiTenantAttribute(this Type type) 38 | { 39 | return type.GetCustomAttribute() != null; 40 | } 41 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/MultiTenantAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant; 5 | 6 | /// 7 | /// Marks a type as multi-tenant. 8 | /// 9 | [AttributeUsage(AttributeTargets.Class, Inherited = false)] 10 | public class MultiTenantAttribute : Attribute 11 | { 12 | 13 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/MultiTenantBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Finbuckle.MultiTenant; 8 | 9 | // TODO: factor TTenantInfo into WithStore only 10 | 11 | /// 12 | /// Builder class for Finbuckle.MultiTenant configuration. 13 | /// 14 | /// The ITenantInfo implementation type. 15 | public class MultiTenantBuilder where TTenantInfo : class, ITenantInfo, new() 16 | { 17 | /// 18 | /// Gets or sets the IServiceCollection instance used by the builder. 19 | /// 20 | public IServiceCollection Services { get; set; } 21 | 22 | /// 23 | /// Construction a new instance of FinbuckleMultiTenantBuilder. 24 | /// 25 | /// An IServiceCollection instance to be used by the builder. 26 | public MultiTenantBuilder(IServiceCollection services) 27 | { 28 | Services = services; 29 | } 30 | 31 | /// 32 | /// Adds and configures an IMultiTenantStore to the application using default dependency injection. 33 | /// > 34 | /// The service lifetime. 35 | /// a parameter list for any constructor parameters not covered by dependency injection. 36 | /// The same MultiTenantBuilder passed into the method. 37 | public MultiTenantBuilder WithStore(ServiceLifetime lifetime, 38 | params object[] parameters) 39 | where TStore : IMultiTenantStore 40 | => WithStore(lifetime, sp => ActivatorUtilities.CreateInstance(sp, parameters)); 41 | 42 | /// 43 | /// Adds and configures an IMultiTenantStore to the application using a factory method. 44 | /// 45 | /// The service lifetime. 46 | /// A delegate that will create and configure the store. 47 | /// The same MultiTenantBuilder passed into the method. 48 | // ReSharper disable once MemberCanBePrivate.Global 49 | public MultiTenantBuilder WithStore(ServiceLifetime lifetime, 50 | Func factory) 51 | where TStore : IMultiTenantStore 52 | { 53 | ArgumentNullException.ThrowIfNull(factory); 54 | 55 | // Note: can't use TryAddEnumerable here because ServiceDescriptor.Describe with a factory can't set implementation type. 56 | Services.Add( 57 | ServiceDescriptor.Describe(typeof(IMultiTenantStore), sp => factory(sp), lifetime)); 58 | 59 | return this; 60 | } 61 | 62 | /// 63 | /// Adds and configures an IMultiTenantStrategy to the application using default dependency injection. 64 | /// 65 | /// The service lifetime. 66 | /// a parameter list for any constructor parameters not covered by dependency injection. 67 | /// The same MultiTenantBuilder passed into the method. 68 | public MultiTenantBuilder WithStrategy(ServiceLifetime lifetime, 69 | params object[] parameters) where TStrategy : IMultiTenantStrategy 70 | => WithStrategy(lifetime, sp => ActivatorUtilities.CreateInstance(sp, parameters)); 71 | 72 | /// 73 | /// Adds and configures an IMultiTenantStrategy to the application using a factory method. 74 | /// 75 | /// The service lifetime. 76 | /// A delegate that will create and configure the strategy. 77 | /// The same MultiTenantBuilder passed into the method. 78 | public MultiTenantBuilder WithStrategy(ServiceLifetime lifetime, 79 | Func factory) 80 | where TStrategy : IMultiTenantStrategy 81 | { 82 | ArgumentNullException.ThrowIfNull(factory); 83 | 84 | // Potential for multiple entries per service is intended. 85 | Services.Add(ServiceDescriptor.Describe(typeof(IMultiTenantStrategy), sp => factory(sp), lifetime)); 86 | 87 | return this; 88 | } 89 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/MultiTenantContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant; 7 | 8 | /// 9 | /// Contains contextual MultiTenant information. 10 | /// 11 | /// The ITenantInfo implementation type. 12 | public class MultiTenantContext : IMultiTenantContext 13 | where TTenantInfo : class, ITenantInfo, new() 14 | { 15 | /// 16 | public TTenantInfo? TenantInfo { get; set; } 17 | 18 | /// 19 | public bool IsResolved => TenantInfo != null; 20 | 21 | /// 22 | public StrategyInfo? StrategyInfo { get; set; } 23 | 24 | /// 25 | public StoreInfo? StoreInfo { get; set; } 26 | 27 | /// 28 | ITenantInfo? IMultiTenantContext.TenantInfo => TenantInfo; 29 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/MultiTenantException.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | namespace Finbuckle.MultiTenant; 5 | 6 | /// 7 | /// An exception generated by Finbuckle.MultiTenant. 8 | /// 9 | public class MultiTenantException : Exception 10 | { 11 | public MultiTenantException(string? message) : base(message) 12 | { 13 | } 14 | 15 | public MultiTenantException(string? message, Exception? innerException) : base(message, innerException) 16 | { 17 | } 18 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/MultiTenantOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.Events; 6 | 7 | namespace Finbuckle.MultiTenant; 8 | 9 | /// 10 | /// Options for multitenant resolution. 11 | /// 12 | /// The ITenantInfo implementation type.X 13 | public class MultiTenantOptions where TTenantInfo : class, ITenantInfo, new() 14 | { 15 | public Type? TenantInfoType { get; internal set; } 16 | public IList IgnoredIdentifiers { get; set; } = new List(); 17 | public MultiTenantEvents Events { get; set; } = new (); 18 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Options/MultiTenantOptionsCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Collections.Concurrent; 5 | using Finbuckle.MultiTenant.Abstractions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Finbuckle.MultiTenant.Options; 9 | 10 | /// 11 | /// Adds, retrieves, and removes instances of TOptions after adjusting them for the current TenantContext. 12 | /// 13 | public class MultiTenantOptionsCache : IOptionsMonitorCache 14 | where TOptions : class 15 | { 16 | private readonly IMultiTenantContextAccessor multiTenantContextAccessor; 17 | 18 | private readonly ConcurrentDictionary> map = new(); 19 | 20 | /// 21 | /// Constructs a new instance of MultiTenantOptionsCache. 22 | /// 23 | /// 24 | /// 25 | public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor) 26 | { 27 | this.multiTenantContextAccessor = multiTenantContextAccessor ?? 28 | throw new ArgumentNullException(nameof(multiTenantContextAccessor)); 29 | } 30 | 31 | /// 32 | /// Clears all cached options for the current tenant. 33 | /// 34 | public void Clear() 35 | { 36 | var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; 37 | var cache = map.GetOrAdd(tenantId, new OptionsCache()); 38 | 39 | cache.Clear(); 40 | } 41 | 42 | /// 43 | /// Clears all cached options for the given tenant. 44 | /// 45 | /// The Id of the tenant which will have its options cleared. 46 | public void Clear(string tenantId) 47 | { 48 | var cache = map.GetOrAdd(tenantId, new OptionsCache()); 49 | 50 | cache.Clear(); 51 | } 52 | 53 | /// 54 | /// Clears all cached options for all tenants and no tenant. 55 | /// 56 | public void ClearAll() 57 | { 58 | foreach (var cache in map.Values) 59 | cache.Clear(); 60 | } 61 | 62 | /// 63 | /// Gets a named options instance for the current tenant, or adds a new instance created with createOptions. 64 | /// 65 | /// The options name. 66 | /// The factory function for creating the options instance. 67 | /// The existing or new options instance. 68 | public TOptions GetOrAdd(string? name, Func createOptions) 69 | { 70 | ArgumentNullException.ThrowIfNull(createOptions); 71 | 72 | name ??= Microsoft.Extensions.Options.Options.DefaultName; 73 | var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; 74 | var cache = map.GetOrAdd(tenantId, new OptionsCache()); 75 | 76 | return cache.GetOrAdd(name, createOptions); 77 | } 78 | 79 | /// 80 | /// Tries to adds a new option to the cache for the current tenant. 81 | /// 82 | /// The options name. 83 | /// The options instance. 84 | /// True if the options was added to the cache for the current tenant. 85 | public bool TryAdd(string? name, TOptions options) 86 | { 87 | name = name ?? Microsoft.Extensions.Options.Options.DefaultName; 88 | var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; 89 | var cache = map.GetOrAdd(tenantId, new OptionsCache()); 90 | 91 | return cache.TryAdd(name, options); 92 | } 93 | 94 | /// 95 | /// Try to remove an options instance for the current tenant. 96 | /// 97 | /// The options name. 98 | /// True if the options was removed from the cache for the current tenant. 99 | public bool TryRemove(string? name) 100 | { 101 | name = name ?? Microsoft.Extensions.Options.Options.DefaultName; 102 | var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? ""; 103 | var cache = map.GetOrAdd(tenantId, new OptionsCache()); 104 | 105 | return cache.TryRemove(name); 106 | } 107 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Options/MultiTenantOptionsManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | // Portions of this file are derived from the .NET Foundation source file located at: 5 | // https://github.com/aspnet/Options/blob/dev/src/Microsoft.Extensions.Options/OptionsManager.cs 6 | 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace Finbuckle.MultiTenant.Options; 10 | 11 | /// 12 | /// Implementation of IOptions and IOptionsSnapshot that uses dependency injection for its private cache. 13 | /// 14 | /// The type of options being configured. 15 | public class MultiTenantOptionsManager : IOptionsSnapshot where TOptions : class 16 | { 17 | private readonly IOptionsFactory _factory; 18 | private readonly IOptionsMonitorCache _cache; // Note: this is a private cache 19 | 20 | /// 21 | /// Initializes a new instance with the specified options configurations. 22 | /// 23 | /// The factory to use to create options. 24 | /// The cache used for options. 25 | public MultiTenantOptionsManager(IOptionsFactory factory, IOptionsMonitorCache cache) 26 | { 27 | _factory = factory; 28 | _cache = cache; 29 | } 30 | 31 | /// 32 | public TOptions Value => Get(Microsoft.Extensions.Options.Options.DefaultName); 33 | 34 | /// 35 | public TOptions Get(string? name) 36 | { 37 | name ??= Microsoft.Extensions.Options.Options.DefaultName; 38 | 39 | // Store the options in our instance cache. 40 | return _cache.GetOrAdd(name, () => _factory.Create(name)); 41 | } 42 | 43 | /// 44 | /// Clears the options stored in the internal cache. 45 | /// 46 | public void Reset() 47 | { 48 | _cache.Clear(); 49 | } 50 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/StoreInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant; 7 | 8 | public class StoreInfo where TTenantInfo : class, ITenantInfo, new() 9 | { 10 | public Type? StoreType { get; internal set; } 11 | public IMultiTenantStore? Store { get; internal set; } 12 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Stores/EchoStore/EchoStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | // ReSharper disable once CheckNamespace 5 | 6 | using Finbuckle.MultiTenant.Abstractions; 7 | 8 | namespace Finbuckle.MultiTenant.Stores.EchoStore; 9 | 10 | /// 11 | /// Basic store that simply returns a tenant based on the identifier without any additional settings. 12 | /// Note that add, update, and remove functionality is not implemented. 13 | /// If underlying configuration supports reload-on-change then this store will reflect such changes. 14 | /// 15 | /// The ITenantInfo implementation type. 16 | public class EchoStore : IMultiTenantStore where TTenantInfo : class, ITenantInfo, new() 17 | { 18 | /// 19 | public async Task TryGetByIdentifierAsync(string identifier) 20 | { 21 | return await Task.FromResult(new TTenantInfo { Id = identifier, Identifier = identifier }).ConfigureAwait(false); 22 | } 23 | 24 | /// 25 | public async Task TryGetAsync(string id) 26 | { 27 | return await Task.FromResult(new TTenantInfo { Id = id, Identifier = id }).ConfigureAwait(false); 28 | } 29 | 30 | /// 31 | /// Not implemented in this implementation. 32 | /// 33 | /// 34 | public Task TryAddAsync(TTenantInfo tenantInfo) 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | /// 40 | /// Not implemented in this implementation. 41 | /// 42 | /// 43 | public Task TryUpdateAsync(TTenantInfo tenantInfo) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | 48 | /// 49 | /// Not implemented in this implementation. 50 | /// 51 | /// 52 | public Task TryRemoveAsync(string identifier) 53 | { 54 | throw new NotImplementedException(); 55 | } 56 | 57 | /// 58 | /// Not implemented in this implementation. 59 | /// 60 | /// 61 | public Task> GetAllAsync() 62 | { 63 | throw new NotImplementedException(); 64 | } 65 | 66 | /// 67 | /// Not implemented in this implementation. 68 | /// 69 | /// 70 | public Task> GetAllAsync(int take, int skip) 71 | { 72 | throw new NotImplementedException(); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.Internal; 6 | 7 | namespace Finbuckle.MultiTenant.Stores.HttpRemoteStore; 8 | 9 | /// 10 | /// Basic store that can only retrieve tenant via HTTP calls. Note that add, update, and remove functionality is not 11 | /// implemented. Any changes to the tenant store must occur on the server. 12 | /// 13 | /// The ITenantInfo implementation type. 14 | public class HttpRemoteStore : IMultiTenantStore 15 | where TTenantInfo : class, ITenantInfo, new() 16 | { 17 | // ReSharper disable once StaticMemberInGenericType 18 | // (also used on HttpRemoteStoreClient) 19 | internal static readonly string DefaultEndpointTemplateIdentifierToken = $"{{{Constants.TenantToken}}}"; 20 | private readonly HttpRemoteStoreClient _client; 21 | private readonly string endpointTemplate; 22 | 23 | /// 24 | /// Constructor for HttpRemoteStore. 25 | /// 26 | /// HttpRemoteStoreClient instance used to retrieve tenant information. 27 | /// Template string for the remote endpoint. 28 | /// 29 | /// 30 | public HttpRemoteStore(HttpRemoteStoreClient client, string endpointTemplate) 31 | { 32 | _client = client ?? throw new ArgumentNullException(nameof(client)); 33 | if (!endpointTemplate.Contains(DefaultEndpointTemplateIdentifierToken)) 34 | { 35 | if (endpointTemplate.EndsWith('/')) 36 | endpointTemplate += DefaultEndpointTemplateIdentifierToken; 37 | else 38 | endpointTemplate += $"/{DefaultEndpointTemplateIdentifierToken}"; 39 | } 40 | 41 | if (Uri.IsWellFormedUriString(endpointTemplate, UriKind.Absolute)) 42 | throw new ArgumentException("Parameter 'endpointTemplate' is not a well formed uri.", 43 | nameof(endpointTemplate)); 44 | 45 | if (!endpointTemplate.StartsWith("https", StringComparison.OrdinalIgnoreCase) 46 | && !endpointTemplate.StartsWith("http", StringComparison.OrdinalIgnoreCase)) 47 | throw new ArgumentException("Parameter 'endpointTemplate' is not a an http or https uri.", 48 | nameof(endpointTemplate)); 49 | 50 | this.endpointTemplate = endpointTemplate; 51 | } 52 | 53 | /// 54 | /// Not implemented in this implementation. 55 | /// 56 | /// 57 | public Task TryAddAsync(TTenantInfo tenantInfo) 58 | { 59 | throw new NotImplementedException(); 60 | } 61 | 62 | /// 63 | /// Not implemented in this implementation. 64 | /// 65 | /// 66 | public Task TryGetAsync(string id) 67 | { 68 | throw new NotImplementedException(); 69 | } 70 | 71 | /// 72 | /// When a not-found (404) status code is encountered 73 | public async Task> GetAllAsync() 74 | { 75 | return await _client.GetAllAsync(endpointTemplate).ConfigureAwait(false); 76 | } 77 | 78 | /// 79 | /// Not implemented in this implementation. 80 | /// 81 | /// 82 | public Task> GetAllAsync(int take, int skip) 83 | { 84 | throw new NotImplementedException(); 85 | } 86 | 87 | /// 88 | public async Task TryGetByIdentifierAsync(string identifier) 89 | { 90 | var result = await _client.TryGetByIdentifierAsync(endpointTemplate, identifier).ConfigureAwait(false); 91 | return result; 92 | } 93 | 94 | /// 95 | /// Not implemented in this implementation. 96 | /// 97 | /// 98 | public Task TryRemoveAsync(string identifier) 99 | { 100 | throw new NotImplementedException(); 101 | } 102 | 103 | /// 104 | /// Not implemented in this implementation. 105 | /// 106 | /// 107 | public Task TryUpdateAsync(TTenantInfo tenantInfo) 108 | { 109 | throw new NotImplementedException(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Stores/HttpRemoteStore/HttpRemoteStoreClient.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Net; 5 | using System.Text.Json; 6 | using Finbuckle.MultiTenant.Abstractions; 7 | 8 | namespace Finbuckle.MultiTenant.Stores.HttpRemoteStore; 9 | 10 | public class HttpRemoteStoreClient where TTenantInfo : class, ITenantInfo, new() 11 | { 12 | private readonly IHttpClientFactory clientFactory; 13 | private readonly JsonSerializerOptions _defaultSerializerOptions; 14 | 15 | public HttpRemoteStoreClient(IHttpClientFactory clientFactory, JsonSerializerOptions? serializerOptions = default) 16 | { 17 | this.clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); 18 | 19 | _defaultSerializerOptions = serializerOptions ?? new JsonSerializerOptions(JsonSerializerDefaults.Web); 20 | } 21 | 22 | public async Task TryGetByIdentifierAsync(string endpointTemplate, string identifier) 23 | { 24 | var client = clientFactory.CreateClient(typeof(HttpRemoteStoreClient).FullName!); 25 | var uri = endpointTemplate.Replace(HttpRemoteStore.DefaultEndpointTemplateIdentifierToken, identifier); 26 | var response = await client.GetAsync(uri).ConfigureAwait(false); 27 | 28 | if (!response.IsSuccessStatusCode) 29 | return null; 30 | 31 | var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 32 | var result = JsonSerializer.Deserialize(json, _defaultSerializerOptions); 33 | 34 | return result; 35 | } 36 | 37 | public async Task> GetAllAsync(string endpointTemplate) 38 | { 39 | var client = clientFactory.CreateClient(typeof(HttpRemoteStoreClient).FullName!); 40 | var uri = endpointTemplate.Replace(HttpRemoteStore.DefaultEndpointTemplateIdentifierToken, string.Empty); 41 | var response = await client.GetAsync(uri).ConfigureAwait(false); 42 | 43 | 44 | if (!response.IsSuccessStatusCode) 45 | { 46 | // Backwards compatibility check for service implementations that do not include this route. 47 | if (response.StatusCode == HttpStatusCode.NotFound) 48 | { 49 | throw new NotImplementedException(); 50 | } 51 | 52 | return Enumerable.Empty(); 53 | } 54 | 55 | var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); 56 | var result = JsonSerializer.Deserialize>(json, _defaultSerializerOptions); 57 | 58 | return result!; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStore.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Collections.Concurrent; 5 | using Finbuckle.MultiTenant.Abstractions; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Finbuckle.MultiTenant.Stores.InMemoryStore; 9 | 10 | /// 11 | /// Basic store that keeps tenants in memory. 12 | /// 13 | /// The ITenantInfo implementation type. 14 | public class InMemoryStore : IMultiTenantStore 15 | where TTenantInfo : class, ITenantInfo, new() 16 | { 17 | private readonly ConcurrentDictionary _tenantMap; 18 | // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable 19 | private readonly InMemoryStoreOptions _options; 20 | 21 | /// 22 | /// Constructor for InMemoryStore. 23 | /// 24 | /// InMemoryStoreOptions instance for desired behavior. 25 | /// 26 | public InMemoryStore(IOptions> options) 27 | { 28 | _options = options.Value; 29 | 30 | var stringComparer = StringComparer.OrdinalIgnoreCase; 31 | if(_options.IsCaseSensitive) 32 | stringComparer = StringComparer.Ordinal; 33 | 34 | _tenantMap = new ConcurrentDictionary(stringComparer); 35 | foreach(var tenant in _options.Tenants) 36 | { 37 | if(String.IsNullOrWhiteSpace(tenant.Id)) 38 | throw new MultiTenantException("Missing tenant id in options."); 39 | if(String.IsNullOrWhiteSpace(tenant.Identifier)) 40 | throw new MultiTenantException("Missing tenant identifier in options."); 41 | if(_tenantMap.ContainsKey(tenant.Identifier)) 42 | throw new MultiTenantException("Duplicate tenant identifier in options."); 43 | 44 | _tenantMap.TryAdd(tenant.Identifier, tenant); 45 | } 46 | } 47 | 48 | /// 49 | public async Task TryGetAsync(string id) 50 | { 51 | var result = _tenantMap.Values.SingleOrDefault(ti => ti.Id == id); 52 | return await Task.FromResult(result).ConfigureAwait(false); 53 | } 54 | 55 | /// 56 | public async Task TryGetByIdentifierAsync(string identifier) 57 | { 58 | _tenantMap.TryGetValue(identifier, out var result); 59 | 60 | return await Task.FromResult(result).ConfigureAwait(false); 61 | } 62 | 63 | /// 64 | public async Task> GetAllAsync() 65 | { 66 | return await Task.FromResult(_tenantMap.Select(x => x.Value).ToList()).ConfigureAwait(false); 67 | } 68 | 69 | /// 70 | /// Not implemented in this implementation. 71 | /// 72 | /// 73 | public Task> GetAllAsync(int take, int skip) 74 | { 75 | throw new NotImplementedException(); 76 | } 77 | 78 | /// 79 | public async Task TryAddAsync(TTenantInfo tenantInfo) 80 | { 81 | var result = tenantInfo.Identifier != null && _tenantMap.TryAdd(tenantInfo.Identifier, tenantInfo); 82 | 83 | return await Task.FromResult(result).ConfigureAwait(false); 84 | } 85 | 86 | /// 87 | public async Task TryRemoveAsync(string identifier) 88 | { 89 | var result = _tenantMap.TryRemove(identifier, out var _); 90 | 91 | return await Task.FromResult(result).ConfigureAwait(false); 92 | } 93 | 94 | /// 95 | public async Task TryUpdateAsync(TTenantInfo tenantInfo) 96 | { 97 | var existingTenantInfo = tenantInfo.Id != null ? await TryGetAsync(tenantInfo.Id).ConfigureAwait(false) : null; 98 | 99 | if (existingTenantInfo?.Identifier != null) 100 | { 101 | var result = _tenantMap.TryUpdate(existingTenantInfo.Identifier, tenantInfo, existingTenantInfo); 102 | return await Task.FromResult(result).ConfigureAwait(false); 103 | } 104 | 105 | return await Task.FromResult(false).ConfigureAwait(false); 106 | } 107 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Stores/InMemoryStore/InMemoryStoreOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Stores.InMemoryStore; 7 | 8 | public class InMemoryStoreOptions 9 | where TTenantInfo : class, ITenantInfo, new() 10 | { 11 | public bool IsCaseSensitive { get; set; } 12 | public IList Tenants { get; set; } = new List(); 13 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Strategies/DelegateStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Strategies; 7 | 8 | public class DelegateStrategy : IMultiTenantStrategy 9 | { 10 | private readonly Func> _doStrategy; 11 | 12 | public DelegateStrategy(Func> doStrategy) 13 | { 14 | _doStrategy = doStrategy ?? throw new ArgumentNullException(nameof(doStrategy)); 15 | } 16 | 17 | public async Task GetIdentifierAsync(object context) 18 | { 19 | var identifier = await _doStrategy(context).ConfigureAwait(false); 20 | return await Task.FromResult(identifier).ConfigureAwait(false); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Strategies/MultiTenantStrategyWrapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Finbuckle.MultiTenant.Strategies; 8 | 9 | public class MultiTenantStrategyWrapper : IMultiTenantStrategy 10 | { 11 | public IMultiTenantStrategy Strategy { get; } 12 | 13 | private readonly ILogger logger; 14 | 15 | public MultiTenantStrategyWrapper(IMultiTenantStrategy strategy, ILogger logger) 16 | { 17 | this.Strategy = strategy ?? throw new ArgumentNullException(nameof(strategy)); 18 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 19 | } 20 | 21 | public async Task GetIdentifierAsync(object context) 22 | { 23 | string? identifier = null; 24 | 25 | try 26 | { 27 | identifier = await Strategy.GetIdentifierAsync(context).ConfigureAwait(false); 28 | } 29 | catch (Exception e) 30 | { 31 | logger.LogError(e, "Exception in GetIdentifierAsync"); 32 | throw new MultiTenantException($"Exception in {Strategy.GetType()}.GetIdentifierAsync.", e); 33 | } 34 | 35 | if(identifier != null) 36 | { 37 | if (logger.IsEnabled(LogLevel.Debug)) 38 | { 39 | logger.LogDebug("GetIdentifierAsync: Found identifier: \"{Identifier}\"", identifier); 40 | } 41 | } 42 | else 43 | { 44 | if (logger.IsEnabled(LogLevel.Debug)) 45 | { 46 | logger.LogDebug("GetIdentifierAsync: No identifier found"); 47 | } 48 | } 49 | 50 | return identifier; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/Strategies/StaticStrategy.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant.Strategies; 7 | 8 | public class StaticStrategy : IMultiTenantStrategy 9 | { 10 | // internal for testing 11 | // ReSharper disable once MemberCanBePrivate.Global 12 | internal readonly string Identifier; 13 | 14 | public int Priority => -1000; 15 | 16 | public StaticStrategy(string identifier) 17 | { 18 | this.Identifier = identifier; 19 | } 20 | 21 | public async Task GetIdentifierAsync(object context) 22 | { 23 | return await Task.FromResult(Identifier).ConfigureAwait(false); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/StrategyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | 6 | namespace Finbuckle.MultiTenant; 7 | 8 | public class StrategyInfo 9 | { 10 | public Type? StrategyType { get; internal set; } 11 | public IMultiTenantStrategy? Strategy { get; internal set; } 12 | } -------------------------------------------------------------------------------- /src/Finbuckle.MultiTenant/TenantInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.Internal; 6 | 7 | namespace Finbuckle.MultiTenant; 8 | 9 | public class TenantInfo : ITenantInfo 10 | { 11 | private string? id; 12 | 13 | public TenantInfo() 14 | { 15 | } 16 | 17 | public string? Id 18 | { 19 | get 20 | { 21 | return id; 22 | } 23 | set 24 | { 25 | if (value != null) 26 | { 27 | if (value.Length > Constants.TenantIdMaxLength) 28 | { 29 | throw new MultiTenantException($"The tenant id cannot exceed {Constants.TenantIdMaxLength} characters."); 30 | } 31 | id = value; 32 | } 33 | } 34 | } 35 | 36 | public string? Identifier { get; set; } 37 | public string? Name { get; set; } 38 | } -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Finbuckle.MultiTenant.AspNetCore.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | $(TargetFramework.Substring(3, 1)) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantAuthenticationSchemeProviderShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.AspNetCore.Test; 10 | 11 | public class MultiTenantAuthenticationSchemeProviderShould 12 | { 13 | [Fact] 14 | public async Task ReturnPerTenantAuthenticationOptions() 15 | { 16 | var services = new ServiceCollection(); 17 | services.AddAuthentication() 18 | .AddCookie("tenant1Scheme") 19 | .AddCookie("tenant2Scheme"); 20 | 21 | services.AddMultiTenant() 22 | .WithPerTenantAuthentication(); 23 | 24 | services.ConfigureAllPerTenant((ao, ti) => 25 | { 26 | ao.DefaultChallengeScheme = ti.Identifier + "Scheme"; 27 | }); 28 | 29 | var sp = services.BuildServiceProvider(); 30 | 31 | var tenant1 = new TenantInfo{ 32 | Id = "tenant1", 33 | Identifier = "tenant1" 34 | }; 35 | 36 | var tenant2 = new TenantInfo{ 37 | Id = "tenant2", 38 | Identifier = "tenant2" 39 | }; 40 | 41 | var mtc = new MultiTenantContext(); 42 | var setter = sp.GetRequiredService(); 43 | setter.MultiTenantContext = mtc; 44 | 45 | mtc.TenantInfo = tenant1; 46 | var schemeProvider = sp.GetRequiredService(); 47 | 48 | var option = await schemeProvider.GetDefaultChallengeSchemeAsync(); 49 | 50 | Assert.NotNull(option); 51 | Assert.Equal("tenant1Scheme", option.Name); 52 | 53 | mtc.TenantInfo = tenant2; 54 | option = await schemeProvider.GetDefaultChallengeSchemeAsync(); 55 | 56 | Assert.NotNull(option); 57 | Assert.Equal("tenant2Scheme", option.Name); 58 | } 59 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/MultiTenantRouteBuilderShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | // Note: MultiTenantRouteBuilder is a trivial tweak of Microsoft's RouteBuilder implementation. -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Routing/ExcludeFromMultiTenantResolutionShould.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | using Finbuckle.MultiTenant.AspNetCore.Extensions; 3 | using Finbuckle.MultiTenant.AspNetCore.Strategies; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.TestHost; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Xunit; 10 | 11 | namespace Finbuckle.MultiTenant.AspNetCore.Test.Routing; 12 | 13 | public class ExcludeFromMultiTenantResolutionShould 14 | { 15 | private const string EndpointStringResponse = "No tenant available."; 16 | 17 | [Theory] 18 | [InlineData("/initech", "initech")] 19 | [InlineData("/", "initech")] 20 | public async Task ReturnExpectedResponse(string path, string identifier) 21 | { 22 | IWebHostBuilder hostBuilder = GetTestHostBuilder(identifier, "__tenant__", path); 23 | using var server = new TestServer(hostBuilder); 24 | var client = server.CreateClient(); 25 | 26 | var response = await client.GetStringAsync(path); 27 | Assert.Equal(EndpointStringResponse, response); 28 | 29 | response = await client.GetStringAsync(path.TrimEnd('/') + "/tenantInfo"); 30 | Assert.Equal("initech", response); 31 | } 32 | 33 | private static IWebHostBuilder GetTestHostBuilder(string identifier, string sessionKey, string routePattern) 34 | { 35 | return new WebHostBuilder() 36 | .ConfigureServices(services => 37 | { 38 | services.AddDistributedMemoryCache(); 39 | services.AddSession(options => 40 | { 41 | options.IdleTimeout = TimeSpan.FromSeconds(5); 42 | options.Cookie.HttpOnly = true; 43 | options.Cookie.IsEssential = true; 44 | }); 45 | 46 | services.AddMultiTenant() 47 | .WithStrategy(ServiceLifetime.Singleton, sessionKey) 48 | .WithInMemoryStore(); 49 | 50 | services.AddMvc(); 51 | }) 52 | .Configure(app => 53 | { 54 | app.UseRouting(); 55 | app.UseSession(); 56 | app.Use(async (context, next) => 57 | { 58 | context.Session.SetString(sessionKey, identifier); 59 | await next(context); 60 | }); 61 | app.UseMultiTenant(); 62 | 63 | app.UseEndpoints(endpoints => 64 | { 65 | var group = endpoints.MapGroup(routePattern); 66 | 67 | group.Map("/", async context => await WriteResponseAsync(context)) 68 | .ExcludeFromMultiTenantResolution(); 69 | 70 | group.Map("/tenantInfo", async context => await WriteResponseAsync(context)); 71 | }); 72 | 73 | var store = app.ApplicationServices.GetRequiredService>(); 74 | store.TryAddAsync(new TenantInfo { Id = identifier, Identifier = identifier }).Wait(); 75 | }); 76 | } 77 | 78 | private static async Task WriteResponseAsync(HttpContext context) 79 | { 80 | var multiTenantContext = context.GetMultiTenantContext(); 81 | 82 | if (multiTenantContext.TenantInfo?.Id is null) 83 | { 84 | await context.Response.WriteAsync(EndpointStringResponse); 85 | } 86 | else 87 | { 88 | await context.Response.WriteAsync(multiTenantContext.TenantInfo.Id); 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Strategies/HostStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.AspNetCore.Strategies; 5 | using Microsoft.AspNetCore.Http; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.AspNetCore.Test.Strategies; 10 | 11 | public class HostStrategyShould 12 | { 13 | private HttpContext CreateHttpContextMock(string host) 14 | { 15 | var mock = new Mock(); 16 | mock.Setup(c => c.Request.Host).Returns(new HostString(host)); 17 | 18 | return mock.Object; 19 | } 20 | 21 | [Theory] 22 | [InlineData("", "__tenant__", null)] // no host 23 | [InlineData("initech", "__tenant__", "initech")] // basic match 24 | [InlineData("Initech", "__tenant__", "Initech")] // maintain case 25 | [InlineData("abc.com.test.", "__tenant__.", null)] // invalid pattern 26 | [InlineData("abc", "__tenant__.", null)] // invalid pattern 27 | [InlineData("abc", ".__tenant__", null)] // invalid pattern 28 | [InlineData("abc", ".__tenant__.", null)] // invalid pattern 29 | [InlineData("abc-cool.org", "__tenant__-cool.org", "abc")] // mixed segment 30 | [InlineData("abc.com.test", "__tenant__.*", "abc")] // first segment 31 | [InlineData("abc", "__tenant__.*", "abc")] // first and only segment 32 | [InlineData("www.example.test", "?.__tenant__.?", "example")] // domain 33 | [InlineData("www.example.test", "?.__tenant__.*", "example")] // 2nd segment 34 | [InlineData("www.example", "?.__tenant__.*", "example")] // 2nd segment 35 | [InlineData("www.example.r", "?.__tenant__.?.*", "example")] // 2nd segment of 3+ 36 | [InlineData("www.example.r.f", "?.__tenant__.?.*", "example")] // 2nd segment of 3+ 37 | [InlineData("example.ok.test", "*.__tenant__.?.?", "example")] // 3rd last segment 38 | [InlineData("w.example.ok.test", "*.?.__tenant__.?.?", "example")] // 3rd last of 4+ segments 39 | [InlineData("example.com", "__tenant__", "example.com")] // match entire domain (2.1) 40 | public async Task ReturnExpectedIdentifier(string host, string template, string? expected) 41 | { 42 | var httpContext = CreateHttpContextMock(host); 43 | var strategy = new HostStrategy(template); 44 | 45 | var identifier = await strategy.GetIdentifierAsync(httpContext); 46 | 47 | Assert.Equal(expected, identifier); 48 | } 49 | 50 | [Theory] 51 | [InlineData("*.__tenant__.*")] 52 | [InlineData("*a.__tenant__")] 53 | [InlineData("a*a.__tenant__")] 54 | [InlineData("a*.__tenant__")] 55 | [InlineData("*-.__tenant__")] 56 | [InlineData("-*-.__tenant__")] 57 | [InlineData("-*.__tenant__")] 58 | [InlineData("__tenant__.-?")] 59 | [InlineData("__tenant__.-?-")] 60 | [InlineData("__tenant__.?-")] 61 | [InlineData("")] 62 | [InlineData(" ")] 63 | [InlineData(null)] 64 | public void ThrowIfInvalidTemplate(string? template) 65 | { 66 | Assert.Throws(() => 67 | new HostStrategy(template!)); 68 | } 69 | 70 | [Fact] 71 | public async Task ReturnNullIfContextIsNotHttpContext() 72 | { 73 | var context = new object(); 74 | var strategy = new HostStrategy("__tenant__.*"); 75 | 76 | Assert.Null(await strategy.GetIdentifierAsync(context)); 77 | } 78 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Strategies/RemoteAuthenticationCallbackStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | // TODO: Implement more tests. 5 | 6 | using Finbuckle.MultiTenant.AspNetCore.Strategies; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.AspNetCore.Test.Strategies; 10 | 11 | public class RemoteAuthenticationCallbackStrategyShould 12 | { 13 | [Fact] 14 | public void HavePriorityNeg900() 15 | { 16 | var strategy = new RemoteAuthenticationCallbackStrategy(null!); 17 | Assert.Equal(-900, strategy.Priority); 18 | } 19 | 20 | [Fact] 21 | public async Task ReturnNullIfContextIsNotHttpContext() 22 | { 23 | var context = new object(); 24 | var strategy = new RemoteAuthenticationCallbackStrategy(null!); 25 | 26 | Assert.Null(await strategy.GetIdentifierAsync(context)); 27 | } 28 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Strategies/RouteStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.AspNetCore.Strategies; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.TestHost; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Xunit; 12 | 13 | namespace Finbuckle.MultiTenant.AspNetCore.Test.Strategies; 14 | 15 | public class RouteStrategyShould 16 | { 17 | [Theory] 18 | [InlineData("/initech", "initech", "initech")] 19 | [InlineData("/", "initech", "")] 20 | public async Task ReturnExpectedIdentifier(string path, string identifier, string expected) 21 | { 22 | IWebHostBuilder hostBuilder = GetTestHostBuilder(identifier, "{__tenant__=}"); 23 | 24 | using (var server = new TestServer(hostBuilder)) 25 | { 26 | var client = server.CreateClient(); 27 | var response = await client.GetStringAsync(path); 28 | Assert.Equal(expected, response); 29 | } 30 | } 31 | 32 | [Fact] 33 | public async Task ReturnNullIfContextIsNotHttpContext() 34 | { 35 | var context = new object(); 36 | var strategy = new RouteStrategy("__tenant__"); 37 | 38 | Assert.Null(await strategy.GetIdentifierAsync(context)); 39 | } 40 | 41 | [Fact] 42 | public async Task ReturnNullIfNoRouteParamMatch() 43 | { 44 | IWebHostBuilder hostBuilder = GetTestHostBuilder("test_tenant", "{controller}"); 45 | 46 | using (var server = new TestServer(hostBuilder)) 47 | { 48 | var client = server.CreateClient(); 49 | var response = await client.GetStringAsync("/test_tenant"); 50 | Assert.Equal("", response); 51 | } 52 | } 53 | 54 | [Theory] 55 | [InlineData(null)] 56 | [InlineData("")] 57 | [InlineData(" ")] 58 | public void ThrowIfRouteParamIsNullOrWhitespace(string? testString) 59 | { 60 | Assert.Throws(() => 61 | new RouteStrategy(testString!)); 62 | } 63 | 64 | private static IWebHostBuilder GetTestHostBuilder(string identifier, string routePattern) 65 | { 66 | return new WebHostBuilder() 67 | .ConfigureServices(services => 68 | { 69 | services.AddMultiTenant().WithRouteStrategy().WithInMemoryStore(); 70 | services.AddMvc(); 71 | }) 72 | .Configure(app => 73 | { 74 | app.UseRouting(); 75 | app.UseMultiTenant(); 76 | app.UseEndpoints(endpoints => 77 | { 78 | endpoints.Map(routePattern, async context => 79 | { 80 | if (context.GetMultiTenantContext()?.TenantInfo != null) 81 | { 82 | await context.Response.WriteAsync(context.GetMultiTenantContext()! 83 | .TenantInfo!.Id!); 84 | } 85 | }); 86 | }); 87 | 88 | var store = app.ApplicationServices.GetRequiredService>(); 89 | store.TryAddAsync(new TenantInfo { Id = identifier, Identifier = identifier }).Wait(); 90 | }); 91 | } 92 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.AspNetCore.Test/Strategies/SessionStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.AspNetCore.Strategies; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.TestHost; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Xunit; 12 | 13 | namespace Finbuckle.MultiTenant.AspNetCore.Test.Strategies; 14 | 15 | public class SessionStrategyShould 16 | { 17 | private static IWebHostBuilder GetTestHostBuilder(string identifier, string sessionKey) 18 | { 19 | return new WebHostBuilder() 20 | .ConfigureServices(services => 21 | { 22 | services.AddDistributedMemoryCache(); 23 | services.AddSession(options => 24 | { 25 | options.IdleTimeout = TimeSpan.FromSeconds(10); 26 | options.Cookie.HttpOnly = true; 27 | options.Cookie.IsEssential = true; 28 | }); 29 | 30 | services.AddMultiTenant() 31 | .WithStrategy(ServiceLifetime.Singleton, sessionKey) 32 | .WithInMemoryStore(); 33 | services.AddMvc(); 34 | }) 35 | .Configure(app => 36 | { 37 | app.UseSession(); 38 | app.UseMultiTenant(); 39 | app.Run(async context => 40 | { 41 | context.Session.SetString(sessionKey, identifier); 42 | if (context.GetMultiTenantContext()?.TenantInfo != null) 43 | { 44 | await context.Response.WriteAsync(context.GetMultiTenantContext()!.TenantInfo!.Id!); 45 | } 46 | }); 47 | 48 | var store = app.ApplicationServices.GetRequiredService>(); 49 | store.TryAddAsync(new TenantInfo { Id = identifier, Identifier = identifier }).Wait(); 50 | }); 51 | } 52 | 53 | [Fact] 54 | public async Task ReturnNullIfContextIsNotHttpContext() 55 | { 56 | var context = new object(); 57 | var strategy = new SessionStrategy("__tenant__"); 58 | 59 | Assert.Null(await strategy.GetIdentifierAsync(context)); 60 | } 61 | 62 | [Fact] 63 | public async Task ReturnNullIfNoSessionValue() 64 | { 65 | var hostBuilder = GetTestHostBuilder("test_tenant", "__tenant__"); 66 | 67 | using var server = new TestServer(hostBuilder); 68 | var client = server.CreateClient(); 69 | var response = await client.GetStringAsync("/test_tenant"); 70 | Assert.Equal("", response); 71 | } 72 | 73 | // TODO: Figure out how to test this 74 | // public async Task ReturnIdentifierIfSessionValue() 75 | // { 76 | // } 77 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/EntityTypeBuilderExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.Data.Sqlite; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.EntityTypeBuilderExtensions; 10 | 11 | public class EntityTypeBuilderExtensionsShould : IDisposable 12 | { 13 | private readonly SqliteConnection _connection; 14 | 15 | public EntityTypeBuilderExtensionsShould() 16 | { 17 | _connection = new SqliteConnection("DataSource=:memory:"); 18 | _connection.Open(); 19 | } 20 | 21 | public void Dispose() 22 | { 23 | _connection?.Dispose(); 24 | } 25 | 26 | private TestDbContext GetDbContext(Action? config = null, TenantInfo? tenant = null) 27 | { 28 | var options = new DbContextOptionsBuilder() 29 | .ReplaceService() // needed for testing only 30 | .UseSqlite(_connection) 31 | .Options; 32 | return new TestDbContext(config, tenant ?? new TenantInfo(), options); 33 | } 34 | 35 | [Fact] 36 | public void SetMultiTenantAnnotation() 37 | { 38 | using var db = GetDbContext(); 39 | var annotation = db.Model.FindEntityType(typeof(MyMultiTenantThing))? 40 | .FindAnnotation(Constants.MultiTenantAnnotationName); 41 | 42 | Assert.True((bool)annotation!.Value!); 43 | } 44 | 45 | [Fact] 46 | public void AddTenantIdStringShadowProperty() 47 | { 48 | using var db = GetDbContext(); 49 | var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); 50 | 51 | Assert.Equal(typeof(string), prop?.ClrType); 52 | Assert.True(prop?.IsShadowProperty()); 53 | Assert.Null(prop?.FieldInfo); 54 | } 55 | 56 | [Fact] 57 | public void RespectExistingTenantIdStringProperty() 58 | { 59 | using var db = GetDbContext(); 60 | var prop = db.Model.FindEntityType(typeof(MyThingWithTenantId))?.FindProperty("TenantId"); 61 | 62 | Assert.Equal(typeof(string), prop!.ClrType); 63 | Assert.False(prop.IsShadowProperty()); 64 | Assert.NotNull(prop.FieldInfo); 65 | } 66 | 67 | [Fact] 68 | public void ThrowOnNonStringExistingTenantIdProperty() 69 | { 70 | using var db = GetDbContext(b => b.Entity().IsMultiTenant()); 71 | Assert.Throws(() => db.Model); 72 | } 73 | 74 | [Fact] 75 | public void SetsTenantIdStringMaxLength() 76 | { 77 | using var db = GetDbContext(); 78 | var prop = db.Model.FindEntityType(typeof(MyMultiTenantThing))?.FindProperty("TenantId"); 79 | 80 | Assert.Equal(Internal.Constants.TenantIdMaxLength, prop!.GetMaxLength()); 81 | } 82 | 83 | [Fact] 84 | public void SetGlobalFilterQuery() 85 | { 86 | // Doesn't appear to be a way to test this except to try it out... 87 | var tenant1 = new TenantInfo 88 | { 89 | Id = "abc" 90 | }; 91 | 92 | var tenant2 = new TenantInfo 93 | { 94 | Id = "123" 95 | }; 96 | 97 | using var db = GetDbContext(null, tenant1); 98 | db.Database.EnsureCreated(); 99 | db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); 100 | db.SaveChanges(); 101 | 102 | Assert.Equal(1, db.MyMultiTenantThings!.Count()); 103 | db.TenantInfo = tenant2; 104 | Assert.Equal(0, db.MyMultiTenantThings!.Count()); 105 | } 106 | 107 | [Fact] 108 | public void RespectExistingQueryFilter() 109 | { 110 | // Doesn't appear to be a way to test this except to try it out... 111 | var tenant1 = new TenantInfo 112 | { 113 | Id = "abc" 114 | }; 115 | 116 | using var db = GetDbContext(config => 117 | { 118 | config.Entity().HasQueryFilter(e => e.Id == 1); 119 | config.Entity().IsMultiTenant(); 120 | }, tenant1); 121 | db.Database.EnsureCreated(); 122 | db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 1 }); 123 | db.MyMultiTenantThings?.Add(new MyMultiTenantThing() { Id = 2 }); 124 | db.SaveChanges(); 125 | 126 | Assert.Equal(1, db.MyMultiTenantThings!.Count()); 127 | } 128 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using Finbuckle.MultiTenant.Internal; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Infrastructure; 8 | 9 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.EntityTypeBuilderExtensions; 10 | 11 | [SuppressMessage("ReSharper", "UnusedMember.Local")] 12 | public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext 13 | { 14 | private readonly Action? _config; 15 | 16 | public TestDbContext(Action? config, TenantInfo tenantInfo, DbContextOptions options) : 17 | base(new StaticMultiTenantContextAccessor(tenantInfo), options) 18 | { 19 | this._config = config; 20 | } 21 | 22 | public DbSet? MyMultiTenantThings { get; set; } 23 | public DbSet? MyThingsWithTenantIds { get; set; } 24 | public DbSet? MyThingsWithIntTenantId { get; set; } 25 | public DbSet? MyMultiTenantThingsWithAttribute { get; set; } 26 | 27 | protected override void OnModelCreating(ModelBuilder modelBuilder) 28 | { 29 | // If the test passed in a custom builder use it 30 | if (_config != null) 31 | _config(modelBuilder); 32 | // Of use the standard builder configuration 33 | else 34 | { 35 | modelBuilder.Entity().IsMultiTenant(); 36 | modelBuilder.Entity().IsMultiTenant(); 37 | } 38 | 39 | base.OnModelCreating(modelBuilder); 40 | } 41 | } 42 | 43 | // ReSharper disable once ClassNeverInstantiated.Global 44 | public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory 45 | { 46 | public object Create(DbContext context) 47 | { 48 | return new object(); 49 | } 50 | 51 | public object Create(DbContext context, bool designTime) 52 | { 53 | return new object(); 54 | } 55 | } 56 | 57 | public class MyMultiTenantThing 58 | { 59 | public int Id { get; set; } 60 | } 61 | 62 | [MultiTenant] 63 | // ReSharper disable once ClassNeverInstantiated.Global 64 | // ReSharper disable once MemberCanBePrivate.Global 65 | public class MyMultiTenantThingWithAttribute 66 | { 67 | public int Id { get; set; } 68 | } 69 | 70 | // ReSharper disable once MemberCanBePrivate.Global 71 | public class MyThingWithTenantId 72 | { 73 | public int Id { get; set; } 74 | public string? TenantId { get; set; } 75 | } 76 | 77 | // ReSharper disable once MemberCanBePrivate.Global 78 | // ReSharper disable once ClassNeverInstantiated.Global 79 | public class MyThingWithIntTenantId 80 | { 81 | public int Id { get; set; } 82 | public int TenantId { get; set; } 83 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeBuilderExtensions/TestIdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.EntityTypeBuilderExtensions; 8 | 9 | public class TestIdentityDbContext : EntityFrameworkCore.MultiTenantIdentityDbContext 10 | { 11 | public TestIdentityDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) 12 | { 13 | } 14 | 15 | public TestIdentityDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 16 | { 17 | } 18 | 19 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 20 | { 21 | optionsBuilder.UseSqlite("DataSource=:memory:"); 22 | base.OnConfiguring(optionsBuilder); 23 | } 24 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeExtensions/EntityTypeExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Xunit; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.EntityTypeExtensions; 7 | 8 | public class EntityTypeExtensionShould 9 | { 10 | [Fact] 11 | public void ReturnTrueOnIsMultiTenantOnIfMultiTenant() 12 | { 13 | using var db = new TestDbContext(); 14 | Assert.True(db.Model.FindEntityType(typeof(MyMultiTenantThing)).IsMultiTenant()); 15 | } 16 | 17 | [Fact] 18 | public void ReturnTrueOnIsMultiTenantOnIfAncestorIsMultiTenant() 19 | { 20 | using var db = new TestDbContext(); 21 | Assert.True(db.Model.FindEntityType(typeof(MyMultiTenantChildThing)).IsMultiTenant()); 22 | } 23 | 24 | [Fact] 25 | public void ReturnFalseOnIsMultiTenantOnIfNotMultiTenant() 26 | { 27 | using var db = new TestDbContext(); 28 | Assert.False(db.Model.FindEntityType(typeof(MyThing)).IsMultiTenant()); 29 | } 30 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/EntityTypeExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.EntityTypeExtensions; 7 | 8 | public class TestDbContext : DbContext 9 | { 10 | // ReSharper disable once MemberHidesStaticFromOuterClass 11 | // ReSharper disable once UnusedMember.Local 12 | DbSet? MyMultiTenantThing { get; set; } 13 | // ReSharper disable once MemberHidesStaticFromOuterClass 14 | // ReSharper disable once UnusedMember.Local 15 | DbSet? MyThing { get; set; } 16 | 17 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 18 | { 19 | optionsBuilder.UseSqlite("DataSource=:memory:"); 20 | base.OnConfiguring(optionsBuilder); 21 | } 22 | 23 | protected override void OnModelCreating(ModelBuilder modelBuilder) 24 | { 25 | modelBuilder.Entity().IsMultiTenant(); 26 | modelBuilder.Entity(); 27 | } 28 | } 29 | 30 | public class MyThing 31 | { 32 | public int Id { get; set; } 33 | } 34 | 35 | public class MyMultiTenantThing 36 | { 37 | public int Id { get; set; } 38 | } 39 | 40 | public class MyMultiTenantChildThing : MyMultiTenantThing 41 | { 42 | 43 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/ModelBuilderExtensions/ModelBuilderExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Xunit; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.ModelBuilderExtensions; 7 | 8 | public class ModelBuilderExtensionShould 9 | { 10 | [Fact] 11 | public void OnConfigureMultiTenantSetMultiTenantOnTypeWithMultiTenantAttribute() 12 | { 13 | using var db = new TestDbContext(); 14 | Assert.True(db.Model.FindEntityType(typeof(MyMultiTenantThing)).IsMultiTenant()); 15 | } 16 | 17 | [Fact] 18 | public void OnConfigureMultiTenantDoNotSetMultiTenantOnTypeWithoutMultiTenantAttribute() 19 | { 20 | using var db = new TestDbContext(); 21 | Assert.False(db.Model.FindEntityType(typeof(MyThing)).IsMultiTenant()); 22 | } 23 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/ModelBuilderExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.ModelBuilderExtensions; 7 | 8 | public class TestDbContext : DbContext 9 | { 10 | public DbSet? MyMultiTenantThings { get; set; } 11 | public DbSet? MyThings { get; set; } 12 | 13 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 14 | { 15 | optionsBuilder.UseSqlite("DataSource=:memory:"); 16 | base.OnConfiguring(optionsBuilder); 17 | } 18 | 19 | protected override void OnModelCreating(ModelBuilder modelBuilder) 20 | { 21 | modelBuilder.ConfigureMultiTenant(); 22 | } 23 | } 24 | 25 | [MultiTenant] 26 | public class MyMultiTenantThing 27 | { 28 | public int Id { get; set; } 29 | } 30 | 31 | public class MyThing 32 | { 33 | public int Id { get; set; } 34 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/ModelExtensions/ModelExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Xunit; 5 | 6 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.ModelExtensions; 7 | 8 | public class ModelExtensionsShould 9 | { 10 | [Fact] 11 | public void ReturnMultiTenantTypes() 12 | { 13 | using var db = new TestDbContext(); 14 | Assert.Contains(typeof(MyMultiTenantThing), db.Model.GetMultiTenantEntityTypes().Select(et => et.ClrType)); 15 | } 16 | 17 | [Fact] 18 | public void NotReturnNonMultiTenantTypes() 19 | { 20 | using var db = new TestDbContext(); 21 | Assert.DoesNotContain(typeof(MyThing), db.Model.GetMultiTenantEntityTypes().Select(et => et.ClrType)); 22 | } 23 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/ModelExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.ModelExtensions; 8 | 9 | [SuppressMessage("ReSharper", "UnusedMember.Local")] 10 | public class TestDbContext : DbContext 11 | { 12 | DbSet? MyMultiTenantThings { get; set; } 13 | DbSet? MyThings { get; set; } 14 | 15 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 16 | { 17 | optionsBuilder.UseSqlite("DataSource=:memory:"); 18 | base.OnConfiguring(optionsBuilder); 19 | } 20 | 21 | protected override void OnModelCreating(ModelBuilder modelBuilder) 22 | { 23 | modelBuilder.Entity().IsMultiTenant(); 24 | } 25 | } 26 | 27 | public class MyMultiTenantThing 28 | { 29 | public int Id { get; set; } 30 | } 31 | 32 | public class MyThing 33 | { 34 | public int Id { get; set; } 35 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantBuilderExtensions/MultiTenantBuilderExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Finbuckle.MultiTenant.EntityFrameworkCore.Stores.EFCoreStore; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Xunit; 9 | 10 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantBuilderExtensions; 11 | 12 | public class MultiTenantBuilderExtensionsShould 13 | { 14 | [Fact] 15 | public void AddEfCoreStore() 16 | { 17 | var services = new ServiceCollection(); 18 | var builder = new MultiTenantBuilder(services); 19 | builder.WithStaticStrategy("initech").WithEFCoreStore(); 20 | var sp = services.BuildServiceProvider().CreateScope().ServiceProvider; 21 | 22 | var resolver = sp.GetRequiredService>(); 23 | Assert.IsType>(resolver); 24 | } 25 | 26 | [Fact] 27 | public void AddEfCoreStoreWithExistingDbContext() 28 | { 29 | var services = new ServiceCollection(); 30 | var builder = new MultiTenantBuilder(services); 31 | services.AddDbContext(o => o.UseSqlite("DataSource=:memory:")); 32 | builder.WithStaticStrategy("initech").WithEFCoreStore(); 33 | var sp = services.BuildServiceProvider().CreateScope().ServiceProvider; 34 | 35 | var resolver = sp.GetRequiredService>(); 36 | Assert.IsType>(resolver); 37 | } 38 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantBuilderExtensions/TestEfCoreStoreDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.EntityFrameworkCore.Stores.EFCoreStore; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantBuilderExtensions; 8 | 9 | public class TestEfCoreStoreDbContext : EFCoreStoreDbContext 10 | { 11 | public TestEfCoreStoreDbContext(DbContextOptions options) : base(options) 12 | { 13 | } 14 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantDbContextExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantDbContextExtensions; 8 | 9 | public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext 10 | { 11 | public DbSet? Blogs { get; set; } 12 | public DbSet? Posts { get; set; } 13 | 14 | public TestDbContext(TenantInfo tenantInfo, 15 | DbContextOptions options) : 16 | base(new StaticMultiTenantContextAccessor(tenantInfo), options) 17 | { 18 | } 19 | } 20 | 21 | [MultiTenant] 22 | public class Blog 23 | { 24 | public int BlogId { get; set; } 25 | public string? Url { get; set; } 26 | public string? Title { get; set; } 27 | 28 | public List? Posts { get; set; } 29 | } 30 | 31 | [MultiTenant] 32 | public class Post 33 | { 34 | public int PostId { get; set; } 35 | public string? Title { get; set; } 36 | public string? Content { get; set; } 37 | 38 | public Blog? Blog { get; set; } 39 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/MultiTenantEntityTypeBuilderExtensionsShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Xunit; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantEntityTypeBuilderExtensions; 9 | 10 | public class MultiTenantEntityTypeBuilderExtensionsShould 11 | { 12 | private TestDbContext GetDbContext(Action config) 13 | { 14 | var options = new DbContextOptionsBuilder() 15 | .ReplaceService() 16 | .Options; 17 | var db = new TestDbContext(config, options); 18 | return db; 19 | } 20 | 21 | [Fact] 22 | public void AdjustUniqueIndexesOnAdjustUniqueIndexes() 23 | { 24 | using var db = GetDbContext(builder => 25 | { 26 | builder.Entity() 27 | .HasIndex(e => e.BlogId, nameof(Blog.BlogId)) 28 | .HasDatabaseName(nameof(Blog.BlogId) + "DbName") 29 | .IsUnique(); 30 | builder.Entity() 31 | .HasIndex(e => e.Url, nameof(Blog.Url)) 32 | .HasDatabaseName(nameof(Blog.Url) + "DbName") 33 | .IsUnique(); 34 | builder.Entity().IsMultiTenant().AdjustUniqueIndexes(); 35 | }); 36 | var indexes = db.Model.FindEntityType(typeof(Blog))?.GetIndexes().Where(i => i.IsUnique); 37 | 38 | foreach (var index in indexes!.Where(i => i.IsUnique)) 39 | { 40 | Assert.Contains("TenantId", index.Properties.Select(p => p.Name)); 41 | } 42 | } 43 | 44 | [Fact] 45 | public void NotAdjustNonUniqueIndexesOnAdjustUniqueIndexes() 46 | { 47 | using var db = GetDbContext(builder => 48 | { 49 | builder.Entity() 50 | .HasIndex(e => e.BlogId, nameof(Blog.BlogId)) 51 | .HasDatabaseName(nameof(Blog.BlogId) + "DbName") 52 | .IsUnique(); 53 | builder.Entity() 54 | .HasIndex(e => e.Url, nameof(Blog.Url)) 55 | .HasDatabaseName(nameof(Blog.Url) + "DbName"); 56 | builder.Entity().IsMultiTenant().AdjustUniqueIndexes(); 57 | }); 58 | var indexes = db.Model.FindEntityType(typeof(Blog))?.GetIndexes().Where(i => i.IsUnique); 59 | 60 | foreach (var index in indexes!.Where(i => !i.IsUnique)) 61 | { 62 | Assert.DoesNotContain("TenantId", index.Properties.Select(p => p.Name)); 63 | } 64 | } 65 | 66 | [Fact] 67 | public void AdjustAllIndexesOnAdjustIndexes() 68 | { 69 | using var db = GetDbContext(builder => 70 | { 71 | builder.Entity() 72 | .HasIndex(e => e.BlogId, nameof(Blog.BlogId)) 73 | .HasDatabaseName(nameof(Blog.BlogId) + "DbName") 74 | .IsUnique(); 75 | builder.Entity() 76 | .HasIndex(e => e.Url, nameof(Blog.Url)) 77 | .HasDatabaseName(nameof(Blog.Url) + "DbName"); 78 | builder.Entity().IsMultiTenant().AdjustIndexes(); 79 | }); 80 | var indexes = db.Model.FindEntityType(typeof(Blog))?.GetIndexes().Where(i => i.IsUnique); 81 | 82 | foreach (var index in indexes!) 83 | { 84 | Assert.Contains("TenantId", index.Properties.Select(p => p.Name)); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Extensions/MultiTenantEntityTypeBuilderExtensions/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.Extensions.MultiTenantEntityTypeBuilderExtensions; 9 | 10 | public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext 11 | { 12 | private readonly Action _config; 13 | 14 | public TestDbContext(Action config, DbContextOptions options) : 15 | base(new StaticMultiTenantContextAccessor(new TenantInfo { Id = "dummy" }), options) 16 | { 17 | this._config = config; 18 | } 19 | 20 | public DbSet? Blogs { get; set; } 21 | public DbSet? Posts { get; set; } 22 | 23 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 24 | { 25 | optionsBuilder.UseSqlite("DataSource=:memory:"); 26 | base.OnConfiguring(optionsBuilder); 27 | } 28 | 29 | protected override void OnModelCreating(ModelBuilder modelBuilder) 30 | { 31 | _config(modelBuilder); 32 | } 33 | } 34 | 35 | public class Blog 36 | { 37 | public int BlogId { get; set; } 38 | public string? Url { get; set; } 39 | 40 | public List? Posts { get; set; } 41 | } 42 | 43 | public class Post 44 | { 45 | public int PostId { get; set; } 46 | public string? Title { get; set; } 47 | public string? Content { get; set; } 48 | 49 | public Blog? Blog { get; set; } 50 | } 51 | 52 | public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory 53 | { 54 | public object Create(DbContext context) 55 | { 56 | return new object(); 57 | } 58 | 59 | public object Create(DbContext context, bool designTime) 60 | { 61 | // Needed for tests that change the model. 62 | return new object(); 63 | } 64 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/Finbuckle.MultiTenant.EntityFrameworkCore.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | $(TargetFramework.Substring(3, 1)) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/MultiTenantDbContextShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantDbContext; 10 | 11 | public class MultiTenantDbContextShould 12 | { 13 | [Fact] 14 | public void WorkWithDependencyInjection() 15 | { 16 | var services = new ServiceCollection(); 17 | services.AddMultiTenant(); 18 | services.AddDbContext(options => 19 | { 20 | options.UseSqlite("DataSource=:memory:"); 21 | }); 22 | var scope = services.BuildServiceProvider().CreateScope(); 23 | 24 | var context = scope.ServiceProvider.GetService(); 25 | Assert.NotNull(context); 26 | } 27 | 28 | [Fact] 29 | public void WorkWithSingleParamCtor() 30 | { 31 | var tenant1 = new TenantInfo 32 | { 33 | Id = "abc", 34 | Identifier = "abc", 35 | Name = "abc" 36 | }; 37 | var mca = new StaticMultiTenantContextAccessor(tenant1); 38 | var c = new TestBlogDbContext(mca); 39 | 40 | Assert.NotNull(c); 41 | } 42 | 43 | [Fact] 44 | public void WorkWithTwoParamCtor() 45 | { 46 | var tenant1 = new TenantInfo 47 | { 48 | Id = "abc", 49 | Identifier = "abc", 50 | Name = "abc" 51 | }; 52 | var mca = new StaticMultiTenantContextAccessor(tenant1); 53 | var c = new TestBlogDbContext(mca, new DbContextOptions()); 54 | 55 | Assert.NotNull(c); 56 | } 57 | 58 | [Fact] 59 | public void WorkWithCreateDbOptions() 60 | { 61 | var tenant1 = new TenantInfo 62 | { 63 | Id = "abc", 64 | Identifier = "abc", 65 | Name = "abc" 66 | }; 67 | var c = 68 | EntityFrameworkCore.MultiTenantDbContext.Create(tenant1, new DbContextOptions()); 69 | 70 | Assert.NotNull(c); 71 | } 72 | 73 | [Fact] 74 | public void WorkWithCreateDependencies() 75 | { 76 | var tenant1 = new TenantInfo 77 | { 78 | Id = "abc", 79 | Identifier = "abc", 80 | Name = "abc" 81 | }; 82 | var c = 83 | EntityFrameworkCore.MultiTenantDbContext.Create(tenant1, new object()); 84 | 85 | Assert.NotNull(c); 86 | } 87 | 88 | [Fact] 89 | public void WorkWithCreateServiceProvider() 90 | { 91 | // create a sp 92 | var services = new ServiceCollection(); 93 | services.AddTransient(sp => 42); 94 | var sp = services.BuildServiceProvider(); 95 | 96 | var tenant1 = new TenantInfo 97 | { 98 | Id = "abc", 99 | Identifier = "abc", 100 | Name = "abc" 101 | }; 102 | var c = 103 | EntityFrameworkCore.MultiTenantDbContext.Create(tenant1, sp); 104 | 105 | Assert.NotNull(c); 106 | } 107 | 108 | [Fact] 109 | public void WorkWithCreateNoOptions() 110 | { 111 | var tenant1 = new TenantInfo 112 | { 113 | Id = "abc", 114 | Identifier = "abc", 115 | Name = "abc" 116 | }; 117 | var c = EntityFrameworkCore.MultiTenantDbContext.Create(tenant1); 118 | 119 | Assert.NotNull(c); 120 | } 121 | 122 | [Fact] 123 | public void CreateMultiTenantIdentityDbContext() 124 | { 125 | var tenant1 = new TenantInfo 126 | { 127 | Id = "abc", 128 | Identifier = "abc", 129 | Name = "abc" 130 | }; 131 | var c = EntityFrameworkCore.MultiTenantDbContext.Create(tenant1); 132 | 133 | Assert.NotNull(c); 134 | } 135 | 136 | [Fact] 137 | public void ThrowOnInvalidDbContext() 138 | { 139 | var tenant1 = new TenantInfo 140 | { 141 | Id = "abc", 142 | Identifier = "abc", 143 | Name = "abc" 144 | }; 145 | 146 | Assert.Throws(() => EntityFrameworkCore.MultiTenantDbContext.Create(tenant1)); 147 | } 148 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantDbContext/TestEntitities.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantDbContext; 8 | 9 | public class TestBlogDbContext : EntityFrameworkCore.MultiTenantDbContext 10 | { 11 | public DbSet? Blogs { get; set; } 12 | public DbSet? Posts { get; set; } 13 | 14 | public TestBlogDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) 15 | { 16 | } 17 | 18 | public TestBlogDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 19 | { 20 | } 21 | 22 | public TestBlogDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, object dependency) : base(multiTenantContextAccessor) 23 | { 24 | } 25 | } 26 | 27 | [MultiTenant] 28 | public class Blog 29 | { 30 | public int BlogId { get; set; } 31 | public string? Title { get; set; } 32 | public List? Posts { get; set; } 33 | } 34 | 35 | [MultiTenant] 36 | public class Post 37 | { 38 | public int PostId { get; set; } 39 | public string? Title { get; set; } 40 | 41 | public int BlogId { get; set; } 42 | public Blog? Blog { get; set; } 43 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantEntityTypeBuilder/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Infrastructure; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantEntityTypeBuilder; 9 | 10 | public class TestDbContext : EntityFrameworkCore.MultiTenantDbContext 11 | { 12 | private readonly Action _config; 13 | 14 | public TestDbContext(Action config, DbContextOptions options) : 15 | base(new StaticMultiTenantContextAccessor(new TenantInfo { Id = "dummy" }), options) 16 | { 17 | this._config = config; 18 | } 19 | 20 | public DbSet? Blogs { get; set; } 21 | public DbSet? Posts { get; set; } 22 | 23 | protected override void OnModelCreating(ModelBuilder modelBuilder) 24 | { 25 | _config(modelBuilder); 26 | } 27 | } 28 | 29 | public class Blog 30 | { 31 | public int BlogId { get; set; } 32 | public string? Url { get; set; } 33 | 34 | public List? Posts { get; set; } 35 | } 36 | 37 | public class Post 38 | { 39 | public int PostId { get; set; } 40 | public string? Title { get; set; } 41 | public string? Content { get; set; } 42 | 43 | public Blog? Blog { get; set; } 44 | } 45 | 46 | // ReSharper disable once ClassNeverInstantiated.Global 47 | public class DynamicModelCacheKeyFactory : IModelCacheKeyFactory 48 | { 49 | public object Create(DbContext context) 50 | { 51 | return new object(); 52 | } 53 | 54 | public object Create(DbContext context, bool designTime) 55 | { 56 | return new Object(); // Never cache! 57 | } 58 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantIdentityDbContext/TestIdentityDbContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantIdentityDbContext; 8 | 9 | public class TestIdentityDbContext : EntityFrameworkCore.MultiTenantIdentityDbContext 10 | { 11 | public TestIdentityDbContext(IMultiTenantContextAccessor multiTenantContextAccessor) : base( 12 | multiTenantContextAccessor) 13 | { 14 | } 15 | 16 | public TestIdentityDbContext(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 17 | { 18 | } 19 | 20 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 21 | { 22 | optionsBuilder.UseSqlite("DataSource=:memory:"); 23 | base.OnConfiguring(optionsBuilder); 24 | } 25 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantIdentityDbContext/TestIdentityDbContextAll.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantIdentityDbContext; 9 | 10 | public class TestIdentityDbContextAll : MultiTenantIdentityDbContext, IdentityUserRole, IdentityUserLogin, IdentityRoleClaim, IdentityUserToken> 11 | { 12 | public TestIdentityDbContextAll(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) 13 | { 14 | } 15 | 16 | public TestIdentityDbContextAll(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 17 | { 18 | } 19 | 20 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 21 | { 22 | optionsBuilder.UseSqlite("DataSource=:memory:"); 23 | base.OnConfiguring(optionsBuilder); 24 | } 25 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantIdentityDbContext/TestIdentityDbContextTUser.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantIdentityDbContext; 9 | 10 | public class TestIdentityDbContextTUser : MultiTenantIdentityDbContext 11 | { 12 | public TestIdentityDbContextTUser(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) 13 | { 14 | } 15 | 16 | public TestIdentityDbContextTUser(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 17 | { 18 | } 19 | 20 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 21 | { 22 | optionsBuilder.UseSqlite("DataSource=:memory:"); 23 | base.OnConfiguring(optionsBuilder); 24 | } 25 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.EntityFrameworkCore.Test/MultiTenantIdentityDbContext/TestIdentityDbContextTUserTRole.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Finbuckle.MultiTenant.EntityFrameworkCore.Test.MultiTenantIdentityDbContext; 9 | 10 | public class TestIdentityDbContextTUserTRole : MultiTenantIdentityDbContext 11 | { 12 | public TestIdentityDbContextTUserTRole(IMultiTenantContextAccessor multiTenantContextAccessor) : base(multiTenantContextAccessor) 13 | { 14 | } 15 | 16 | public TestIdentityDbContextTUserTRole(IMultiTenantContextAccessor multiTenantContextAccessor, DbContextOptions options) : base(multiTenantContextAccessor, options) 17 | { 18 | } 19 | 20 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 21 | { 22 | optionsBuilder.UseSqlite("DataSource=:memory:"); 23 | base.OnConfiguring(optionsBuilder); 24 | } 25 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/ConfigurationStoreTestSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Finbuckle:MultiTenant:Stores:ConfigurationStore": { 3 | "Defaults": { 4 | "ConnectionString": "Datasource=sample.db" 5 | }, 6 | "Tenants": [ 7 | { 8 | "Id": "initech-id", 9 | "Identifier": "initech", 10 | "Name": "Initech" 11 | }, 12 | { 13 | "Id": "lol-id", 14 | "Identifier": "lol", 15 | "Name": "LOL", 16 | "ConnectionString": "Datasource=lol.db" 17 | } 18 | ] 19 | } 20 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/ConfigurationStoreTestSettings_NoDefaults.json: -------------------------------------------------------------------------------- 1 | { 2 | "Finbuckle:MultiTenant:Stores:ConfigurationStore": { 3 | "Tenants": [ 4 | { 5 | "Id": "initech-id", 6 | "Identifier": "initech", 7 | "Name": "Initech", 8 | "ConnectionString": "Datasource=sample.db" 9 | }, 10 | { 11 | "Id": "lol-id", 12 | "Identifier": "lol", 13 | "Name": "LOL", 14 | "ConnectionString": "Datasource=lol.db" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Finbuckle.MultiTenant.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0;net9.0 4 | false 5 | enable 6 | true 7 | 8 | 9 | 10 | 11 | $(TargetFramework.Substring(3, 1)) 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/MultiTenantContextShould.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | using Xunit; 3 | 4 | namespace Finbuckle.MultiTenant.Test; 5 | 6 | public class MultiTenantContextShould 7 | { 8 | [Fact] 9 | public void ReturnFalseForIsResolvedIfTenantInfoIsNull() 10 | { 11 | IMultiTenantContext context = new MultiTenantContext(); 12 | Assert.False(context.IsResolved); 13 | } 14 | 15 | [Fact] 16 | public void ReturnTrueIsResolvedIfTenantInfoIsNotNull() 17 | { 18 | var context = new MultiTenantContext 19 | { 20 | TenantInfo = new TenantInfo() 21 | }; 22 | 23 | Assert.True(context.IsResolved); 24 | } 25 | 26 | [Fact] 27 | public void ReturnFalseForIsResolvedIfTenantInfoIsNull_NonGeneric() 28 | { 29 | IMultiTenantContext context = new MultiTenantContext(); 30 | Assert.False(context.IsResolved); 31 | } 32 | 33 | [Fact] 34 | public void ReturnTrueIsResolvedIfTenantInfoIsNotNull_NonGeneric() 35 | { 36 | var context = new MultiTenantContext 37 | { 38 | TenantInfo = new TenantInfo() 39 | }; 40 | 41 | IMultiTenantContext iContext = context; 42 | Assert.True(iContext.IsResolved); 43 | } 44 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Options/MultiTenantOptionsManagerShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Options; 5 | using Microsoft.Extensions.Options; 6 | using Moq; 7 | using Xunit; 8 | 9 | namespace Finbuckle.MultiTenant.Test.Options; 10 | 11 | public class MultiTenantOptionsManagerShould 12 | { 13 | [Theory] 14 | [InlineData("OptionName1")] 15 | [InlineData("OptionName2")] 16 | public void GetOptionByName(string optionName) 17 | { 18 | var mock = new Mock>(); 19 | mock.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(new Object()); 20 | 21 | var manager = new MultiTenantOptionsManager(null!, mock.Object); 22 | 23 | manager.Get(optionName); 24 | 25 | mock.Verify(c => c.GetOrAdd(It.Is(p => p == optionName), It.IsAny>()), Times.Once); 26 | } 27 | 28 | [Fact] 29 | public void GetOptionByDefaultNameIfNameNull() 30 | { 31 | var mock = new Mock>(); 32 | mock.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(new Object()); 33 | 34 | var manager = new MultiTenantOptionsManager(null!, mock.Object); 35 | 36 | manager.Get(null!); 37 | 38 | mock.Verify(c => c.GetOrAdd(It.Is(p => p == Microsoft.Extensions.Options.Options.DefaultName), It.IsAny>()), Times.Once); 39 | } 40 | 41 | [Fact] 42 | public void GetOptionByDefaultNameIfGettingValueProp() 43 | { 44 | var mock = new Mock>(); 45 | mock.Setup(c => c.GetOrAdd(It.IsAny(), It.IsAny>())).Returns(new Object()); 46 | 47 | var manager = new MultiTenantOptionsManager(null!, mock.Object); 48 | 49 | var dummy = manager.Value; 50 | 51 | mock.Verify(c => c.GetOrAdd(It.Is(p => p == Microsoft.Extensions.Options.Options.DefaultName), It.IsAny>()), Times.Once); 52 | } 53 | 54 | [Fact] 55 | public void ClearCacheOnReset() 56 | { 57 | var mock = new Mock>(); 58 | mock.Setup(i => i.Clear()); 59 | 60 | var manager = new MultiTenantOptionsManager(null!, mock.Object); 61 | manager.Reset(); 62 | 63 | mock.Verify(i => i.Clear(), Times.Once); 64 | } 65 | 66 | // ReSharper disable once ClassNeverInstantiated.Global 67 | public class TestOptionsCache : IOptionsMonitorCache where TOptions : class 68 | { 69 | public virtual void Clear() 70 | { 71 | throw new NotImplementedException(); 72 | } 73 | 74 | public virtual TOptions GetOrAdd(string? name, Func createOptions) 75 | { 76 | throw new NotImplementedException(); 77 | } 78 | 79 | public virtual bool TryAdd(string? name, TOptions options) 80 | { 81 | throw new NotImplementedException(); 82 | } 83 | 84 | public virtual bool TryRemove(string? name) 85 | { 86 | throw new NotImplementedException(); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Stores/HttpRemoteStoreClientShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Stores.HttpRemoteStore; 5 | using Xunit; 6 | 7 | namespace Finbuckle.MultiTenant.Test.Stores; 8 | 9 | public class HttpRemoteStoreClientShould 10 | { 11 | [Fact] 12 | public void ThrowIfHttpClientFactoryIsNull() 13 | { 14 | Assert.Throws(() => new HttpRemoteStoreClient(null!)); 15 | } 16 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Stores/IMultiTenantStoreTestBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Abstractions; 5 | using Xunit; 6 | 7 | #pragma warning disable xUnit1013 // Public method should be marked as test 8 | 9 | namespace Finbuckle.MultiTenant.Test.Stores; 10 | 11 | // TODO convert these to async 12 | 13 | public abstract class MultiTenantStoreTestBase 14 | { 15 | protected abstract IMultiTenantStore CreateTestStore(); 16 | 17 | protected virtual IMultiTenantStore PopulateTestStore(IMultiTenantStore store) 18 | { 19 | store.TryAddAsync(new TenantInfo { Id = "initech-id", Identifier = "initech", Name = "Initech" }).Wait(); 20 | store.TryAddAsync(new TenantInfo { Id = "lol-id", Identifier = "lol", Name = "Lol, Inc." }).Wait(); 21 | 22 | return store; 23 | } 24 | 25 | //[Fact] 26 | public virtual void GetTenantInfoFromStoreById() 27 | { 28 | var store = CreateTestStore(); 29 | 30 | Assert.Equal("initech", store.TryGetAsync("initech-id").Result!.Identifier); 31 | } 32 | 33 | //[Fact] 34 | public virtual void ReturnNullWhenGettingByIdIfTenantInfoNotFound() 35 | { 36 | var store = CreateTestStore(); 37 | 38 | Assert.Null(store.TryGetAsync("fake123").Result); 39 | } 40 | 41 | //[Fact] 42 | public virtual void GetTenantInfoFromStoreByIdentifier() 43 | { 44 | var store = CreateTestStore(); 45 | 46 | Assert.Equal("initech", store.TryGetByIdentifierAsync("initech").Result!.Identifier); 47 | } 48 | 49 | //[Fact] 50 | public virtual void ReturnNullWhenGettingByIdentifierIfTenantInfoNotFound() 51 | { 52 | var store = CreateTestStore(); 53 | Assert.Null(store.TryGetByIdentifierAsync("fake123").Result); 54 | } 55 | 56 | //[Fact] 57 | public virtual void AddTenantInfoToStore() 58 | { 59 | var store = CreateTestStore(); 60 | 61 | Assert.Null(store.TryGetByIdentifierAsync("identifier").Result); 62 | Assert.True(store.TryAddAsync(new TenantInfo 63 | { Id = "id", Identifier = "identifier", Name = "name" }).Result); 64 | Assert.NotNull(store.TryGetByIdentifierAsync("identifier").Result); 65 | } 66 | 67 | //[Fact] 68 | public virtual void UpdateTenantInfoInStore() 69 | { 70 | var store = CreateTestStore(); 71 | 72 | var result = store.TryUpdateAsync(new TenantInfo 73 | { Id = "initech-id", Identifier = "initech2", Name = "Initech2" }).Result; 74 | Assert.True(result); 75 | } 76 | 77 | //[Fact] 78 | public virtual void RemoveTenantInfoFromStore() 79 | { 80 | var store = CreateTestStore(); 81 | Assert.NotNull(store.TryGetByIdentifierAsync("initech").Result); 82 | Assert.True(store.TryRemoveAsync("initech").Result); 83 | Assert.Null(store.TryGetByIdentifierAsync("initech").Result); 84 | } 85 | 86 | //[Fact] 87 | public virtual void GetAllTenantsFromStoreAsync() 88 | { 89 | var store = CreateTestStore(); 90 | Assert.Equal(2, store.GetAllAsync().Result.Count()); 91 | } 92 | 93 | //[Fact] 94 | public virtual void GetAllTenantsFromStoreAsyncSkip1Take1() 95 | { 96 | var store = CreateTestStore(); 97 | var tenants = store.GetAllAsync(1, 1).Result.ToList(); 98 | Assert.Single(tenants); 99 | 100 | var tenant = tenants.FirstOrDefault(); 101 | Assert.NotNull(tenant); 102 | Assert.Equal("lol", tenant!.Identifier); 103 | } 104 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Strategies/DelegateStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Strategies; 5 | using Xunit; 6 | 7 | namespace Finbuckle.MultiTenant.Test.Strategies; 8 | 9 | public class DelegateStrategyShould 10 | { 11 | [Fact] 12 | public async Task CallDelegate() 13 | { 14 | int i = 0; 15 | var strategy = new DelegateStrategy(_ => Task.FromResult((i++).ToString())); 16 | await strategy.GetIdentifierAsync(new object()); 17 | 18 | Assert.Equal(1, i); 19 | } 20 | 21 | [Theory] 22 | [InlineData("initech-id")] 23 | [InlineData("")] 24 | [InlineData(null)] 25 | public async Task ReturnDelegateResult(string? identifier) 26 | { 27 | var strategy = new DelegateStrategy(async _ => await Task.FromResult(identifier)); 28 | var result = await strategy.GetIdentifierAsync(new object()); 29 | 30 | Assert.Equal(identifier, result); 31 | } 32 | 33 | [Fact] 34 | public async Task BeAbleToReturnNull() 35 | { 36 | var strategy = new DelegateStrategy(async _ => await Task.FromResult(null)); 37 | var result = await strategy.GetIdentifierAsync(new object()); 38 | 39 | Assert.Null(result); 40 | } 41 | 42 | [Fact] 43 | public void ThrowIfNullDelegate() 44 | { 45 | Assert.Throws(() => new DelegateStrategy(null!)); 46 | } 47 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/Strategies/StaticStrategyShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Strategies; 5 | using Xunit; 6 | 7 | namespace Finbuckle.MultiTenant.Test.Strategies; 8 | 9 | public class StaticStrategyShould 10 | { 11 | [Theory] 12 | [InlineData("initech")] 13 | [InlineData("Initech")] // maintain case 14 | [InlineData("")] // empty string 15 | [InlineData(" ")] // whitespace 16 | [InlineData(null)] // null 17 | public async Task ReturnExpectedIdentifier(string? staticIdentifier) 18 | { 19 | var strategy = 20 | new StaticStrategy(staticIdentifier!); 21 | 22 | var identifier = await strategy.GetIdentifierAsync(new Object()); 23 | 24 | Assert.Equal(staticIdentifier, identifier); 25 | } 26 | 27 | [Fact] 28 | public void HavePriorityNeg1000() 29 | { 30 | var strategy = new StaticStrategy(""); 31 | Assert.Equal(-1000, strategy.Priority); 32 | } 33 | } -------------------------------------------------------------------------------- /test/Finbuckle.MultiTenant.Test/TenantInfoShould.cs: -------------------------------------------------------------------------------- 1 | // Copyright Finbuckle LLC, Andrew White, and Contributors. 2 | // Refer to the solution LICENSE file for more information. 3 | 4 | using Finbuckle.MultiTenant.Internal; 5 | using Xunit; 6 | 7 | namespace Finbuckle.MultiTenant.Test; 8 | 9 | public class TenantInfoShould 10 | { 11 | [Fact] 12 | public void ThrowIfIdSetWithLengthAboveTenantIdMaxLength() 13 | { 14 | // ReSharper disable once ObjectCreationAsStatement 15 | new TenantInfo { Id = "".PadRight(1, 'a') }; 16 | 17 | // ReSharper disable once ObjectCreationAsStatement 18 | new TenantInfo { Id = "".PadRight(Constants.TenantIdMaxLength, 'a') }; 19 | 20 | Assert.Throws(() => new TenantInfo 21 | { Id = "".PadRight(Constants.TenantIdMaxLength + 1, 'a') }); 22 | Assert.Throws(() => new TenantInfo 23 | { 24 | Id = "".PadRight(Constants.TenantIdMaxLength 25 | + 999, 'a') 26 | }); 27 | } 28 | } --------------------------------------------------------------------------------