SaasSuite.EfCore 26.3.3.2

dotnet add package SaasSuite.EfCore --version 26.3.3.2
                    
NuGet\Install-Package SaasSuite.EfCore -Version 26.3.3.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SaasSuite.EfCore" Version="26.3.3.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SaasSuite.EfCore" Version="26.3.3.2" />
                    
Directory.Packages.props
<PackageReference Include="SaasSuite.EfCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SaasSuite.EfCore --version 26.3.3.2
                    
#r "nuget: SaasSuite.EfCore, 26.3.3.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SaasSuite.EfCore@26.3.3.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SaasSuite.EfCore&version=26.3.3.2
                    
Install as a Cake Addin
#tool nuget:?package=SaasSuite.EfCore&version=26.3.3.2
                    
Install as a Cake Tool

SaasSuite.EfCore

NuGet License: Apache-2.0 Platform

Entity Framework Core integration for multi-tenant SaaS applications with automatic tenant isolation and per-tenant database support.

Overview

SaasSuite.EfCore provides seamless integration between Entity Framework Core and SaasSuite's multi-tenancy features. It automatically isolates tenant data through query filters, manages per-tenant databases, and prevents accidental cross-tenant data access.

Features

  • Automatic Query Filtering: Global filters automatically scope queries to the current tenant
  • Tenant ID Auto-Injection: Automatically sets tenant ID on new entities
  • Per-Tenant Databases: Support for separate databases per tenant
  • Per-Tenant Model Caching: Different schemas for different tenants
  • Change Tracking Protection: Prevents unauthorized tenant ID modifications
  • Flexible Configuration: Enable/disable features based on your needs

Installation

dotnet add package SaasSuite.EfCore
dotnet add package SaasSuite.Core

Quick Start

1. Mark Entities as Tenant-Scoped

using SaasSuite.Core;
using SaasSuite.EfCore.Interfaces;

public class Product : ITenantEntity
{
    public int Id { get; set; }
    public TenantId TenantId { get; set; } // Required by ITenantEntity
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public class Order : ITenantEntity
{
    public int Id { get; set; }
    public TenantId TenantId { get; set; }
    public DateTime OrderDate { get; set; }
    public List<OrderItem> Items { get; set; }
}

2. Configure DbContext

using Microsoft.EntityFrameworkCore;
using SaasSuite.Core.Interfaces;

public class AppDbContext : DbContext
{
    private readonly ITenantAccessor _tenantAccessor;
    
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
    
    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        ITenantAccessor tenantAccessor) : base(options)
    {
        _tenantAccessor = tenantAccessor;
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        
        // Apply automatic tenant query filters
        modelBuilder.ApplyTenantQueryFilter(_tenantAccessor);
    }
}

3. Register Services

var builder = WebApplication.CreateBuilder(args);

// Register SaasSuite.Core
builder.Services.AddSaasCore();

// Register tenant-aware DbContext
builder.Services.AddSaasTenantDbContext<AppDbContext>(options =>
{
    options.AutoSetTenantId = true;
    options.UseGlobalQueryFilter = true;
    options.UsePerTenantModelCache = false; // Enable if using per-tenant schemas
});

Core Features

Automatic Query Filtering

All queries on ITenantEntity types are automatically filtered to the current tenant:

public class ProductService
{
    private readonly AppDbContext _context;
    
    public ProductService(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<List<Product>> GetProductsAsync()
    {
        // Automatically filtered to current tenant
        // No need to manually filter by TenantId
        return await _context.Products.ToListAsync();
    }
    
    public async Task<Product?> GetProductByIdAsync(int id)
    {
        // Also automatically filtered
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id);
    }
}

Generated SQL automatically includes tenant filter:

SELECT * FROM Products 
WHERE TenantId = 'tenant-123' AND Id = @p0

Automatic Tenant ID Injection

When saving new entities, the tenant ID is automatically set:

public async Task<Product> CreateProductAsync(Product product)
{
    // No need to manually set TenantId
    _context.Products.Add(product);
    await _context.SaveChangesAsync();
    
    // product.TenantId is now set automatically
    return product;
}

Change Tracking Protection

Prevents accidental or malicious tenant ID changes:

var product = await _context.Products.FindAsync(id);
product.TenantId = new TenantId("different-tenant"); // This will be ignored!

await _context.SaveChangesAsync(); // TenantId remains unchanged

Per-Tenant Databases

Using Tenant-Specific Connection Strings

