I am creating a new Blazor server side application with EF Core 6. I am more used to using database first and Dapper. However I am trying to get more familiar with EF Core and code first approach as requested by the department.
While reviewing the documentation on the changes for EF Core 6 in relation to the many to many relationships, I am stuck on stuck on how to configure the relationships properly for what I call a nested many to many. I believe I have the basic concept of the base 2 tables with many to many but this is a little more complex. I have searched MS and SO but only found some articles that are close but not fitting my situation.
The situation is this, for example 1 table Applications, 1 table Environments and a table Databases. There are an application can have multiple environments at which point the combination of an application(s) / environment(s) can have multiple databases.
Here is how the tables were designed.
CREATE TABLE [dbo].[App] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] VARCHAR (100) NOT NULL,
CONSTRAINT [PK_Applications] PRIMARY KEY CLUSTERED ([Id] ASC))
CREATE TABLE [dbo].[Env] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Name] VARCHAR (80) NOT NULL,
[Value] VARCHAR (100) NOT NULL,
CONSTRAINT [PK_Env] PRIMARY KEY CLUSTERED ([Id] ASC))
CREATE TABLE [crosswalk].[App_Env] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[AppId] INT NOT NULL,
[EnvId] INT NOT NULL,
CONSTRAINT [PK_App_Env] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_App_Env_App] FOREIGN KEY ([AppId]) REFERENCES [dbo].[App] ([Id]),
CONSTRAINT [FK_App_Env_EnvId] FOREIGN KEY ([EnvId]) REFERENCES [dbo].[Env] ([Id]))
CREATE TABLE [crosswalk].[App_Env_Db] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[AppEnvtId] INT NOT NULL,
[DbId] INT NOT NULL,
CONSTRAINT [PK_App_Env_Db] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_App_Env_Db_App_Env] FOREIGN KEY ([AppEnvtId]) REFERENCES [crosswalk].[App_Env] ([Id]),
CONSTRAINT [FK_App_Env_Db] FOREIGN KEY ([DbId]) REFERENCES [dbo].[Db] ([Id]))
The application is not complete so not able to run it. Was able to come up with one of the configurations but not sure on how to code the rest.
entity.HasMany(d => d.Envs)
.WithMany(p => p.Apps)
.UsingEntity<Dictionary<string, object>>(
"AppEnv",
l => l.HasOne<Env>().WithMany().HasForeignKey("AppId"),
r => r.HasOne<App>().WithMany().HasForeignKey("EnvId"),
j =>
{
j.HasKey("EnvId", "AppId");
j.ToTable("Crosswalk.App_Env");
j.HasIndex(new[] { "EnvId" }, "IX_AppEnv_EnvId");
j.HasIndex(new[] { "AppId" }, "IX_AppEnv_AppId");
});
The models are as follows:
[Table("App")]
public partial class App
{
public App() => Envs = new HashSet<Env>();
[Key]
public int Id { get; set; }
[Required]
[StringLength(100)]
[Unicode(false)]
public string Name { get; set; }
[InverseProperty("App")]
public virtual ICollection<LookupCode> Envs { get; set; }
}
[Table("Env")]
public partial class LookupCode
{
public LookupCode() => Apps = new HashSet<Apps>();
[Key]
public int Id { get; set; }
[Required]
[StringLength(80)]
[Unicode(false)]
public string Name { get; set; }
[Required]
[StringLength(100)]
[Unicode(false)]
public string Value { get; set; }
public int SortOrder { get; set; }
[InverseProperty("Env")]
public virtual ICollection<App> Apps { get; set; }
}
[Table("Db")]
public partial class Db
{
public Db() => AppEnvDbs = new HashSet<AppEnvDb>();
[Key]
public int Id { get; set; }
[Required]
[StringLength(50)]
[Unicode(false)]
public string Name { get; set; }
[InverseProperty("Db")]
public virtual ICollection<AppEnvDb> AppEnvDbs { get; set; }
}
If this is not possible I can go back to creating the DTO's for the link tables and manually adding/creating/deleting the link records.
EDIT: Added missing table definition for Db. Although the DTO model was shown I forgot to include the data table.
CREATE TABLE [dbo].[Db] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[DbServerId] INT NOT NULL,
[Name] VARCHAR (50) NOT NULL,
CONSTRAINT [PK_Db] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Related
I'm trying to make a one-to-many relationship with a composite primary key:
public class Bom
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
public string Name { get; set; }
}
public class ChildReference
{
[Key]
public int Id { get; set; }
public string BomId { get; set; } // Should be the foreign key from the bom-table
public ICollection<Bom> Boms { get; set; }
}
......
builder.Entity<ChildReference>().HasKey(t => new { t.Id, t.BomId });
When I run this, Entity Framework Core creates two columns in the Bom-table called ChildReferenceBomId and ChildReferenceId. I don't want that. I want it to only create one column caled ChildReferenceId that should be the foreign key to the ChildReference table.
The reason why I want to create a composite primary key inside the ChildReference table is because I want to add rows to the table like this:
INSERT INTO ChildReference(Id, BomId) VALUES(1, '1')
INSERT INTO ChildReference(Id, BomId) VALUES(1, '2')
I'm not sure if I'm doing this the right way. Can anyone help me?
EDIT:
I basically want to do the following with entity framework core:
CREATE TABLE [dbo].BOMChildren (
[BOMChildId] [int] NOT NULL,
[BOMId] [int] NOT NULL,
CONSTRAINT [PK_BOMChildId_BOMId] PRIMARY KEY CLUSTERED
(
[BOMChildId] ASC,
[BOMId] ASC
))
CREATE TABLE [dbo].BOM (
[BOMId] [int] PRIMARY KEY IDENTITY(1,1) NOT NULL,
[BOMPartId] [nvarchar](64) NOT NULL,
[Qty] [int] NOT NULL,
[UnitOfMeasure] [nvarchar](32),
[ParentId] [int] NULL,
[ChildReference] [int] NULL,
[BOMItemDataId] [int]
)
ALTER TABLE [dbo].[BOMChildren]
ADD CONSTRAINT [FK_BOMChildren_BOM]
FOREIGN KEY([BOMId]) REFERENCES [dbo].[BOM] ([BOMId])
GO
ALTER TABLE dbo.Bom
ADD CONSTRAINT FK_Bom_BomChild
FOREIGN KEY(ChildReference, BOMId) REFERENCES [dbo].BOMChildren([BOMChildId], [BOMId])
Anyone who can push me in the right direction?
You can use set it up like this:
public class Bom
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public string Id { get; set; }
public string Name { get; set; }
public int ChildReferenceId { get; set; }
public ChildReference CurrentChildReference{ get; set; }
}
public class ChildReference
{
[Key]
public int Id { get; set; }
public ICollection<Bom> Boms { get; set; }
}
You can after that configure a one-to-many relationship for the above entities using Fluent API by overriding the OnModelCreating method in the context class, as shown here:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// configures one-to-many relationship
modelBuilder.Entity<Bom>()
.HasRequired<ChildReference>(s => s.CurrentChildReference)
.WithMany(g => g.Boms)
.HasForeignKey<int>(s => s.ChildReferenceId);
}
You can use the Create/Alter SQL statements that you posted above to create the tables within your database.
After that you can do a Reverse Engineer Model to generate EF code first DbContext based on your existing Database tables. You can follow the steps within chapter 3 (https://learn.microsoft.com/en-us/ef/ef6/modeling/code-first/workflows/existing-database)
I'm having an issue with EF Core automatically adding foreign keys while I didn't ask for it.
So basically I'm using code first migrations in C# with SQL Server, some models and the basic context down below. But as you can see with my example models I'm adding a dependency from Event -> Item, one to many, what I want is many to many. What I think is happening is that EFCore is switching the foreign key dependencies around? I had linktables before like EventItems.
Am I required to use the linktables again or is there an other way to fix this?
Event model
namespace Models
{
public class Event
{
[Required]
public int Id { get; set; }
public string Description { get; set; }
[Required]
public DateTime StartDate { get; set; }
[Required]
public DateTime EndDate { get; set; }
public ICollection<Item> Rewards { get; set; }
}
}
Event SQL
CREATE TABLE [dbo].[Events] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Description] NVARCHAR (MAX) NULL,
[StartDate] DATETIME2 (7) NOT NULL,
[EndDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_Events] PRIMARY KEY CLUSTERED ([Id] ASC)
);
Item model
namespace Models
{
public class Item
{
[Required]
public int Id { get; set; }
[Required]
public string Path { get; set; }
}
}
Item SQL
CREATE TABLE [dbo].[Items] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[EventId] INT NULL,
[Path] NVARCHAR (MAX) DEFAULT (N'') NOT NULL,
[QuestId] INT NULL,
CONSTRAINT [PK_Items] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Items_Events_EventId] FOREIGN KEY ([EventId]) REFERENCES [dbo].[Events] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_Items_EventId]
ON [dbo].[Items]([EventId] ASC);
Edited
I have 2 tables, Nurse and Person. Person has a 1 to 0 or 1 relationship to Nurse. In other words some Persons in my application will also be a Nurse. I would like to use the same primary key in both tables. When I run my code querying the Nurse table, I end up with a "System.Data.Entity.Core.EntityCommandExecutionException", and then on the inner exception: "SqlException: Invalid column name 'Person_Id'."
My Nurse model does not have a "Person_Id" field in it. The generated query seems to have added it. I must be missing something in my model definition, as removing "public virtual Person Person { get; set; }" makes the query generate correctly. EF seems to get hung up on the fact that the PK is Id in the Person table, but NurseId in the Nurse table.
I've found multiple places on the internet that show you how to map this with Fluent API, but nothing that matches my case. Because most Persons in my application are not Nurse, I don't want to have to reference the Nurse model in my Person model, which takes away the solution I keep seeing online:
//won't work because I am trying to no have a reference to Nurse in my Person model. So i.Nurse below doesn't exist
modelBuilder.Entity<Nurse>().HasRequired(i => i.Person).WithOptional(i => i.Nurse)
The query EF generates:
{SELECT
1 AS [C1],
[Extent1].[NurseId] AS [NurseId],
[Extent1].[CredentialId] AS [CredentialId],
[Extent1].[DisciplineId] AS [DisciplineId],
[Extent1].[UnitId] AS [UnitId],
[Extent1].[YearsOfService] AS [YearsOfService],
[Extent1].[Person_Id] AS [Person_Id]
FROM [dbo].[Nurse] AS [Extent1]}
Here are the tables:
CREATE TABLE [dbo].[Person]
(
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
CONSTRAINT [PK__Person] PRIMARY KEY CLUSTERED ([Id] ASC)
)
CREATE TABLE [dbo].[Nurse]
(
[NurseId] BIGINT NOT NULL,
CONSTRAINT [PK_Nurse] PRIMARY KEY ([NurseId]),
CONSTRAINT [FK_Nurse_Person] FOREIGN KEY ([PersonId]) REFERENCES [dbo].[Person] ([Id])
)
And the Entities:
public class Person
{
[Key]
public long Id { get; set; }
}
public class Nurse
{
[Key]
public long NurseId { get; set; }
public long? CredentialId { get; set; }
public long? DisciplineId { get; set; }
public long? UnitId { get; set; }
public int? YearsOfService { get; set; }
[ForeignKey("PersonId")]
public virtual Person Person { get; set; }
[ForeignKey("CredentialId")]
public virtual Credential Credential { get; set; }
[ForeignKey("DisciplineId")]
public virtual Discipline Discipline { get; set; }
[ForeignKey("UnitId")]
public virtual Unit Unit { get; set; }
}
ApplicationDBContext:
public class ApplicationDBContext : DbContext
{
public ApplicationDBContext() : base("ApplicationDbContext")
{
}
public DbSet<Nurse> Nurses { get; set; }
public DbSet<Person> People { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Nurse>().HasKey(n => n.NurseId);
}
}
Is it possible to do this, or do I need to reference Nurse in my Person table and go with what I've seen online?
I think your problem is that you are using PersonId has the primary key in Nurse. Try giving Nurse its own Id. It is a good idea for every table to have its own Ids. EF has issues with tables or views that do not have a unique primary key.
CREATE TABLE [dbo].[Person]
(
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
CONSTRAINT [PK__Person] PRIMARY KEY CLUSTERED ([Id] ASC)
)
CREATE TABLE [dbo].[Nurse]
(
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
[PersonId] BIGINT unique NOT NULL,
CONSTRAINT [PK_Nurse] PRIMARY KEY ([Id]),
CONSTRAINT [FK_Nurse_Person] FOREIGN KEY ([PersonId]) REFERENCES [dbo].[Person] ([Id])
)
For those that stumble across this. I found the answer here. The fluent api I posted was close, it should have been:
modelBuilder.Entity<Nurse>().HasRequired(n => n.Person).WithOptional();
I have 3 entities; Groups, Scopes, Vlans
What I'm trying to accomplish:
A Group can have many Scopes
A Group can have many Vlans
A Scope must have a Group
A Vlan must have a Group
A Scope can have a Vlan
A Vlan can have a Scope
So kind of a soft relationship between vlans and scopes.
When not setting vlan or scope as a requirement EF complains about not knowing the principal of the relationship, So how do I fix this soft one to one relationship?
My Models:
public class Group
{
public Group()
{
Scopes = new HashSet<Scope>();
Vlans = new HashSet<VLAN>();
}
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public ICollection<VLAN> Vlans { get; set; }
public ICollection<Scope> Scopes { get; set; }
}
public class Scope
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public virtual VLAN Vlan { get; set; }
public virtual Group Group { get; set; }
}
public class VLAN
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public virtual Scope Scope { get; set; }
[Required]
public virtual Group Group { get; set; }
}
There is a similar problem on SO, related to bidirectional Zero-to-One relationships:
Implementing Zero Or One to Zero Or One relationship in EF Code first by Fluent API
I've tried the same approach on your schema and it works.
See my FluentAPI configuration below:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<VLAN>()
.HasOptional(vlan => vlan.Scope)
.WithOptionalPrincipal();
modelBuilder.Entity<VLAN>()
.HasRequired(vlan => vlan.Group);
modelBuilder.Entity<Scope>()
.HasOptional(scope => scope.Vlan)
.WithOptionalPrincipal();
modelBuilder.Entity<Scope>()
.HasRequired(scope => scope.Group);
modelBuilder.Entity<Group>()
.HasMany(group => group.Scopes);
modelBuilder.Entity<Group>()
.HasMany(group => group.Vlans);
}
To be sure, that it suites your needs, see the generated SQL for VLANs and Scopes tables:
CREATE TABLE [dbo].[Scopes] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[VLAN_Id] INT NULL,
[Group_Id] INT NOT NULL,
CONSTRAINT [PK_dbo.Scopes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.Scopes_dbo.VLANs_VLAN_Id] FOREIGN KEY ([VLAN_Id]) REFERENCES [dbo].[VLANs] ([Id]),
CONSTRAINT [FK_dbo.Scopes_dbo.Groups_Group_Id] FOREIGN KEY ([Group_Id]) REFERENCES [dbo].[Groups] ([Id]) ON DELETE CASCADE);
CREATE TABLE [dbo].[VLANs] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[Scope_Id] INT NULL,
[Group_Id] INT NOT NULL,
CONSTRAINT [PK_dbo.VLANs] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_dbo.VLANs_dbo.Scopes_Scope_Id] FOREIGN KEY ([Scope_Id]) REFERENCES [dbo].[Scopes] ([Id]),
CONSTRAINT [FK_dbo.VLANs_dbo.Groups_Group_Id] FOREIGN KEY ([Group_Id]) REFERENCES [dbo].[Groups] ([Id]) ON DELETE CASCADE);
I have a Base class, a Derived class and Derived has a collection of Items.
I want to configure EF to delete Items when their parent Derived is deleted.
The following minimal (LinqPad) example shows how I'm trying to achieve that, but it doesn't generate the on delete cascade part, just regular FKs.
I tried [Required] attribute - didn't work.
How do I make it add delete cascade option to the FK specification?
[System.ComponentModel.DataAnnotations.Schema.Table("Bases")]
public class Base
{
public int Id {get;set;}
public string Name {get; set;}
}
[System.ComponentModel.DataAnnotations.Schema.Table("Derived")]
public class Derived : Base
{
public virtual ICollection<Item> Items {get;set;}
public Derived()
{
Items = new HashSet<Item>();
}
}
public class Item
{
public int Id {get;set;}
public int ParentId {get;set;}
public Derived Parent {get;set;}
}
public class TestDbContext : System.Data.Entity.DbContext
{
public System.Data.Entity.DbSet<Base> Bases { get; set; }
public System.Data.Entity.DbSet<Derived> Derived { get; set; }
public System.Data.Entity.DbSet<Item> Items { get; set; }
protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder)
{
System.Data.Entity.Database.SetInitializer<TestDbContext>(null);
modelBuilder.Entity<Item>().HasRequired(x=>x.Parent).WithMany(x=>x.Items).HasForeignKey(x=>x.ParentId).WillCascadeOnDelete(true);
}
}
void Main()
{
var ctx = new TestDbContext();
var ddl = (ctx as System.Data.Entity.Infrastructure.IObjectContextAdapter).ObjectContext.CreateDatabaseScript();
Console.WriteLine(ddl);
}
This is the DDL it generates:
create table [dbo].[Bases] (
[Id] [int] not null identity,
[Name] [nvarchar](max) null,
primary key ([Id])
);
create table [dbo].[Derived] (
[Id] [int] not null,
primary key ([Id])
);
create table [dbo].[Items] (
[Id] [int] not null identity,
[ParentId] [int] not null,
primary key ([Id])
);
alter table [dbo].[Derived] add constraint [Derived_TypeConstraint_From_Base_To_Derived] foreign key ([Id]) references [dbo].[Bases]([Id]);
alter table [dbo].[Items] add constraint [Item_Parent] foreign key ([ParentId]) references [dbo].[Derived]([Id]);