EF Code First migration maxLength being set to 4000 instead of max - entity-framework

tl;dr: I have a column that should be NVARCHAR(MAX) but scaffolding a migration with Add-Migration is giving me a column with max length 4000 in Up(). What do I have to do to get this to be MAX?
Consider the following model:
public class Foo
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
[Required]
[DataType(DataType.MultilineText)]
public string Memo { get; set; }
[Required]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
I have two conventions set up in Context.OnModelCreating:
A convention which sets the default maxLength of any string property not otherwise configured to 1024, with the line:
Properties<string>().Configure(c => c.HasMaxLength(1024));
An attribute-based convention which reads the DataTypeAttribute and sets some properties accordingly, namely the column type and length:
switch (attribute.DataType)
{
case DataType.MultilineText:
configuration.HasColumnType("nvarchar").IsMaxLength();
break;
case DataType.EmailAddress:
configuration.HasColumnType("nvarchar").HasMaxLength(255);
break;
}
I tested this all before setting up migrations and it worked beautifully, giving me the following table - note that Memo is being created as NVARCHAR(MAX):
CREATE TABLE dbo.Foo (
Id INT IDENTITY NOT NULL,
Name NVARCHAR(100) NOT NULL,
Memo NVARCHAR(MAX) NOT NULL,
Email NVARCHAR(255) NOT NULL,
CONSTRAINT [PK_Foo] PRIMARY KEY (Id)
)
Then I enabled migrations and added one, and got the following table definition in Up():
CreateTable(
"dbo.Foo",
c => new
{
Id = c.Int(nullable: false, identity: true),
Name = c.String(nullable: false, maxLength: 100),
Memo = c.String(nullable: false, maxLength: 4000),
Email = c.String(nullable: false, maxLength: 255),
})
.PrimaryKey(t => t.Id)
Wha? Where'd that maxLength: 4000 come from? It's not the MAX I would expect, nor is it even the 1024 I configured as the default with my first convention. It's certainly respecting the 100 and 255 of Name and Email (proving my attribute-based convention is working)...
So, is this a bug in the framework, or is there something about migrations that I'm not understanding? And either way, what can I do to get Code First Migrations to respect my IsMaxLength()?

Apparently IsMaxLength() has a different meaning in the context vs in a migration. In the context, it creates a column of NVARCHAR(MAX) but once you enable migrations this is interpreted as "the biggest possible value the column allows, except for MAX". As #marc_s points out in a comment, this is 4000 for a nvarchar column.
Indeed, while I have read this works outside of migrations, setting HasMaxLength(int.MaxValue) and calling Add-Migration gives me the following error:
(0,0) : error 0026: MaxLength '2147483647' is not valid. Length must be between '1' and '4000' for 'nvarchar' type.
Playing around a little more, I decided to try HasColumnType("nvarchar(MAX)"), which to my surprise worked! So replacing the call in the convention above works.
I can't find any way to read out the result of IsMaxLength() anywhere where I can set the type (for example IStoreModelConvention is too late), so it's not the prettiest solution, but at least I can move forward.

Related

Entity Framework Code First Migrations Guid NewId Instead Of NewSequentialId

