EF Core one to many to many to one relationship - data-annotations

I've worked with databases for a long time now but am new to Entity Framework. I handle both the aspects of programming and database development. As a db developer, I try to keep it clean so this structure that I came up with works well for me but I'm not sure if Entity Framework even supports it for I've tried for several days, using different scenarios, Data Annotations as well as Fluent API but couldn't get this to work.
What I'm trying to do might be a bit unconventional but what I'm trying to avoid is having to duplicate a file table for each area hence I define 1 file table that can be used by multiple areas using a Relationship. Thus, what I have is: one [company, employee, or project] can have many files (one to many). Similarly, the file table can be sourced by any area (many to many, in this case, it's not the data but rather the structure, hopefully that makes sense). The file records are related to only 1 area [company, employee, or project] (many to one).
The obvious advantage to this method is that I can avoiding having to manage 3 file tables but it doesn't end there. As you can see from the FileAccess table, instead of having multiple tables here or multiple fields to represent pointers to the multiple tables, I only need to manage 1 table for file access. The key is in the RelationTable and RelationId rather than the specific File.Id.
Below is a simplified example of the structure I'm trying to accomplish. Can it be done in Entity Framework?
public class Company
{
public Guid Id { get; set; }
public string Name { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class Project
{
public int Id { get; set; }
public Guid? CompanyId { get; set; }
public string ProjectNo {get; set; }
public virtual ICollection<File> Files { get; set; }
}
public class File
{
public int Id { get; set; }
public Int16 RelationTable { get; set; } 0=Company, 1=Employee, 2=Project
public string RelationId { get; set; } Company.Id, Employee.Id, Project.Id
public string FileName { get; set; }
}
public class FileAccess
{
public int Id { get; set; }
public int EmployeeId { get; set; }
public Int16 RelationTable { get; set; } 0=Company, 1=Employee, 2=Project
public string RelationId { get; set; } Company.Id, Employee.Id, Project.Id
public string AccessType
}

As Ivan pointed out, EF doesn't support this due to the foreign key limitations but I was able to come up with a working solution. However, I must warn you that I'm only on my 3rd week of EF so I don't know what ramifications this may cause but this is what I did, for those who may be interested.
As it turns out (through trial and error), EF just needs the OnModelCreating to wire up the relationship between the objects, it doesn't really need the FK to be created thus I defined the relationship this way:
modelBuilder.Entity<File>()
.HasIndex(k => new { k.RelationTable, k.RelationId }); //for performance
modelBuilder.Entity<FileAccess>()
.HasMany(fa => fa.Files)
.WithOne(f => f.FileAccess)
.HasForeignKey(k => new { k.RelationTable, k.RelationId })
.HasPrincipalKey(k => new { k.RelationTable, k.RelationId });
//Using enumerations to control
relationships and adding PERSISTED so it doesn't need to be maintained plus it
won't break the Add-Migration with the "non persistent error"
modelBuilder.Entity<Project>()
.Property(f => f.RelationTable)
.HasComputedColumnSql((int)NTGE.Database.Shared.eFileRelTable.Projects + " PERSISTED") //This injects the value so we don't have to store it
.HasDefaultValue(123); //This doesn't really matter, we just need it so EF doesn't try to insert a value when saving, which will cause an error
modelBuilder.Entity<Project>()
.HasMany(p => p.Files)
.WithOne(f => f.Project)
.HasForeignKey(k => new { k.RelationTable, k.RelationId })
.HasPrincipalKey(k => new { k.RelationTable, k.Id });
When you add the above codes and run the Add-Migration, it'll cause it to add the below codes, which will break the Update-Database command so you'll need to comment it out in the Up function.
//migrationBuilder.AddForeignKey(
// name: "FK_Files_Projects_RelationTable_RelationId",
// table: "Files",
// columns: new[] { "RelationTable", "RelationId" },
// principalTable: "Projects",
// principalColumns: new[] { "RelationTable", "Id" },
// onDelete: ReferentialAction.Cascade);
//migrationBuilder.AddForeignKey(
// name: "FK_Files_FileAccess_RelationTable_RelationId",
// table: "Files",
// columns: new[] { "RelationTable", "RelationId" },
// principalTable: "FileAccess",
// principalColumns: new[] { "RelationTable", "RelationId" },
// onDelete: ReferentialAction.Cascade);
You'll need to do the same with the Down function else you won't be able to roll back your changes.
//migrationBuilder.DropForeignKey(
// name: "FK_Files_Projects_RelationTable_RelationId",
// table: "Files");
//migrationBuilder.DropForeignKey(
// name: "FK_Files_FileAccess_RelationTable_RelationId",
// table: "Files");
Now you can do an Update-Database and it should run just fine. Running the app works perfectly fine as well. I'm able to use the EF method to get the Project with the associated files and worked with the FileAccess object as well. However, keep in mind that this is a hack and future versions of EF might not support it. Cheers!

Related

Cannot create foreign key constraint on self-joining Many-to-Many relationship

I have created the following classes:
public class Character
{
public int ID { get; set; }
public string Title { get; set; }
public ICollection<Relationship> RelatedTo { get; set; }
public ICollection<Relationship> RelatedFrom { get; set; }
}
public class Relationship
{
public int ToID { get; set; }
public int FromID { get; set; }
public Character CharacterFrom { get; set; }
public Character CharacterTo { get; set; }
public string Details { get; set; }
}
In my Context I have this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Relationship>()
.HasKey(r => new { r.ToID, r.FromID });
modelBuilder.Entity<Relationship>()
.HasOne(r => r.CharacterFrom)
.WithMany(r => r.RelatedTo)
.HasForeignKey(r => r.FromID)
.OnDelete(DeleteBehavior.ClientSetNull);
modelBuilder.Entity<Relationship>()
.HasOne(r => r.CharacterTo)
.WithMany(r => r.RelatedFrom)
.HasForeignKey(r => r.ToID)
.OnDelete(DeleteBehavior.ClientSetNull);
}
I think that it is right but I cannot apply the migration due to the following error:
Cannot create the foreign key "FK_Relationship_Character_FromID" with the SET NULL referential action, because one or more referencing columns are not nullable.
I've tried every combination of DeleteBehaviour for OnDelete. None of them work. I don't believe I can make the ICollections nullable and it doesn't seem right that I'd want to. I've spent two hours on this searching for answers. Every tutorial or explanation on EF Core that I've tried to follow seems to take a slightly different approach and be subtly incompatible with every other one. Please help!
The error is telling you that you cannot use DeleteBehavior.ClientSetNull (or DeleteBehavior.SetNull) because the corresponding FK property is not nullable - both ToID and FromID are of type int, hence does not allow setting to null (neither client nor server).
To turn off the cascade delete (in order to break the multiple cascade paths I guess) for required FK relationships, use DeleteBehavior.Restrict instead.

Code migration unexpectedly tries to rename table

I want to implement a change log as advised in
Dev Express XAF T474899
I am using the security system generated by the XAF new solution wizard
I have defined some business objects to store the change log information.
One of these objects stores a link to the user
public virtual User User { get; set; }
On generating the code migration I am surprised to see the Up() method add the following
RenameTable(name: "dbo.UserRoles", newName: "RoleUsers");
DropPrimaryKey("dbo.RoleUsers");
AddPrimaryKey("dbo.RoleUsers", new[] { "Role_ID", "User_ID" });
On another occasion I found the following in an Up()
RenameTable(name: "dbo.EventResources", newName: "ResourceEvents");
// lots of other stuff
DropPrimaryKey("dbo.ResourceEvents");
AddPrimaryKey("dbo.ResourceEvents", new[] { "Resource_Key", "Event_ID" });
On both occasions the code that creates the entities is a Dev Express libary.
I have cross posted this question to Dev Express Support
The Dev Express business objects are defined in DevExpress.Persistent.BaseImpl.EF;
My DbContext context refers to them as
public DbSet<Role> Roles { get; set; }
public DbSet<User> Users { get; set; }
The meta data for Role shows
The meta data for User shows
My own business classes contain
namespace SBD.JobTalk.Module.BusinessObjects
{
[NavigationItem("Configuration")]
[DisplayName("Staff")]
[DefaultProperty("Summary")]
[ImageName("BO_Employee")]
[Table("Staff")]
public class Staff : BasicBo
{
public Staff()
{
Person = new Person();
}
public virtual Person Person { get; set; }
[StringLength(100, ErrorMessage = "The field cannot exceed 100 characters. ")]
[scds.Index("IX_Staff_UserName", 1, IsUnique = true)]
public string UserName { get; set; }
[NotMapped]
public string Summary => $"{Person.FirstName} {Person.LastName}";
//public virtual User User { get; set; }
}
}
public abstract class BasicBo : IXafEntityObject
{
[Browsable(false)]
[Key]
public virtual int Id { get; set; }
public virtual void OnCreated()
{
}
public virtual void OnSaving()
{
}
public virtual void OnLoaded()
{
}
}
If I un-comment the code to have the User property inside Staff, and generate a migration, the migration Up is
public override void Up()
{
RenameTable(name: "dbo.UserRoles", newName: "RoleUsers");
DropPrimaryKey("dbo.RoleUsers");
AddColumn("dbo.Staff", "User_ID", c => c.Int());
AddPrimaryKey("dbo.RoleUsers", new[] { "Role_ID", "User_ID" });
CreateIndex("dbo.Staff", "User_ID");
AddForeignKey("dbo.Staff", "User_ID", "dbo.Users", "ID");
}
[Update]
Interestingly there are more Dev Express tables than I first thought.
The primary keys are Identity.
I think am using Standard Authentication created before Dev Express added the Allow/Deny ability (V16.1)
[Update]
When I create a new project with the above settings, here is the DbContext.
using System;
using System.Data;
using System.Linq;
using System.Data.Entity;
using System.Data.Common;
using System.Data.Entity.Core.Objects;
using System.Data.Entity.Infrastructure;
using System.ComponentModel;
using DevExpress.ExpressApp.EF.Updating;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
namespace XafApplication1.Module.BusinessObjects {
public class XafApplication1DbContext : DbContext {
public XafApplication1DbContext(String connectionString)
: base(connectionString) {
}
public XafApplication1DbContext(DbConnection connection)
: base(connection, false) {
}
public XafApplication1DbContext()
: base("name=ConnectionString") {
}
public DbSet<ModuleInfo> ModulesInfo { get; set; }
public DbSet<PermissionPolicyRole> Roles { get; set; }
public DbSet<PermissionPolicyTypePermissionObject> TypePermissionObjects { get; set; }
public DbSet<PermissionPolicyUser> Users { get; set; }
public DbSet<ModelDifference> ModelDifferences { get; set; }
public DbSet<ModelDifferenceAspect> ModelDifferenceAspects { get; set; }
}
}
OK, I will take a stab :) Your Up() code is trying to rename the table UserRoles to RoleUsers. This means you have a prior migration where UserRoles was the table name - probably from your DevEx stuff. This could happen if they changed their models in an upgrade. The current models are expecting RoleUsers etc. so you need to get there.
So first option is let the migration do the renaming to match the underlying model. I assume this didn't work or causes other issues?
You might be able to 'fool' entity framework into using the old tables with fluent code or annotations, but if it has new columns or relationships that won't work.
What I would do is this:
1) Create a new test project with the same references you had and
copy your context and DbSets. Point the connection string to a
new database.
2) Add a migration and script it out:
update-database -Script.
3) Examine this script a use it to create
the objects needed in your database. Migrate data from the old
tables to new if needed.
4) Remove the old tables
5) In your actual
project add a migration to resync your models:
add-migration SyncDevExUpdate -IgnoreChange, update-database
Now you will have the tables your models expect.

