I am writing a multi tenant application. Almost all tables have "AccountId" to specify which tenant owns the record. I have one table that holds a list of "Vendors" that all tenants have access to, it does not have AccountId.
Some tenants want to add custom fields onto a Vendor record.
How do I set this up in Code First Entity Framework? This is my solution so far but I have to fetch all favorite vendors since I can't write a sub-query in EF and then when I update the record, deletes are happening.
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's 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 I need to use a multi-column foreign key, but I'm having trouble setting this up.
Schema should look like the following..
Vendor
Id
FavVendor
VendorId
AccountId
CustomField1
Then I can query the vendor, get the FavVendor for the logged in account and go on my merry way.
My current solution, which gives me an extra "Vendor_Id" foreign key, but doesn't set it properly
This should be possible by setting up a "one to one" relationship and having the foreign key be "Vendor Id" and "Account Id"
Trying to get this setup in entity framework 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();
Also receiving the following error when I try and set foreign key on FavVendor.Vendor
FavVendor_Vendor_Source: : Multiplicity is not valid in Role 'FavVendor_Vendor_Source' in relationship 'FavVendor_Vendor'. Because the Dependent Role properties are not the key properties, the upper bound of the multiplicity of the Dependent Role must be '*'.
Tricky issue not naturally supported by EF. One of the cases where DTOs and projection provides you the required control. Still pure EF solution exists, but must be programmed very carefully. I'll try to cover as much aspects as I can.
Let start with what can't be done.
This should be possible by setting up a "one to one" relationship and having the foreign key be "Vendor Id" and "Account Id"
This is not possible. The physical (store) relationship is one-to-many (Vendor (one) to FavVendor (many)), although the logical relationship for a specific AccountId is one-to-one. But EF supports only physical relationships, so there is simply no way to represent the logical relationship, which additionally is dynamic.
Shortly, the relationship has to be one-to-many as in your initial 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 fetch all favorite vendors since I can't write a sub-query in EF and then when I update the record, deletes are happening.
Both aforementioned issues can be solved by wring a code in a special way.
First, since nether lazy nor eager loading supports filtering, the only remaining option is explicit loading (described in the Applying filters when explicitly loading related entities section of the documentation) or projection and rely on context navigation property fixup (which in fact explicit loading is based on). To avoid side effects, the lazy loading must be turned off for the involved entities (I already did that by removing virtual keyword from navigation properties) and also the data retrieval should always be through new short lived DbContext instances in order to eliminate the unintentional loading of related data caused by the same navigation property fixup feature which we rely on to do the filtering of FavVendors.
With that being said, here are some of the operations:
Retrieving Vendors with filtered FavVendors for specific AccountId:
For retrieving single vendor by Id:
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 generically, for vendors query (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 a Vendor and FavVendor for a specific AccountId from disconnected entity:
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 async version just use await on corresponding Async methods)
In order to prevent deleting unrelated FavVendors, you first load the Vendor with filtered FavVendors from database, then depending of the passed object FavVendors content either add new, update or delete the existing FavVendor record.
To recap, it's doable, but hard to implement and maintain (especially if you need to include Vendor and filtered FavVendors in a query returning some other entity referencing Vendor, because you cannot use the typical Include methods). You might consider trying some 3rd party packages like Entity Framework Plus
which with its Query Filter and Include Query Filter features could significantly simplify the querying part.
your focus is incorrect
instead of
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 the FavVendor for the logged in account and go on my merry way.
so each account has yours private vendors for that the relation should are between favVendor and TenantVendor
your queries so could be some 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's 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 sample 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 identity key i think is better but is your choose
First, it is difficult to answer the question the way it is asked. There are 2 questions in here, one is about custom fields the other is about Favorite vendor. Also I have to make an assumption that AccountId refers to the primary key of the Tenant; if so you could consider renaming AccountId to TenantId for consistency.
The first part about:
Some tenants want to add custom fields onto a Vendor record.
This depends on the extent of needing custom fields. Is this needed in other areas of the system. If so then this is one of the benefits of a NoSQL database like MongoDB. If the customized fields are just in this one area I would add a 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 about favorite vendors:
but I have to fetch all favorite vendors
I would really like to know more about the business requirement here. Is every Tenant required to have a favorite vendor? Can Tenants have more than one favorite vendor?
Depending on these answers Favorite could be a 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) ;
Related
I feel like I'm missing something obvious here; I'm using .Net 5 with Entity Framework Core. The problem is that the foreign key is correct, but the associated navigation property is always empty and has no data. Do I have to do something with the fluent framework, or do something special with my includes?
I have 3 simplified entities and a database context method in this example, the project is much too large to include entirely. In the method, CalendarEvents is a DbSet:
public class CalendarEvent: IJsonSerializable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// Gets the personnel associated with this event
/// </summary>
public virtual List<SchedulePerson> SchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class SchedulePerson : IJsonSerializable, ICloneable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int EmployeeId { get; set; }
public virtual Employee Employee { get; set; }
public virtual CalendarEvent AssociatedCalendarEvent { get; set; }
}
public class Employee : IJsonSerializable
{
[Key]
public int Id { get; set; }
public virtual List<SchedulePerson> AssociatedSchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class DbContext : DbContext
{
public DbSet<CalendarEvent> CalendarEvents { get; set; }
public DbSet<SchedulePerson> SchedulePeople { get; set; }
public DbSet<Employee> Employees { get; set; }
public DbContext(DbContextOptions<VibrationContext> options): base(options)
{
}
public CalendarEvent GetEvent(int calendarEventId)
{
var currentCalEvent = this.CalendarEvents.Where(x => x.Id == calendarEventId);
var dummy1 = currentCalEvent.FirstOrDefault();
var dummy2 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ToList();
var dummy3 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ThenInclude(people => people.Employee).ToList();
return currentCalEvent.FirstOrDefault();
}
}
In this case Employee is the associated navigation property, and the associated key EmployeeId, is correct. For the moment I've added extra logic that will populate each employee per schedule person separately, manually based on the foreign key EmployeeId, when I get the event, but I'd rather not have to add logic like that each time. If that's something unavoidable then that's fine, but I'd like to do things properly and let Entity Framework Core handle as much as possible.
For additional context:
SchedulePerson -> CalendarEvent is a many-to-one relationship
Employee -> SchedulePerson is a many-to-one relationship
In other words a calendar event can contain many schedule persons, but a schedule person can only be associated with one calendar event.
Employee can be associated with many schedule persons, but each schedule person is only associated with one employee.
There should only be one employee for each real person, but there can be multiple SchedulePersons for each real person.
Thank you for your help and let me know if there is any more information I can provide.
Also if anything else looks bad or wrong in these code snippets please let me know.
Edit, this is what I have to do if I want to get the employees in my request:
private void UpdateEmployeeContents(CalendarEvent calendarEvent)
{
foreach (SchedulePerson person in calendarEvent.SchedulePeople)
{
person.Employee = this.Employees.Where(x => x.Id == person.EmployeeId).FirstOrDefault();
}
}
This question already has answers here:
Create code first, many to many, with additional fields in association table
(7 answers)
Closed 2 years ago.
i m working on EF 6 (mapping with many to many relationship) ,see
https://www.codeproject.com/Articles/234606/Creating-a-Many-To-Many-Mapping-Using-Code-First
where it created "PersonCourses" as middle table ,now i have two problem with that In a many-to-many relationship EF manages the join table internally and hidden. It's a table without an Entity class in your model.
so what if i need to access "PersonCourses" in my code (project) and what if i need to add certain columns with it ??
EF can auto-manage the joining table so long as it just contains the two FKs as a composite PK. If you want to add to that then you need to declare the joining table as an entity with a one-to-many from each side.
So instead of:
[Table("Persons")]
public class Person
{
// ...
public virtual ICollection<Course> Courses { get; set; } = new List<Course>();
}
[Table("Courses")]
public class Course
{
// ...
public virtual ICollection<Person> People { get; set; } = new List<Person>();
}
modelBuilder.Entity<Person>()
.HasMany(x => x.Courses)
.WithMany(x => x.People);
you would have:
[Table("Persons")]
public class Person
{
// ...
public virtual ICollection<PersonCourse> PersonCourses { get; set; } = new List<PersonCourse>();
}
[Table("Courses")]
public class Course
{
// ...
public virtual ICollection<PersonCourse> PersonCourses { get; set; } = new List<PersonCourse>();
}
[Table("PersonCourses")]
public class PersonCourse
{
[Key, Column(Order=0), ForeignKey("Person")]
public int PersonId { get; set; }
[Key, Column(Order=1), ForeignKey("Course")]
public int CourseId { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
modelBuilder.Entity<Person>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Person);
modelBuilder.Entity<Course>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Course);
The main disadvantage of this is that Person no longer has a collection of Courses, but of PersonCourses so you have to dive the extra level in your projections every time you want to get details about course names. It might be tempting to leave the PersonCourses collection on Person named as "Courses" but I found that this can get misleading as you can end up with collections on some objects called Persons being Person vs. PersonCourse or Courses being Course vs. PersonCourse. It's generally less confusing when the collection name reflects the type.
So instead of:
var courses = context.Persons
.Where(x => x.PersonId == personId)
.SelectMany(x => x.Courses)
.ToList();
You need to change that to:
var courses = context.Persons
.Where(x => x.PersonId == personId)
.SelectMany(x => x.PersonCourses.Select(pc => pc.Course))
.ToList();
Update: To have an Id column on PersonCourse:
[Table("PersonCourses")]
public class PersonCourse
{
[Key]
public int Id { get; set; }
[ForeignKey("Person")]
public int PersonId { get; set; }
[ForeignKey("Course")]
public int CourseId { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
... or better, do away with the FK fields in the entity and map them via configuration:
[Table("PersonCourses")]
public class PersonCourse
{
[Key]
public int Id { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
EF6 may map these automatically by convention, but explicitly you can use:
modelBuilder.Entity<Person>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Person)
.Map(x => x.Mpakey("PersonId");
modelBuilder.Entity<Course>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Course)
.Map(x => x.Mpakey("CourseId");
I recommend this approach, like Shadow Properties with EF Core to avoid having 2 sources of truth for the Person ID or Course ID.
I.e. PersonCourse.PersonId vs. PersonCourse.Person.Id
When updating entities with navigation properties, you should update references via the navigation property, (personCourse.Course = newCourse) not via a FK property. (personCourse.CourseId = newCourseId) Doing so, or intermixing the source of truth for the FK can lead to weird results depending on what the DbContext is tracking at the time.
I have the following model classes in EF Core 2.2
public class User
{
[Key]
public long Id { get; set; }
[ForeignKey("Post")]
public long? PostId { get; set; }
public virtual Post Post { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
[Key]
public long Id { get; set; }
[ForeignKey("User")]
public long UserId { get; set; }
public virtual User User { get; set; }
}
I have checked the relations with SSMS and they are fine.
But when I use
dbContext.Posts.Include(p => p.User);
EF Core generates the following join statement
FROM Posts [p]
LEFT JOIN Users [p.Users] ON [p].[Id] = [p.Users].[PostId]
I'm including User from Post and expect it to be as below
FROM Posts [p]
LEFT JOIN Users [p.Users] ON [p].[UserId] = [p.Users].[Id]
What is wrong with models?
Assume that I want to save last PostId in User model.
Is there an attribute to tell ef core which property to use when joining models?
From the discussion on the other response it looks like you want a User to contain Posts, but then also have the User track a reference to the Latest post. EF can map this, however you will probably need to be a bit explicit about the relationships.
For instance:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
public virtual Post LatestPost { get; set; }
}
[Table("Posts")]
public class Post
{
[Key]
public int PostId { get; set; }
public string PostText { get; set; }
public DateTime PostedAt { get; set; }
public virtual User User { get; set; }
}
then a Configuration to ensure EF wires up the relationship between user and posts correctly:
// EF6
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
HasMany(x => x.Posts)
.WithRequired(x => x.User)
.Map(x=>x.MapKey("UserId"));
HasOptional(x => x.LatestPost)
.WithMany()
.Map(x=>x.MapKey("LatestPostId"));
}
}
// EFCore
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasMany(x => x.Posts)
.WithOne(x => x.User)
.HasForeignKey("UserId");
HasOne(x => x.LatestPost)
.WithMany()
.IsRequired(false)
.HasForeignKey("LatestPostId");
}
}
You can accomplish this in the OnModelCreating event with the modelBuilder reference as well. Note here I am not declaring FK properties in my entities. This too is an option, but I generally recommend not declaring FKs to avoid reference vs. FK update issues. I've named the LatestPost FK as LatestPostId just to reveal a bit more accurately what it is for. It could be mapped to a "PostId" if you so choose.
Now lets say I go to add a new post and I want to associate it to the user, and assign it as the LatestPost for that user:
using (var context = new SomethingDbContext())
{
var user = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 1);
var newPost = new Post { PostText = "Test", User = user };
user.Posts.Add(newPost);
user.LatestPost = newPost;
context.SaveChanges();
}
You can update the "latest" post reference by loading the user and setting the LatestPost reference to the desired post.
There is a risk with this structure however that you should consider. The issue is that there is no way to reliably enforce (at a data level) that the LatestPost reference in a User actually references a post associated to that user. For instance, if I have a latest post pointing to a particular post, then I delete that post reference from the user's Posts collection, that can result in FK constraint errors, or simply disassociate the post from the user, but the user latest post still points at that record. I can also assign another user's post to this user's latest post reference. I.e.
using (var context = new SomethingDbContext())
{
var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 1);
var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 2);
var newPost = new Post { PostText = "Test", User = user1 };
user1.Posts.Add(newPost);
user1.LatestPost = newPost;
user2.LatestPost = newPost;
context.SaveChanges();
}
And that would be perfectly fine. User 2's "LatestPostId" would be set to this new post, even though this post's UserId only refers to User1.
A better solution when dealing with something like a Latest post is to not denormalize the schema to accommodate it. Instead, use unmapped properties in the entity for the latest post, or better, rely on projection to retrieve this data when it's needed. In both cases you would remove the LatestPostId from the User table
Unmapped property:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
[NotMapped]
public Post LatestPost
{
get { return Posts.OrderByDescending(x => x.PostedAt).FirstOrDefault(); }
}
}
The caveat of the unmapped property approach is that you need to remember to eager-load Posts on the User if you want to access this property, otherwise you will trip a lazy load. You also cannot use this property in Linq expressions that get sent to EF (EF6) though they may work with EFCore, but risk performance issues if the expression gets translated to in-memory early. EF will not be able to translated LatestPost to SQL since there would be no key in the schema.
Projection:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}
Then if you want to retrieve a user and it's latest post:
var userAndPost = context.Users.Where(x => x.UserId == userId)
.Select(x => new { User = x, LatestPost = x.Posts.OrderByDescending(PostedAt).FirstOrDefault()} ).Single();
Projection with Select can retrieve entities of interest, or better, simply return the fields from those entities into a flattened view model or DTO to send to UI or such. This results in more efficient queries against the database. Using Select to retrieve the details you don't need to worry about eager-loading via Include, and when done correctly, will avoid pitfalls with lazy loading.
The relationship between User and Post is wrong. This should be your model:
public class User
{
[Key]
public long Id { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
[Key]
public long Id { get; set; }
[ForeignKey("User")]
public long UserId { get; set; }
public virtual User User { get; set; }
}
That is one-to-many relationship: one user can have many posts and a post can have just one user.
You had enough of answers on this question to how to create the model , I will just highlight what's wrong in your class
You have craete postid in user model as FK and therefore it will always consider it as relationship between user and post , you have to remove that and design it something similar to what is said by steve
In short; we need a cache, that cache has related entities. We also need to then store the expired items in a separate table for compliance and archival reasons.
Imagine the following four classes:
public abstract class Cache
{
public int Id { get; set; }
public Cache()
{
Entities = new HashSet<Entity>();
}
public ICollection<Entity> Entities { get; set; }
}
public class AvailableCache : Cache
{
}
public class ArchivedCache : Cache
{
}
public class Entity
{
public int Id { get; set; }
public int? CacheId { get; set; }
public AvailableCache { get; set; }
public ArchivedCache { get; set; }
}
Together with the following DbContext:
public partial class DbCacheContext : DbContext
{
public virtual DbSet<AvailableCache> { get; set; }
public virtual DbSet<ArchivedCache> { get; set; }
public virtual DbSet<Entity> Entities { get; set; }
protected override void OnModelCreatning(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ArchivedCache>(p =>
{
p.Property(p => p.Id)
.ValueGeneratedOnAdd();
p.HasMany(p => p.Entities).WithOne(p => p.ArchivedCache).HasForeignKey(p => p.CacheId);
}
modelBuilder.Entity<AvailableCache>(p =>
{
p.HasMany(p => p.Entites).WithOne(p => p.AvailableCache).HasForeignkey(p => p.CacheId);
}
}
A recurring job populates the cache, checks for expired items and archives them. First attempt a column was added to ArchivedCache
public class ArchivedCache : Cache
{
public int OriginalId { get; set; }
}
This was then used as to save the original ID to define the relationship toward Entity. Then I tried to remove that, adding the ValueGeneratedOnAdd() and give it the same ID as AvailableCache.
The issue is that when trying to insert an Entity the foreign key constraint won't allow it since it doesn't exist in that table.
Since AvailableCache and ArchivedCache basically same object I would like to keep the relationship simple toward Entity.
Currently as I see it I have three options, I don't like any of them:
Define both IDs on Entity (I don't like this option because I don't want to clutter Entity with two ID:s that in the end point toward the same object, just in different tables/times)
Skip the ArchivedCache relationship and let the DBA worry about it since he was the one that specifically requested this.
I could remove the abstract class, resulting in one table with a discriminator but the DBA insisted on two tables.
But before I do either I wanted to check if there's perhaps something in EF Core that would allow this.
I have two tables. Questions and UserProfile
public QuestionMap()
{
this.HasKey(t => t.QuestionId);
this.Property(t => t.Title)
.IsRequired()
.HasMaxLength(100);
this.Property(t => t.CreatedBy)
.IsRequired();
}
public UserProfileMap()
{
this.HasKey(t => t.UserId);
this.Property(t => t.UserName)
.IsRequired()
.HasMaxLength(56);
}
My repository call looks like this at the moment:
public virtual IQueryable<T> GetAll()
{
return DbSet;
}
Is there a way that I can change the repository call so that it will go to the database, join the Question and UserProfile tables and then bring back a list that has the UserName from the UserProfile table? I realize I may have to create another class (that includes UserName) for the return type and I can do that.
You can't configure property mapping to some field of joined entity. But you can create navigation properties in your Question and UserProfile entites, which will provide joined enitites:
public class Question
{
public int QuestionId { get; set; }
public string Title { get; set; }
public virtual UserProfile CreatedBy { get; set; }
}
public class UserProfile
{
public int UserId { get; set; }
public string UserName { get; set; }
public virtual ICollection<Question> Questions { get; set; }
}
And add mapping configuration to user mapping:
HasMany(u => u.Questions)
.WithRequired(q => q.CreatedBy)
.Map(m => m.MapKey("UserId"))
.WillCascadeOnDelete(true);
Now you will be able to eager load users when querying questions:
var questions = context.Questions.Include(q => q.CreatedBy);
foreach(var question in questions)
Console.WriteLine(question.CreatedBy.UserName);
You should make the join in your controller not your repository. The repository is supposed to deal with only one entity in your database.
Or you can create a View in your database, and add it as an entity to your entity framework model.
Actually, you CAN join tables using LINQ without navigation properties. Let me demonstrate:
public class Question
{
public int QuestionID { get; set; }
public string Title { get; set; }
public string TextBody { get; set; }
public int CreatedBy { get; set; }
}
public class UserProfile
{
[Key]
public int UserID { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
}
So that's the (very simple) model. Now, I'm not at all familiar with the IQueryable<T> GetAll() method shown in the OP's post, but I'm assuming that this method can be used to return an IQueryable<Question> that would, when executed, get all the Questions from the database. In my example, I'm just going to use MyDbContext instead, but this could be replaced with your other method if you prefer it...
So the LINQ join would be done as follows:
public void SomeMethod()
{
var results = MyDbContext.Questions.Join(MyDbContext.UserProfiles,
q => q.CreatedBy,
u => u.UserID,
(q,u) => new {
q.QuestionID,
q.Title,
q.TextBody,
u.UserName
});
foreach (var result in results)
{
Console.WriteLine("User {0} asked a question titled {1}:",
result.UserName, result.Title);
Console.WriteLine("\t{0}", result.TextBody)
}
}
Now, I do think that navigation properties are MUCH better for such obvious and common relationships. However, there are times when you might not want to add a navigation property for a relationship that is seldom referenced. In those cases, it might just be better to use the Join method.