Dapper insert into table that has a composite PK - c#

I have a table that has a primary key composed of two columns, neither of which are auto-incrementing, and my Dapper insert (part of Dapper Extensions) is failing on the insert saying that the first of the two columns does not allow a null, even tho the value I'm passing in is not null.
Table Student:
StudentId (PK, not null) \_ (combined to form primary key)
StudentName (PK, not null) /
Active -- just another column
C#:
public class Student {
public int StudentId { get; set; }
public string StudentName { get; set; }
public bool Active { get; set; }
}
var newStudent = new Student { StudentId = 5, StudentName = "Joe", Active = true };
var insertSuccess = myConn.Insert<Student>(newStudent);
Error:
Cannot insert the value NULL into column 'StudentId', table 'dbo.Student'; column does not allow nulls. INSERT fails.
Dapper is for some reason not getting the StudentId with a value of 5. Do I have to do something special for tables that have combined PK's, or with tables that have PK's that are not auto-incrementing? Thanks.

Adding an AutoClassMapper will change the behavior for all classes. If you wish to handle just this one class you can create a Map for just this class.
public class StudentClassMapper : ClassMapper<Student>
{
public StudentClassMapper()
{
Map(x => x.StudentId).Key(KeyType.Assigned);
Map(x => x.StudentName).Key(KeyType.Assigned);
AutoMap(); // <-- Maps the unmapped columns
}
}

Dapper.Contrib offers an annotation to solve this problem.
public class Student {
[ExplicitKey]
public int StudentId { get; set; }
[ExplicitKey]
public string StudentName { get; set; }
public bool Active { get; set; }
}
ExplicitKey means it is a key field whose value you must specify; it is not auto-generated by the database.
I'm assuming when you said "Dapper Extensions," you were referring to a different extension library. You may find you can easily switch over to Dapper.Contrib.

I'm not sure this is the problem, but AFAIK Dapper Extensions doesn't support composite primary keys by default.
You will probably have to code your own AutoClassMapper: https://github.com/tmsmith/Dapper-Extensions/wiki/AutoClassMapper
The default AutoClassMapper makes certain assumptions about your database schema and POCOs:
AutoClassMapper assumes that your table names are singular (Ex: Car table name and Car POCO name).
Each POCO has at least one property named Id or ends with Id.
If multiple properties end with Id, Dapper Extensions will use the first Id property as the primary key.
If the Id property is determined to be an Integer, the KeyType will be set to Identity.
If the Id property is determined to be a Guid, the KeyType will be set to Guid.
If the id property is anything other than an Integer our Guid, the KeyType will be set to Assigned.

Related

Entity Add/Insert with column equal to identity primary key value

