EF 5 DbContext: Delete the undeletable - entity-framework

I'm using EF5 with DbContext and Database-First approach in a WPF application and ran into some logical problems during the deletion of entities and the usage of the required data annotation attribute in the following scenario:
The are two tables referencing each other with foreign keys without cascading delete:
|----A----| |----B----|
|ID int |<-| |ID int |
|---------| |-|A_ID int |
Thus, "A" can't be deleted if some "B" references "A".
The EF 5 model includes the associations and - as no cascading is set - OnDelete is set to "None" to both Ends of the associations. I've decorated both the "A_ID" field and the Navigation property "B.A" with a [Required] attribute - and here my trouble starts when I delete an "A" entity with "B" entities refrenencing this "A" entity:
MyContext.Set<A>().Remove(MyA);
MyContext.SaveChanges();
SaveChanges sets all navigation propoerties of "B" to the removed "A" entity to null. This invalidates "B" as the navigation propoerties have the Required-Attribute throwing an exception that "A" can not be deleted because "B" is invalid - which is somehow a strange reason.
However, after removing the Required-Attribute on the Navigation properties and leaving the Required-Attribute on the B.A_ID property the correct error is thrown.
Finally, after the database exception I end up with an object graph in which all naviagation properties of "A" are set to "null".
I think this is the intended behaviour of the EF but this leads to two problems:
Firstly, the delete Operation is not valid. I haven't found anything about a "Delete"-Validation. Data annotaions consider only property changes.
Secondly, how to recover removed entities after an exception as all navigation properties are set to "null". The EF 5 associations are not as precise as the SQL Server ones which allows "None", "Set to NULL" or "Cascade" on associations. In the case of "None" SQL Server throws an exceptions leaving all entities untouched.
If anybody ran into the same problems of a "CanDelete"-Validation and entity recovery after an database exception and has a solution or can point me to other related threads, please tell me.
Yours,
Marcus
Example Classes:
// Entity to delete
public partial class A {
public A() {
this.Bs = new HashSet<B>();
}
public int ID { get; set; }
public Nullable<int> C_ID { get; set; }
public string Name { get; set; }
public virtual C C { get; set; }
public virtual ICollection<B> Bs { get; set; }
}
// Child entities of A with foreign key constraint
public partial class B {
public int ID { get; set; }
public int A_ID { get; set; }
public string Name { get; set; }
public virtual A A { get; set; }
}
// Example class of an additional entity referencing A
public partial class C {
public C() {
this.As = new HashSet<A>();
}
public int ID { get; set; }
public string Name { get; set; }
public virtual ICollection<A> As { get; set; }
}
Database Schema:
CREATE DATABASE [EFABC]
GO
USE [EFABC]
GO
CREATE TABLE [dbo].[tA](
[ID] [int] IDENTITY(1,1) NOT NULL,
[C_ID] [int] NULL,
[Name] [nvarchar](50) NULL,
CONSTRAINT [PK_tA] PRIMARY KEY CLUSTERED
(
[ID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[tB](
[ID] [int] IDENTITY(1,1) NOT NULL,
[A_ID] [int] NOT NULL,
[Name] [nvarchar](50) NULL,
CONSTRAINT [PK_tB] PRIMARY KEY CLUSTERED
(
[ID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[tC](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Name] [nvarchar](50) NULL,
CONSTRAINT [PK_tC] PRIMARY KEY CLUSTERED
(
[ID] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
ALTER TABLE [dbo].[tA] WITH CHECK ADD CONSTRAINT [FK_tA_tC] FOREIGN KEY([C_ID])
REFERENCES [dbo].[tC] ([ID])
GO
ALTER TABLE [dbo].[tA] CHECK CONSTRAINT [FK_tA_tC]
GO
ALTER TABLE [dbo].[tB] WITH CHECK ADD CONSTRAINT [FK_tB_tA] FOREIGN KEY([A_ID])
REFERENCES [dbo].[tA] ([ID])
GO
ALTER TABLE [dbo].[tB] CHECK CONSTRAINT [FK_tB_tA]
GO

Simply delete all the B entities before you delete your A entity
[Update]
Since it is database first, the fact that your B Class contains
public int A_ID { get; set; }
makes me think that the relationship is not quite as you describe.
Try looking at in a database diagram.

As this behaviour is intended by the EF I used a workaround for WPF applications using DbContext. The problem occurs because I use a DbContext instance during the lifetime of a window. Thus, the object graph in the context has to be consistent until the window is closed. In the case of a failed delete operation whether because of foreign key constraints or because of a database connection failure the object graph is changed during the remove method.
As a workaround I have implemented the following methods:
Check foreign key constraints before the removal of entities.
As only the loaded object graph is affected only loaded entities have to be checked. This is not completely satisfying as the business logic has to implement this check independently from the EF which already knows all constraints.
Delete Entities in a separate DbContext
This way the original context isn’t changed if the deletion failed. This works fine for situations in which a window is equivalent to an deleted entity. In this case the window is closed if the delete operation succeded and held open with its original context if the delete operation fails. However, the situation in which the deletion of sub-entities fails isn’t covered with this method.
Finally, you could refresh the whole context after the failure of a delete operation but in this case you’re losing all changes in entities set before.

Related

EF Core: How to use foreign key in a PostgreSQL array

I have a PostgreSQL database with following tables:
create table tbl1(
id serial primary key,
...
)
create table tbl2(
id serial primary key,
tbl1_ids int[]
)
tbl2.tbl1_ids is a reference to the tbl1.id (I prefer not to declare it as foreign key, but I can do that if it solves my problem).
It works fine, and it's significantly more space efficient than using the traditional N-M relation.
However, I don't know how to declare an EF Core navigation property.
I'd expect something like this to work.
class Tbl2 {
// these two work fine
public int Id { get; set; }
public int[] Tbl1Ids { get; set; }
// I'd like to do this, so I can use it in queries
public Tbl1[] Tbl1s { get; set; }
}
EF apparently only recognizes navigation properties if the foreign key property ends with Id, but it also seems to expect that it has type int, not int[]. I'd expect I'll need to do some Fluent API configuration for this, but I could not figure this out yet.

EF Core set Id to Int.MinValue and try to insert in database

I am using EF Core and I have a problem when I save a new entity.
Here is my model class
[Column("Id")]
public int ID { get; set; }
[Required]
[Column("Pratica", TypeName = "varchar(10)")]
public string PRATICA { get; set; }
[Column("Anno")]
public int ANNO { get; set; }
[Required]
[Column("Variante", TypeName = "varchar(2)")]
public string VARIANTE { get; set; }
Here I create and initialize a new PRAT object:
var prat = new PRAT();
prat.PRATICA = "Prova";
prat.ANNO = 2000;
prat.VARIANTE = "0";
context.PRAT.Add(prat);
context.SaveChangesAsync();
Just after the context.PRAT.Add(prat) line if I check prat.ID member I get something like -2147482647
After context.SaveChangesAsync I get the error "Cannot insert explicit value for identity column in table 'Prat' when IDENTITY_INSERT is set to OFF"
This is the generated SQL statement:
INSERT INTO [Prat] ([Id], [Anno], [Pratica], [Variante]) VALUES (#p0, #p1, #p2, #p3);
As you can see the Id Field is added to the list of fields, but this field is Identity!
If, before context.SaveChangesAsync() I set
prat.ID = 0
the generated SQL Statement is
INSERT INTO [Prat] ([Anno], [Pratica], [Variante]) VALUES (#p0, #p1, #p2);
And all works fine.
Thank you.
I think you need to configure your model with the DatabaseGenerated attribute, or configure it with fluent api
...
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("Id")]
public int ID { get; set; }
...
The primary key property is of type int, by convention EF Core assumes that the database will use the SQL
IDENTITY command to create a unique key when a new row is added. So you must define your database column as identity column.
For anyone still dealing with this, the other answers are insufficient. Primary keys for ints, shorts, guids etc in EF core are automatically generated.
The DatabaseGeneratedOption.Identity is for columns that are not primary keys.
The real problem is that somewhere in your code (potentially your database seeder if you have one) is pushing entities with manually entered primary keys.
For example:
_context.Jobs.Add(
new Job()
{
JobId = 1,
Name = "Truck Driver",
},
);
_context.SaveChanges();
Doing so tells ef core that you will be supplying primary keys for that entity and it will not know how to generate them. I am unsure why this is because you would think ef core could just grab the max value primary key and add 1 but I think the PK value generation code under the hood is the same for all primary key datatypes (including guid where max value isn't a thing).
Anyways, remove the code where you are manually inserting primary keys and the Add functionality should work as expected.

Entity Framework 6, cascade delete of mapped join table

I have simple "graph" representation stored in database. There is Node entity which has it's ID, Label and list of adjacent nodes.
public class Node
{
[Key]
public int Id { get; set; }
[StringLength(128)]
public string Label { get; set; }
public virtual ICollection<Node> AdjacentNodes { get; set; }
}
Then in OnModelCreating on database context class, I have:
modelBuilder.Entity<Node>()
.HasMany(n => n.AdjacentNodes)
.WithMany()
.Map(n => n.MapLeftKey("From").MapRightKey("To").ToTable("NodeEdge"));
Now, when I want to delete any node which has already any relation, I get constraint error:
The DELETE statement conflicted with the REFERENCE constraint "FK_dbo.NodeEdge_dbo.Nodes_From". The conflict occurred in database "master", table "dbo.NodeEdge", column 'From'.
The statement has been terminated.
Join table looks like this:
CREATE TABLE [dbo].[NodeEdge] (
[From] INT NOT NULL,
[To] INT NOT NULL,
CONSTRAINT [PK_dbo.NodeEdge] PRIMARY KEY CLUSTERED ([From] ASC, [To] ASC),
CONSTRAINT [FK_dbo.NodeEdge_dbo.Nodes_From] FOREIGN KEY ([From]) REFERENCES [dbo].[Nodes] ([Id]),
CONSTRAINT [FK_dbo.NodeEdge_dbo.Nodes_To] FOREIGN KEY ([To]) REFERENCES [dbo].[Nodes] ([Id])
);
Is there any way how to add ON DELETE CASCADE on join table constraints - so it will clear up all references when I'm deleting nodes? (... and keeping model simple without having join table entity)
When the many-to-many association is a self reference, EF chooses to create the foreign keys as not cascading. If it would make both keys cascading there would be a circular cascade, which Sql Server doesn't allow. Apparently, EF doesn't want to choose for you which of both keys should be cascading, so it chooses none.
You can however add the ON DELETE CASCADE clause to the FK_dbo.NodeEdge_dbo.Nodes_From foreign key afterwards, or add it in the migration Up method.

Entity Framework 5 recursive relations

CREATE TABLE ConfigurationItem
(
OID BIGINT NOT NULL
,ParentItemOID BIGINT
);
ALTER TABLE ConfigurationItem ADD CONSTRAINT PK_CONFIGURATIONITEM PRIMARY KEY (OID);
ALTER TABLE ConfigurationItem ADD CONSTRAINT FK_CONFIGURATIONITEM_PARENTITEMOID FOREIGN KEY (ParentItemOID ) REFERENCES CONFIGURATIONITEM(OID);
Every time fetch data ConfigurationItem I would like to get
parent ConfigurationItem
and List of child ConfigurationItems
and no recursion.
This was the entity created
[Table("ConfigurationItem", Schema = "dbo")]
public partial class ConfigurationItem : TaggableItem
{
public Int64 OID { get; set; }
public Int64? ParentItemOID { get; set; }
[ForeignKey("ParentItemOID")]
public ConfigurationItem Parent;
[InverseProperty("ParentItemOID")]
//Not a virtual because it is need to be marshalled via WCF
public List<ConfigurationItem> Children { get; set; }
}
I can't make this to work.
Example following errors happen:
InnerException: System.Data.SqlClient.SqlException
HResult=-2146232060
Message=Invalid column name 'ConfigurationItem_OID'.
Source=.Net SqlClient Data Provider
ErrorCode=-2146232060
Class=16
LineNumber=32
Number=207
Procedure=""
Server=localhost
State=1
What would be the correct way to make this work in Entity framework?
I think your original exception stems from the default naming convention used by EF. Either rename the OID property in the ConfigurationItem class to Configuration_OID or use the Column annotation on the OID property to indicate that you want to override the default naming convention for that column, e.g. [Column("OID")].
I have a schema with a self-referential table like this and I haven't found it necessary to use the InverseProperty annotation, so you may be able to just get rid of it.

EF 4.1 Code First. Table-per-type inheritance with different primary key name from its base class' primary key name

Given this:
create table Location(
LocationId int identity(1,1) not null primary key,
Address nvarchar(max) not null,
City nvarchar(max) null,
State nvarchar(max) not null,
ZipCode nvarchar(max) not null
);
create table Park(
ParkId int not null primary key references Location(LocationId),
Name nvarchar(max) not null
);
I tried this mapping:
modelBuilder.Entity<Location>();
modelBuilder.Entity<Park>().ToTable("Park");
modelBuilder.Entity<Park>().Property(x => x.LocationId).HasColumnName("ParkId");
Unfortunately that didn't work.
using (var db = new Ef())
{
var park = new Park { Name = "11th Street Park", Address = "801 11th Street", City = "Aledo", State = "TX", ZipCode = "76106" };
db.Set<Location>().Add(park);
db.SaveChanges();
}
It has this error:
The property 'LocationId' is not a declared property on type 'Park'.
Verify that the property has not been explicitly excluded from the
model by using the Ignore method or NotMappedAttribute data
annotation. Make sure that it is a valid primitive property.
How should I map Park entity so its LocationId property fall to ParkId column?
I have this mapping by the way:
public class Location
{
public virtual int LocationId { get; set; }
public virtual string Address { get; set; }
public virtual string City { get; set; }
public virtual string State { get; set; }
public virtual string ZipCode { get; set; }
}
public class Park : Location
{
public virtual string Name { get; set; }
}
If it could help, this is possible in EF 4.0 (via designer), just followed the steps in Chapter 2-11 of Entity Framework 4.0 Recipes, Problem Solution Approach. Now I'm trying it on code first via EF 4.1
[EDIT]
If I change the ParkId to LocationId, things are ok. However, with designer approach, it is possible to map the LocationId to ParkId of table Park; I want to achieve the same thing with code first
create table Park(
LocationId int not null primary key references Location(LocationId),
Name nvarchar(max) not null
);
As I know (and I tried it multiple times) code first doesn't support this => your derived type should use same column names for primary key.
This problem can be described very simply: Current fluent mapping implementation doesn't allow overriding mapping rules from parent entity => parent entity defines names of primary key columns in all derived entities.
IMO the most probable reason is that it was really designed as code first where you don't have existing database and you do not have to bother with database naming - it was up to EF to define names as it needed. Once DbContext API was released people started to use it with existing database massively. But here comes a problem: Initial use cases didn't count with this so some scenarios which are pretty easily done in EDMX are not possible. This is one of them.
Here is a workaround for this issue:
Create a view for the derived table and map your entity class that view. Rename the key column in your view so that it matches the key column in the base table.
eg:
base table User (UserID, FirstName, LastName)
derived table Manager (ManagerID, DepartmentID)
Entity Framework fails to update Manager as the key column is different!
solution:
create view UserManager
as
select
ManagerID as UserID,
DepartmentID
from Manager
Then map the Manager class to the UserManager view, instead of to the Manager table.