Composite primary key
See original GitHub issueGreat 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:
- Created 3 years ago
- Comments:7 (3 by maintainers)
Top 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 >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
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.
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.