During an insert/add can I make a different column equal to the newly valued primary key that's an identity auto generated value all in one write/save process? I know I can grab after fact and change but that's extra database hits I'm trying to avoid.
public class myDataTableRec
{
public int Id { get; set; } //This is an Identity Primary Key
public string Name { get; set; }
public int PostId { get; set; } //Want this the same as Id when it gets generated
}
myrec = new myDataTableRec;
db.myDataTable.Add(myrec);
db.SaveChanges();
From https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.dbcontext.add?view=efcore-5.0
"Add(TEntity)
Begins tracking the given entity, and any other reachable entities that are not already being tracked, in the Added state such that they will be inserted into the database when SaveChanges() is called.
Use State to set the state of only a single entity."
Since it tracks the entity, you can do the following:
myrec=new myDataTableRec;
db.myDataTable.Add(myrec);
myrec.PostId = myrec.Id
db.SaveChanges();
Edit: If it creates the Id after the Save, the you can always create your own Id (Guid.NewGuid()) and don't let EF generate it.
You have to set the property of StoreGeneratedPattern to identity and then you'll be able to achieve this
myrec = new myDataTableRec;
db.myDataTable.Add(myrec);
db.SaveChanges();
var id = myrec.Id;
In SQL it's not possible to insert an entity and set its generated primary in a foreign key column to itself in one atomic operation. That's only possible if the primary key is not generated, i.e. not an identity column.
However, just as EF is capable of inserting related entities in one operation and setting generated key values in foreign keys on the fly, one might argue that EF could support setting a parent to itself.
Take this entity class (replacing your placeholder name and enhancing it with navigation properties):
class Post
{
public int ID { get; private set; }
public string Name { get; set; }
public int? ParentPostID { get; private set; }
public Post ParentPost { get; set; }
public ICollection<Post> ChildPosts { get; private set; }
}
EF could have chosen to support this scenario:
using var db = new MyContext();
var root = new Post { Name = "Root" };
root.ParentPost = root;
db.Set<Post>().Add(root);
But it doesn't. It tries to insert the entity with a ParentPostID equal to the temporary (negative) ID value. Obviously, that's a FK violation.
To do this cleanly you have to add a transaction and set & save the self reference separately:
using var db = new MyContext();
var root = new Post { Name = "Root" };
db.Set<Post>().Add(root);
using var ts = new TransactionScope();
db.SaveChanges();
root.ParentPost = root;
db.SaveChanges();
ts.Complete();
Note that the parent id has to be nullable. Also note that the navigation property allows setting the parent without ever knowing its key value. Some people like to do these things in a DDD style.

Entity Framework creates non-existing column in query

I face the problem that EF creates a column in the query that does not exist in the Oracle database table.
The simplified model which is created by EF looks like this (I use DB first approach):
public partial class USER
{
public int ID { get; set; }
public string NAME { get; set; }
public int PROCESS_ID { get; set; }
public virtual PROCESS PROCESS { get; set; }
}
public partial class PROCESS
{
public PROCESS()
{
this.USER = new HashSet<User>();
}
public int ID { get; set; }
public virtual ICollection<USER> USER { get; set; }
}
I set up the foreign key constraint in the oracle sql developer.
When I try to get the Users for a selected Process like this:
var users = context.Users.Where(u => u.PROCESS_ID == 0);
It produces following error:
ORA-00904: "Extent1"."R1": invalid ID
So i took a look on the produced SQL:
SELECT
"Extent1".ID,
"Extent1".NAME,
"Extent1".R1,
FROM DB.USER "Extent1"
WHERE "Extent1".R1 = :p__linq__0
Of course this produces an error because R1 isn't a column in the table. But I can't figure out where it comes from. It seems like EF can't map the foreign key properly thats why it's also missing in the generated SQL query?
Maybe someone has a tip for me :)
To follow up my comment, here is a link to the conventions.
The convention for a foreign key is that it must have the same data type as the principal entity's primary key property and the name must follow one of these patterns:
[navigation property name][principal primary key property name]Id
[principal class name][primary key property name]Id
[principal primary key property name]Id
Your convention [navigation property name]_ID isn't on the list.
Encountered the same error recently while working with Oracle using DevArt provider. Turned out it was caused by a column name being longer than 30 chars. OP mentioned that the model posted in his question is a simplified one so it still may be the case.

Exclude primary key in model in .NET Core as SQL generates PK when INSERT

I have a simple table Tags in my SQL Server database:
Tags
(
ID INT NOT NULL PRIMARY KEY,
Name VARCHAR(255)
)
So for now I want to have the corresponding model in my .NET Core Web API:
Tags.cs:
public partial class Tags
{
public string Name { get; set; }
public Tags(string name)
{
Name = name;
}
}
If I am right, SQL Server should autoincrement the ID when inserting an item into table Tags.
How can I support this without having ID as an additional property in my model?
I also tried the data annotation for generated values on add
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]. But this won't work if I want to create a new model like:
Tags test = new Tags("test");
With an ID parameter in my model I would always need to send an additional int which I don't use when calling the constructor.
I tried now following code inside my model for ID property:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ID { get; set; }
For now it's working.
Your Tags class should have an Id property, otherwise you can't distinguish Tags with the same name. If Name is unique, you could consider not adding an ID at all and make Name the primary key, but I wouldn't recommend this.
To support this:
Tags test = new Tags("test");
you could simply create two constructors, one to initialize new Tags to store and one to construct existing tags.
public Tags(string name)
{
Name = name;
}
public Tags(int id, string name)
{
Id = id;
Name = name;
}

