question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Composite primary key

See original GitHub issue

Great work on this library. I’ve been playing around with it and EFCore… I’m curious what your opinion is using a composite primary key of TenantId and Id in the AppDbContext? I had to use the fluent API extensively for this. The LocalTenant record is populated to the DB as part of provisioning a new tenant in the CatalogDbContext.

Adding ValueGeneratedOnAdd to the LocalTenant.cs TenantId kept EFCore from throwing System.InvalidOperationException: Unable to track an entity of type 'Currency' because primary key property 'TenantId' is null. when trying to Add and SaveChanges for a Currency record.

Startup.cs

// ...
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMultiTenant()
        .WithEFCoreStore<CatalogDbContext>()
        .WithStaticStrategy("initech"); // TODO

    // Register the db context, but do not specify a provider/connection string since
    // these vary by tenant.
    services.AddDbContext<AppDbContext>();
}
// ...

CatalogDbContext.cs

public class CatalogDbContext : EFCoreStoreDbContext
{
    public CatalogDbContext(DbContextOptions<CatalogDbContext>  options) : base(options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=Data/Catalog.db"); // TODO change me to a real DB
        base.OnConfiguring(optionsBuilder);
    }
}

AppDbContext.cs

public class AppDbContext : MultiTenantDbContext
{
    public AppDbContext(TenantInfo tenantInfo) : base(tenantInfo)
    {
    }

    public AppDbContext(TenantInfo tenantInfo, DbContextOptions<AppDbContext> options) : base(tenantInfo, options)
    {
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        if (!optionsBuilder.IsConfigured)
        {
            optionsBuilder.UseSqlite(ConnectionString); // TODO change me to real DB
        }

        base.OnConfiguring(optionsBuilder);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
        
        base.OnModelCreating(modelBuilder);
    }

    public DbSet<LocalTenant> LocalTenants { get; set; }
    public DbSet<Currency> Currencies { get; set; }
}

AppDbContextFactory.cs

/// <summary>
/// The AppDbContext requires a tenant to function
/// Use a design-time factory and a dummy tenant to get around this for EF Migrations
/// https://docs.microsoft.com/en-us/ef/core/miscellaneous/cli/dbcontext-creation#from-a-design-time-factory
/// </summary>
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    public AppDbContext CreateDbContext(string[] args)
    {
        var dummy = new TenantInfo(
            "dummy", "dummy", "Dummy", "Data Source=Data/Seed.db", null
        );
        return new AppDbContext(dummy);
    }
}

Currency.cs

public class Currency
{
    public string TenantId { get; set; }
    public string Id { get; set; }
    public string NaturalId { get; set; }
    public string DisplayName { get; set; }
}

public class CurrencyEntityTypeConfiguration : IEntityTypeConfiguration<Currency>
{
    public void Configure(EntityTypeBuilder<Currency> builder)
    {
        builder
            .ToTable("currency");
        
        #region Primary Key and Indexes
        builder
            .IsMultiTenant()
            .HasKey(c => new {c.TenantId, c.Id});

        builder
            .HasIndex(c => new {c.TenantId, c.NaturalId})
            .IsUnique();
        #endregion

        #region Columns

        builder
            .Property(c => c.TenantId)
            .HasColumnName("tenant_id");
        builder
            .HasOne<LocalTenant>()
            .WithMany()
            .HasForeignKey(c => c.TenantId);
        builder
            .Property(c => c.Id)
            .HasColumnName("id");
        builder
            .Property(c => c.NaturalId)
            .HasColumnName("natural_id");
        builder
            .Property(c => c.DisplayName)
            .HasColumnName("display_name");
        #endregion
    }
}

LocalTenant.cs

public class LocalTenant
{
    public string TenantId { get; set; }
    public string NaturalId { get; set; }
    public string DisplayName { get; set; }
    public bool Active { get; set; }
    public bool Enabled { get; set; }
}

public class LocalTenantEntityTypeConfiguration : IEntityTypeConfiguration<LocalTenant>
{
    public void Configure(EntityTypeBuilder<LocalTenant> builder)
    {
        builder
            .ToTable("local_tenant");
        
        #region Primary Key and Indexes
        builder
            .IsMultiTenant()
            .HasKey(t => t.TenantId);
        
        builder
            .HasIndex(t => t.NaturalId)
            .IsUnique();
        #endregion

        #region Columns

        builder
            .Property(t => t.TenantId)
            .HasColumnName("tenant_id")
            .ValueGeneratedOnAdd(); // This was needed
        builder
            .Property(t => t.NaturalId)
            .HasColumnName("natural_id");
        builder
            .Property(t => t.DisplayName)
            .HasColumnName("display_name");
        builder
            .Property(t => t.Active)
            .HasColumnName("active");
        builder
            .Property(t => t.Enabled)
            .HasColumnName("enabled");
        #endregion
    }
}

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:7 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
dirrajcommented, Mar 18, 2023

Hey @jimmymcpeter and anyone else stumbling upon this.

Three years later I’ve got to say I also struggled using Finbuckle and composite keys.

I initially adopted a strict Id+TenantId composite key policy for all entities, trying to use it as an extra layer of protection and prevent data leaks.

builder.Entity<EntityBase>().IsMultiTenant().AdjustKey(builder.Entity<EntityBase>().Metadata.GetKeys().First(), builder).AdjustIndexes();

But trying to add a new entity using the navigation of another entity, I’ve went in circles to work around the two different problems which arise: Tenant cannot but null vs FK cannot be changed.

This is having used or not used Guid.Empty.ToString() as default TenantId in ctor. And used or not used TenantNotSetMode.Overwrite and TenantMismatchMode.Overwrite.

Of course setting the TenantId explicitly worked perfectly well but I didn’t want to do this.

In the end I’ve decided to give up the composite key and use merely indexes instead, knowing that Finbuckle will still use the shadow property in it’s query filtering.

0reactions
jimmymcpetercommented, Mar 30, 2020

No problem! I have some other things I’ve found I will probably put in a separate issue for your comments. My local_tenant table is for keeping referential integrity of whatever tenants are on that specific database.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Composite Key in SQL
Now a composite key is also a primary key, but the difference is that it is made by the combination of more than...
Read more >
Composite Key in SQL: Your Ultimate Guide to Mastery
A composite key in SQL can be defined as a combination of multiple columns, and these columns are used to identify all the...
Read more >
How can I define a composite primary key in SQL?
Good answer. Just to clarify, QuestionID and MemberID are not separate primary keys but rather the combination of them form a unique pair/tuple....
Read more >
SQL COMPOSITE KEY
A primary key that is made by the combination of more than one attribute is known as a composite key. In other words...
Read more >
What is the Composite Primary Key?
What is the Composite Primary Key? · Composite : means a combination of multiple components. · primary key: means a key that can...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found