Entity framework - Define connection where neither side is required

I have a problem, I have to create a model where we have two entities which CAN be linked together but can also exist as stand alone entities.
The model currently looks like this:
public class Authorisation
{
public int AuthorisationID { get; set; }
public virtual Change Change { get; set; }
}
public class Change
{
public int ChangeID{ get; set; }
public virtual Authorisation Authorisation { get; set; }
public int? AuthorisationID{ get; set; }
}
The reason for this is that we can have an authorization record independent of a change, and some changes require authorisation and some dont, so neither side of the relationship is required.
I can configure this with the fluent API like so:
modelBuilder.Entity<Authorisation>()
.HasOptional(t => t.Change)
.WithOptionalPrincipal(t => t.Authorisation);
And alls well, Except that the migration that it creates looks like this
CreateTable(
"dbo.Changes",
c => new
{
ChangeID = c.Int(nullable: false, identity: true),
AuthorisationID = c.Int(),
Authorisation_AuthorisationID = c.Int(),
})
.PrimaryKey(t => t.ChangeID)
.ForeignKey("dbo.Authorisations", t => t.Authorisation_AuthorisationID)
.Index(t => t.Authorisation_AuthorisationID);
EF is deciding that its going to add a new column (Authorisation_AuthorisationID) for me to use as the FK between the two entities, what I really want to be able to do is to use the change.AuthorisationID property as the FK onto the Authorisation, I cannot find a way to configure this at all (Please note that I need the FK to be in the model - for consistency with the rest of the app more than anything else).
To sum up I need to be able to create a relationship between two entities where both sides of the relationship are optional and if possible I want to be able to define the FK column myself.
Am I just approaching this wrong? Ive been staring at the same block of code for so long I could be missing something simple.
Looks like explicit foreign key property is not supported for one-to-one relationships - there is no HasForeignKey Fluent API and also if you put ForeignKey attribute on the navigation property you get exception saying that multiplicity must be *.
So the only choice you have is to remove the explicit Change.AuthorisationID property and work only with navigation properties:
Model:
public class Authorisation
{
public int AuthorisationID { get; set; }
public virtual Change Change { get; set; }
}
public class Change
{
public int ChangeID{ get; set; }
public virtual Authorisation Authorisation { get; set; }
}
Configuration:
modelBuilder.Entity<Authorisation>()
.HasOptional(t => t.Change)
.WithOptionalPrincipal(t => t.Authorisation)
.Map(a => a.MapKey("AuthorisationID"));