EntityFramework 'join' with fixed value segments in a composite key

Currently for EF5, but planning to upgrade to EF6 'soon'.
With following Tables:
Note these tables are simplified to explain the issue, the real tables are obviously more complex. The database is a legacy database and I can't change the tables.
BarRed and BarBlue are different tables with a different structure, only the way they link to Foo is identical.
[Table("Foo")]
public class Foo
{
// Id+ColorCode is the primary Key on this table
[Key,Column(Order=0)]
int Id;
[Key,Column(Order=1)]
int ColorCode; // 1=red, 2=blue, 3=green, …
public virtual ICollection<BarRed> BarRed { get; set; } // when ColorCode==1, empty/null otherwise
public virtual ICollection<BarRed> BarBlue { get; set; } // when ColorCode==2, empty/null otherwise
}
[Table("BarRed")]
public class BarRed
{
[Key]
int Id; // Id is the primary key in this table.
// There is no ColorCode column in this table, essentially the ColorCode for each record in this table is the literal value 1.
[ForeignKey("Id,1")] // This doesn't work, doesn't accept the 1
public virtual Foo Foo { get; set; }
}
[Table("BarBlue")]
public class BarBlue
{
[Key]
int Id; // Id is the primary key in this table.
// There is no ColorCode column in this table, essentially the ColorCode for each record in this table is the literal value 2.
[ForeignKey("Id,2")] // This doesn't work, doesn't accept the 2
public virtual Foo Foo { get; set; }
}
So in short, how do I get EntityFramework to link 2 tables in a One-To-Many relation when the 'Many' side table doesn't have an explicit column for one of the key segments, but a fixed literal value should be used on all rows.
I also have a similar issue/problem in the other direction, a One-To-Many where the 'One' side doesn't have an explicit column on each record but a literal value should be used.
I can join the tables from SQL and this does work and uses the indexes properly, I need this for EF to make the database accessible from OData.

Cannot insert the value NULL into column in ASP.NET MVC Entity Framework

When trying to use this code:
var model = new MasterEntities();
var customer = new Customers();
customer.Sessionid = 25641;
model.Customers.Add(customer);
model.SaveChanges();
I get:
{"Cannot insert the value NULL into column 'Sessionid', table
'master.dbo.Column'; column does not allow nulls. INSERT
fails.\r\nThe statement has been terminated."}
The column "Sessionid" is actually the primary key and is marked with [KEY] like this:
public class Customers
{
[Key]
public long Sessionid { get; set; }
public long? Pers { get; set; }
}
So according to this question, it seems as if when the property is marked with [KEY], EF ignores my own declaration of Sessionid since it expects the database to assign the value.
So how can I solve this? If I remove [KEY] I get the "entity type has no key defined" exception...
I solved it by adding [DatabaseGenerated(DatabaseGeneratedOption.None)] like this:
public class Customers
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public long Sessionid { get; set; }
public long? Pers { get; set; }
}
You can configure SQL to auto-generate (and auto-increment) the primary key for the table upon inserts. Then just remove the [Key] in C# and you don't need to set the ID in the application manually, the db will generate it for you.
I have encountered this problem multiple times while working with Microsoft SQL Server and I have followed the same way to fix it. To solve this problem, make sure Identity Specification is set to Yes. Here's how it looks like:
In this way the column number auto increments as a primary key normally would.
HOW?: right-click the table that contains the column, choose Design, select the primary key and in Column Properties window find Identity Specification and set it to Yes.

Categories

Resources