Entity Framework Multi Tenant Configure Shared Table

I am writing an application for several tenants. Almost all tables have an “AccountId” to indicate who owns the tenant. I have one table that contains a list of "sellers" to which all tenants have access, it does not have an account.

Some tenants want to add custom fields to a Vendor record.

How do I install this in the First Entity Framework? This is my solution so far, but I have to get all the selected providers, since I can not write a subquery in EF, and then when I update the record, the deletion occurs.

public class Vendor { public int Id { get;set;} public string Name { get; set; } } public class TenantVendor { public int AccountId { get;set;} public int VendorId{ get;set;} public string NickName { get; set; } } // query // how do I only get single vendor for tenant? var vendor = await DbContext.Vendors .Include(x => x.TenantVendors) .SingleAsync(x => x.Id == vendorId); // now filter tenant favorite vendor // problem: if I update this record later, it deletes all records != account.Id vendor.TenantVendors= vendor.FavoriteVendors .Where(x => x.AccountId == _account.Id) .ToList(); 

I know that I need to use a multi-column foreign key, but I am having trouble setting it up.

The scheme should look as follows.

 Vendor Id FavVendor VendorId AccountId CustomField1 

Then I can request a provider, get FavVendor for a registered account and continue my fun journey.

My current solution, which gives me an extra "Vendor_Id" foreign key, but doesn't set it correctly

This should be possible by setting up a one-to-one relationship and having the foreign key "Merchant ID" and "Account ID"

Trying to get this setting within an entity now ...

 public class Vendor { public int Id { get; set; } public string Name { get; set; } public virtual FavVendor FavVendor { get; set; } } public class FavVendor { public string NickName { get; set; } [Key, Column(Order = 0)] public int VendorId { get; set; } public Vendor Vendor { get; set; } [Key, Column(Order = 1)] public int AccountId { get; set; } public Account Account { get; set; } } // query to get data var dbVendorQuery = dbContext.Vendors .Include(x => x.FavVendor) .Where(x => x.FavVendor == null || x.FavVendor.AccountId == _account.Id) ; // insert record if (dbVendor.FavVendor == null) { dbVendor.FavVendor = new FavVendor() { Account = _account, }; } dbVendor.FavVendor.NickName = nickName; dbContext.SaveChanges(); 

enter image description here

The following error also appears when trying to install a foreign key on FavVendor.Vendor

FavVendor_Vendor_Source :: Multiplicity is not valid in the role "FavVendor_Vendor_Source" with respect to "FavVendor_Vendor". Since the properties of a dependent role are not key properties, the upper bound on the multiplicity of the dependent role must be "*".

+5
source share
3 answers

A difficult problem, naturally not supported by EF. One of the cases where DTO and projection provide the necessary control. There is still a clean EF solution, but it must be programmed very carefully. I will try to cover as many aspects as possible.

To begin with, what cannot be done.

This should be possible by setting up a one-to-one relationship and having the foreign key "Merchant ID" and "Account ID"

It's impossible. The physical (storage) relationship is one-to-many ( Vendor (one) to FavVendor (many)), although the logical relationship for a particular AccountId is one-to-one . But EF only supports physical relationships, so it is simply impossible to imagine a logical relationship that is additionally dynamic.

In short, the relationship should be one-to-many as in your original design. Here is the final model:

 public class Vendor { public int Id { get; set; } public string Name { get; set; } public ICollection<FavVendor> FavVendors { get; set; } } public class FavVendor { public string NickName { get; set; } [Key, Column(Order = 0)] public int VendorId { get; set; } public Vendor Vendor { get; set; } [Key, Column(Order = 1)] public int AccountId { get; set; } } 

This is my solution so far, but I have to get all the selected providers, since I can not write a subquery in EF, and then when I update the record, the deletion occurs.

Both of the above problems can be solved using special code.

Firstly, since full loading and loading do not support filtering, the only remaining option is to explicitly load (described in Applicable filters when explicitly loading related objects in the documentation) or projection and rely on fixing the context navigation property (which is based on explicit loading). To avoid side effects, lazy loading should be disabled for the objects involved (I already did this by removing the virtual from the navigation properties), and the data search should always consist of new short instances of DbContext to eliminate the unintended loading of related data caused by that the same navigation property correction function that we rely on to filter FavVendors .

With that said, here are some of the operations:

Retrieving providers with filtered FavVendors for a specific AccountId:

To obtain a separate provider by identifier:

 public static partial class VendorUtils { public static Vendor GetVendor(this DbContext db, int vendorId, int accountId) { var vendor = db.Set<Vendor>().Single(x => x.Id == vendorId); db.Entry(vendor).Collection(e => e.FavVendors).Query() .Where(e => e.AccountId == accountId) .Load(); return vendor; } public static async Task<Vendor> GetVendorAsync(this DbContext db, int vendorId, int accountId) { var vendor = await db.Set<Vendor>().SingleAsync(x => x.Id == vendorId); await db.Entry(vendor).Collection(e => e.FavVendors).Query() .Where(e => e.AccountId == accountId) .LoadAsync(); return vendor; } } 