MultiTenant Application Prevent Tenant Access Data from Other Tenant in Shared Database

I’m working on a tenant application and i was wondering how i can block tenant access other tenant data.
First, let me expose some facts:
The app is not free, 100% for sure the malicious user is a client.
All the primary keys/identity are integers (Guid solve this problem but we can't change right now).
The app use shared database and shared schema.
All the tenants are business group wich own several shops.
I'm use Forgery...
I have some remote data chosen by dropdown and its easy change the id's and acess data from other tenants, if you have a little knowledge you can f*ck other tenants data.
The first thing i think was check every remote field but this is kind annoying...
So i build a solution compatible with Code First Migrations using Model Convention and Composite Keys, few tested, working as expected.
Here's the solution:
Convention Class
public class TenantSharedDatabaseSharedSchemaConvention<T> : Convention where T : class
{
public Expression<Func<T, object>> PrimaryKey { get; private set; }
public Expression<Func<T, object>> TenantKey { get; private set; }
public TenantSharedDatabaseSharedSchemaConvention(Expression<Func<T, object>> primaryKey, Expression<Func<T, object>> tenantKey)
{
this.PrimaryKey = primaryKey;
this.TenantKey = tenantKey;
base.Types<T>().Configure(m =>
{
var indexName = string.Format("IX_{0}_{1}", "Id", "CompanyId");
m.Property(this.PrimaryKey).IsKey().HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity).HasColumnOrder(0).HasColumnAnnotation("Index", new IndexAnnotation(new[] {
new IndexAttribute(indexName, 0) { IsUnique = true }
}));
m.Property(this.TenantKey).IsKey().HasDatabaseGeneratedOption(DatabaseGeneratedOption.None).HasColumnOrder(1).HasColumnAnnotation("Index", new IndexAnnotation(new[] {
new IndexAttribute(indexName, 1) { IsUnique = true }
}));
});
}
}
Convetion Registration:
** On convention register i pass two properties, first the primary key and second is the tenant id.
modelBuilder.Conventions.Add(new TenantSharedDatabaseSharedSchemaConvention<BaseEntity>(m => m.Id, m => m.CompanyId));
Base Entity Model
public class BaseEntity
{
public int Id { get; set; }
public int CompanyId { get; set; }
public Company Company { get; set; }
}
Order Entity (Example)
** Here i reference the currency and client with company and all work as expected...
public class Order : BaseEntity
{
[Required]
public int CurrencyId { get; set; }
[ForeignKey("CompanyId, CurrencyId")]
public virtual Currency Currency { get; set; }
[Required]
public int ClientId { get; set; }
[ForeignKey("CompanyId, ClientId")]
public virtual Client Client { get; set; }
public string Description { get; set; }
}
Is there any impact on performance?
Is there any disadvantage compared to check every remote field?
Someone have the same idea and/or problem and came with another solution?
IMHO, anywhere in the application, you will be having a mapping that states that user x is entitled to manage or access tenant(s) a(,b). In your businesses layer you should check it the user is ever entitled to see the data using the forged ID. In your case, the forged I'd will belong to another tenant that the user does not have access to, so you will return an unauthorized / security violation exception.