We're using EF6 with code first migrations pointing to an Azure SQL. We've started into using some Guid for both primary keys as well as alongside int primary keys.
Primary Key:
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
Alongside int PK:
[Index, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid PolymorphicId { get; set; }
When I generate the migration, I get as follows (respectively):
Id = c.Guid(nullable: false, identity: true),
PolymorphicId = c.Guid(nullable: false, identity: true),
Expectation: SQL generated to have default values of newsequentialid
Actually Happening:
[Id] [uniqueidentifier] NOT NULL DEFAULT newid(),
[PolymorphicId] [uniqueidentifier] NOT NULL DEFAULT newid(),
How do I make my EF migrations generate with newsequentialid instead of newid? Everything I've looked up online says that they should be generating with newsequentialid.
When targeting Azure, SqlServerMigrationSqlGenerator will default to "newid()". When targeting on-premesis Sql Server 2005 or later, it will default to "newsequentialid()".
Source: GitHub SqlServerMigrationSqlGenerator.cs
Optional fix: Create a custom SqlGenerator, inheriting SqlServerMigrationSqlGenerator, override GuidColumnDefault
Optional fix: As posted by JFM, modify the generated migration file, setting the defaultValueSql
You could try setting the sql used by the sql server column key generation in your migration script, I beleive it should look somethnig similar to this:
Id = c.Guid(nullable: false, identity: true, defaultValueSql: "newsequentialid()")

EF Code First Model with Properties that Hold Encrypted Data

I've got some encrypted code in a table in the database which I am maintaining. This is circa 2012, so no "Always On" encryption. 3 columns contain encrypted data.
If I reverse engineer an EF domain, the Model which is created for that table contains properties for those columns which have a type byte[]. This is to be expected, as the columns are varbinary. So, it looks like this:
class Person
{
public byte[] FirstName { get; set; } // FirstName
}
Is there an elegant way to do some kind of EF mapping/configuration such that the FirstName class has a type of string and that it decrypts automagically by the framework? I realize I can just instantiate a Person object using sql, but it would be nice to offload this processing to the framework.
I've seen one of two solutions around where people are basically using a sql query for every property. They decorate the property with an Encrypt attribute and iterate the properties of every property. But with a sql query for every property for every object in a list - that does not exactly scale.
Has anyone "solved" this issue before?
Note: to retrieve the data, you first need to send a sql statement akin to:
OPEN SYMMETRIC KEY SomeKey DECRYPTION BY CERTIFICATE SomeCertificate
Thanks
In this answer I'm going to set out the things you need to do to deal with encrypted columns in EF. So, the columns in question will have a type of VARBINARY(MAX). Lets say you table looks something like this:
CREATE TABLE dbo.Person
(
SomeId int NOT NULL,
CreatedByUserId uniqueidentifier NULL,
CreatedUtcDate datetimeoffset(7) NULL,
Rowversion timestamp NULL,
FirstName varbinary(MAX) NULL,
LastName varbinary(MAX) NULL
)
Step 1 - Create a View which returns the decrypted columns. The view should basically be identical to your table, but for the columns which hold encrypted data, it should return the decrypted data. It would looks something like this:
CREATE VIEW [dbo].[v_Person]
AS
SELECT [SomeId]
,[CreatedByUserId]
,[CreatedUtcDate]
,[RowVersion]
,CONVERT(NVARCHAR(50),DECRYPTBYKEY([FirstName])) [FirstName]
,CONVERT(NVARCHAR(50),DECRYPTBYKEY([LastName])) [LastName]
FROM [dbo].[Person]
Step 2 - Create your domain model Person class with string as the relevant property type, not byte[] (note the select statement in the View above where we have cast the decrypted columns to NVARCHAR).
public class Person
{
public int SomeId { get; set; }
public string FirstName { get; set; } // string, not binary
public string LastName { get; set; } // string, not binary
public Guid CreatedByUserId { get; set; }
public DateTime CreatedUtcDate { get; set; }
public int SomeForeignKeyId { get; set; }
}
Step 3 - We need to set up a mapping for that Domain class. (The solution I am setting out here is for EF6. I am aware that EF Core does not support separate mapping files yet, so this would need to be done in the OnModelCreating event of you DbContext). Create a mapping class for you domain object which looks like this:
public class PersonMap : EntityTypeConfiguration<Person>
{
public PersonConfiguration(string schema)
{
ToTable("v_Person", schema); // note we map to the View
HasKey(x => x.SomeId);
// ... other properties elided for brevity
Property(x => x.FirstName)
.HasColumnName(#"FirstName")
.HasColumnType("nvarchar")
.IsOptional()
.HasMaxLength(50);
Property(x => x.LastName)
.HasColumnName(#"LastName")
.HasColumnType("nvarchar")
.IsOptional()
.HasMaxLength(50);
// Foreign keys
HasRequired(a => a.LogbookEntry)
.WithOptional(b => b.Person)
.WillCascadeOnDelete(false);
MapToStoredProcedures(p =>
p.Insert(i => i.HasName("Insert_Person"))
.Update(u => u.HasName("Update_Person"))
.Delete(d => d.HasName("Delete_Person")));
}
}
Note how we mapped to the view, v_Person, and not the raw table.
Also note the call to MapToStoredProcedures, which I explain next.
Step 4 - The last step is to create some stored procedures for your Insert, Update and Deletes. When you invoke SaveChanges, these will be invoked by EF and the relevant stored proc will be invoked depending on which EntityState the entity has. I won't set out all 3, but an example of the Update stored proc might look something like:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[Update_Person]
#SomeId INT,
#CreatedByUserId UNIQUEIDENTIFIER,
#CreatedUtcDate DATETIME,
#RowVersion_Original timestamp,
#FirstName NVARCHAR(50),
#LastName NVARCHAR(50) = NULL
AS
BEGIN
SET NOCOUNT ON;
DECLARE #CertKey NVARCHAR(7) = 'CertKey';
UPDATE PersonDetail
SET
FirstName = ENCRYPTBYKEY(KEY_GUID(#CertKey), #FirstName),
LastName = ENCRYPTBYKEY(KEY_GUID(#CertKey), #LastName)
WHERE SomeId = #SomeId
SELECT SomeId, RowVersion
FROM PersonDetail
WHERE SomeId = #SomeId
END
Feel free to comment if you have done it a better way.
Cheers

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.

EntityFramework 4.1 DbContext select adding CAST columns with additional characters

i'm doing a standard GetAll() from dbcontext:
DbContext.Set<T>()
however i'm getting a weird message from oracle:
{"ORA-00904: \"Extent1\".\"Sub_Object_ID\": invalid identifier"}
if i look at generated sql (by looking at the query variable), i see that a few variables are added at the end as CASTS
"Extent1"."SomeEntity_ID", <-- correct
"Extent1"."SomeEnttiy2_ID", <-- correct
"Extent1"."Sub_Object", <-- correct
CAST( "Extent1"."SomeEntity_ID1" AS number(10,0)) AS "C3", <-- "1" appended
CAST( "Extent1"."SomeEnttiy2_ID1" AS number(10,0)) AS "C4", <-- "1" appended
CAST( "Extent1"."Sub_Object_ID" AS number(10,0)) AS "C5", <-- "_ID" appended
...
FROM "dbo"."MyEntity" "Extent1"
all the properties were correctly identified in the main portion of the select. however in the CAST portion, property names were appended with digits and _ID.. this is causing the select to fail..
looking at my entity, i have the properties specified once.. in this format:
public Nullable<decimal> SomeEntity_ID { get; set; }
what's with the casts?
this was just a matter of configuring foreign keys. I still don't understand the intention of this default behavior (adding a set of select columns for every foreign with appended "1")..
but declaring foreign keys fixes it.
via fluent API:
modelBuilder.Entity<FirmPerson>()
.HasRequired(f => f.Firm)
.WithMany(p => p.FirmPerson)
.HasForeignKey(f => f.FirmID);
or via attribute:
public int FirmID { get; set; }
[ForeignKey("FirmID")]
public virtual Firm Foo { get; set; }

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.