or more in general, for supplier requests (with filtering, ordering, paging, etc. already applied):

 public static partial class VendorUtils { public static IEnumerable<Vendor> WithFavVendor(this IQueryable<Vendor> vendorQuery, int accountId) { var vendors = vendorQuery.ToList(); vendorQuery.SelectMany(v => v.FavVendors) .Where(fv => fv.AccountId == accountId) .Load(); return vendors; } public static async Task<IEnumerable<Vendor>> WithFavVendorAsync(this IQueryable<Vendor> vendorQuery, int accountId) { var vendors = await vendorQuery.ToListAsync(); await vendorQuery.SelectMany(v => v.FavVendors) .Where(fv => fv.AccountId == accountId) .LoadAsync(); return vendors; } } 

Updating provider and FavVendor for a specific AccountId from a disconnected object:

 public static partial class VendorUtils { public static void UpdateVendor(this DbContext db, Vendor vendor, int accountId) { var dbVendor = db.GetVendor(vendor.Id, accountId); db.Entry(dbVendor).CurrentValues.SetValues(vendor); var favVendor = vendor.FavVendors.FirstOrDefault(e => e.AccountId == accountId); var dbFavVendor = dbVendor.FavVendors.FirstOrDefault(e => e.AccountId == accountId); if (favVendor != null) { if (dbFavVendor != null) db.Entry(dbFavVendor).CurrentValues.SetValues(favVendor); else dbVendor.FavVendors.Add(favVendor); } else if (dbFavVendor != null) dbVendor.FavVendors.Remove(dbFavVendor); db.SaveChanges(); } } 

(For the asynchronous version, just use await for the appropriate Async methods)

To prevent the removal of unrelated FavVendors , you first load the Vendor with the filtered FavVendors from the database, and then depending on the contents of the FavVendors object FavVendors either add a new one, update or delete the existing FavVendor entry.

Recall that this is doable, but difficult to implement and maintain (especially if you need to enable Vendor and filter FavVendors in a query that returns some other object referencing Vendor , because you cannot use the typical Include ). You might want to try some third-party packages, such as Entity Framework Plus , which, with the help of the Query Filter and Include Query Filter, can greatly simplify the part of the query.

+3
source

your focus is wrong

instead

  Vendor TenantVendor One to many Vendor FavVendor One to many Account FavVendor One to many 

I think it should be

  Vendor TenantVendor OK TenantVendor FavVendor One to many 

in your comment

Get FavVendor for a registered account and continue my fun journey.

therefore, each account has your private providers, as the relationship should be between favVendor and TenantVendor

Your requests may look like

 // query // how do I only get single vendor for tenant? var vendor = DbContext.TenantVendor .Include(x => x.Vendor) .Where(x => x.VendorId == [your vendor id]) .SingleOrDefault(); // now filter tenant favorite vendor // problem: if I update this record later, it deletes all records != account.Id vendor.TenantVendors= DbContext.FavVendor .Where(x => x.TenantVendor.AccountId = [account id]) .ToList(); 

Here is an example EntityFramework map

 public class Vendor { public int Id { get; set; } public string Name { get; set; } } public class TenantVendor { public int Id {get; set; public int AccountId { get;set;} public int VendorId{ get;set;} public virtual Vendor Vendor {get;set;} public string NickName { get; set; } } public class FavVendor { public int Id { get;set; } public string NickName { get; set; } public int TenantVendorId { get; set; } public virtual TenantVendor TenantVendor { get; set; } } 

In dbcontext

 .... protected override void OnModelCreating(DbModelBuilder builder) { builder.Entity<Vendor>() .HasKey(t => t.Id) .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); builder.Entity<TenantVendor>() .HasKey(t => t.Id) .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); builder.Entity<TenantVendor>() .HasRequired(me => me.Vendor) .WithMany() .HasForeignKey(me => me.VendorId) .WillCascadeOnDelete(false); builder.Entity<FavVendor>() .HasKey(t => t.Id) .Property(p => p.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity); builder.Entity<FavVendor>() .HasRequired(me => me.TenantVendor) .WithMany() .HasForeignKey(me => me.TenantVendorId) .WillCascadeOnDelete(false); } .. 

I changed your composite key to an identification key, I think it’s better, but your choice

0
source

Firstly, it is difficult to answer the question of how it is asked. There are two questions here, one about custom fields, and the other about a select provider. Also I have to make an assumption that AccountId refers to the tenant's primary key; if so, you might consider renaming AccountId to TenantId for consistency.

The first part is about:

Some tenants want to add custom fields to a Vendor record.

It depends on the degree of need for custom fields. This is necessary in other areas of the system. If so, this is one of the benefits of a NoSQL database such as MongoDB. If the configured fields are only in this area, I would add the TenantVendorCustomField table:

 public class TenantVendorCustomField { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id {get; set;} public int AccountId { get;set;} public int VendorId{ get;set;} public string FieldName { get; set; } public string Value {get; set; } [ForeignKey("AccountId")] public virtual Tenant Tenant { get; set; } [ForeignKey("VendorId")] public virtual Vendor Vendor { get; set; } } 

The next part is about your favorite sellers:

but I have to get all the selected suppliers

I would love to know more about the business requirement here. Does each tenant need to have a favorite seller? Can tenants have more than one favorite supplier?

Depending on these answers, the Favorites may be the property of TenantVendor:

 public class TenantVendor { public int AccountId { get;set;} public int VendorId{ get;set;} public string NickName { get; set; } public bool Favorite {get; set;} } var dbVendorQuery = dbContext.TenantVendors .Include(x => x.Vendor) .Where(x => x.TenantVendor.Favorite && x.TenantVendor.AccountId == _account.Id) ; 
0
source

Source: https://habr.com/ru/post/1265183/


All Articles