Fluent NHibernate Joined References Ignoring Cascade Rule

I'm using Fluent NHibernate in an attempt to improve testability and maintainability on a web application that is using a legacy database and I'm having some trouble mapping this structure properly:
I have two tables that really represent one entity in the domain, and so I'm using a join to map them as such, and a third table that represents a second entity.
DB Tables:
[eACCT]
ACCT_ID
ACCT_NAME
[eREPORT_TYPE]
ACCT_ID
REPORT_NO
[eREPORT_TYPE_DESC]
REPORT_NO
REPORT_TYPE
Entities:
public class Account
{
public virtual string AccountID { get; set; }
public virtual string AccountName { get; set; }
public virtual ReportType ReportType { get; set; }
}
public class ReportType
{
public virtual int Number { get; set; }
public virtual string Type { get; set; }
}
Mapping:
public AccountMap()
{
Table("eACCT");
Id(x => x.AccountID, "ACCT_ID");
Map(x => x.AccountName, "ACCT_NAME");
Join("eREPORT_TYPE", m =>
{
m.KeyColumn("ACCT_ID");
m.References(x => x.ReportType)
.Cascade.None()
.Column("REPORT_NO");
});
}
public ReportTypeMap()
{
Table("eREPORT_TYPE_DESC");
Id(x => x.Number)
.Column("REPORT_NO")
.GeneratedBy.Assigned();
Map(x => x.Type, "REPORT_TYPE");
}
This works fine for my Gets, but when I modify Account.ReportType.Number and then SaveOrUpdate() Account, I get the error: 'identifier of an instance of DataTest.Model.ReportType was altered from (old_value) to (new_value)'.
All I want to do is modify Account's reference to ReportType and I thought that by setting the Cascade.None() property on the reference to ReportType, NHibernate wouldn't attempt to save the ReportType instance as well, but I must be misunderstanding how that works. I've tried making ReportType ReadOnly(), making the reference to ReportType ReadOnly(), etc and nothing seems to help.
Any ideas?
Finally solved this problem. Turns out I wasn't thinking about this in an NHibernate way. In my mind I had a new ReportType.Number, so that's what I needed to update. In reality, what I needed to do was get the ReportType with the new ReportType.Number and set the Account.ReportType. Doing it this way worked as expected.

Resources