using SaasSuite.EfCore.Implementations;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Register tenant store with connection strings
        services.AddSingleton<ITenantStore, MyTenantStore>();
        
        // Register DbContext factory
        services.AddSingleton<ITenantDbContextFactory<AppDbContext>, 
            TenantDbContextFactory<AppDbContext>>();
        
        services.AddSaasTenantDbContext<AppDbContext>(options =>
        {
            options.UsePerTenantModelCache = true; // Enable for per-tenant schemas
        });
    }
}

Accessing Tenant-Specific Context

public class MultiTenantDataService
{
    private readonly ITenantDbContextFactory<AppDbContext> _contextFactory;
    
    public MultiTenantDataService(
        ITenantDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }
    
    public async Task<List<Product>> GetProductsForTenantAsync(TenantId tenantId)
    {
        await using var context = await _contextFactory
            .CreateDbContextAsync(tenantId);
        
        return await context.Products.ToListAsync();
    }
}

Per-Tenant Schema Support

For scenarios where each tenant has a different database schema:

public class TenantModelCacheKeyFactory : IModelCacheKeyFactory
{
    private readonly ITenantAccessor _tenantAccessor;
    
    public TenantModelCacheKeyFactory(ITenantAccessor tenantAccessor)
    {
        _tenantAccessor = tenantAccessor;
    }
    
    public object Create(DbContext context, bool designTime)
    {
        if (designTime)
            return new object();
        
        var tenantId = _tenantAccessor.TenantContext?.TenantId.Value ?? "default";
        return (context.GetType(), tenantId);
    }
}

// Register the custom model cache key factory
services.AddSingleton<IModelCacheKeyFactory, TenantModelCacheKeyFactory>();

Configuration Options

public class EfCoreOptions
{
    // Automatically set TenantId on new entities
    public bool AutoSetTenantId { get; set; } = true;
    
    // Apply global query filters for tenant isolation
    public bool UseGlobalQueryFilter { get; set; } = true;
    
    // Enable per-tenant model caching (for different schemas)
    public bool UsePerTenantModelCache { get; set; } = false;
    
    // Default connection string (when tenant doesn't specify one)
    public string? DefaultConnectionString { get; set; }
}

Advanced Usage

Bypassing Tenant Filters

For admin operations that need to access all tenant data:

public async Task<List<Product>> GetAllProductsAcrossTenantsAsync()
{
    // Disable query filters
    return await _context.Products
        .IgnoreQueryFilters()
        .ToListAsync();
}

Manual Tenant Filtering

When working with non-tenant entities or complex queries:

public async Task<Order> GetOrderWithDetailsAsync(int orderId)
{
    var tenantId = _tenantAccessor.TenantContext.TenantId;
    
    return await _context.Orders
        .Where(o => o.TenantId == tenantId && o.Id == orderId)
        .Include(o => o.Items)
        .FirstOrDefaultAsync();
}

Migrations with Multi-Tenancy

// Run migrations for a specific tenant
public async Task MigrateTenantDatabaseAsync(TenantId tenantId)
{
    await using var context = await _contextFactory
        .CreateDbContextAsync(tenantId);
    
    await context.Database.MigrateAsync();
}

// Run migrations for all tenants
public async Task MigrateAllTenantsAsync()
{
    var tenants = await _tenantStore.GetAllAsync();
    
    foreach (var tenant in tenants)
    {
        await MigrateTenantDatabaseAsync(tenant.TenantId);
    }
}

Best Practices

  1. Always Use ITenantEntity: Mark all tenant-scoped entities with the interface
  2. Trust the Filters: Let automatic filtering handle tenant isolation
  3. Avoid Manual TenantId Checks: The query filters handle this for you
  4. Test Isolation: Verify queries don't return cross-tenant data
  5. Use IgnoreQueryFilters Sparingly: Only for admin operations
  6. Separate Shared Data: Use different entities for tenant-agnostic data

Testing

Create a test DbContext with in-memory database:

public class TestAppDbContext : AppDbContext
{
    public TestAppDbContext(
        DbContextOptions<AppDbContext> options,
        ITenantAccessor tenantAccessor) 
        : base(options, tenantAccessor)
    {
    }
    
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseInMemoryDatabase("TestDb");
        }
    }
}

// In tests
var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase(databaseName: "TestDatabase")
    .Options;

var mockTenantAccessor = Mock.Of<ITenantAccessor>(
    x => x.TenantContext == new TenantContext 
    { 
        TenantId = new TenantId("test-tenant") 
    });

var context = new AppDbContext(options, mockTenantAccessor);

License

This package is licensed under the Apache License 2.0. See the LICENSE file in the repository root for details.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
26.3.3.2 90 3/3/2026