18

Since Entity Framework uses nvarchar(max) as default for strings I would like to set something else as default.

https://dba.stackexchange.com/questions/48408/ef-code-first-uses-nvarcharmax-for-all-strings-will-this-hurt-query-performan

In Entity Framework 6.1.3 I could modify OnModelCreating(DbModelBuilder modelBuilder) like this:

modelBuilder.Properties<DateTime>().Configure(c => c.HasColumnType("datetime2"));
modelBuilder.Properties<DateTime>().Configure(c => c.HasPrecision(0));

modelBuilder.Properties<string>()
    .Configure(s => s.HasMaxLength(256).HasColumnType("nvarchar"));

If I then modified a property with data annotations EF used these values instead, like this:

[MaxLength(128)]
public string Name { get; set; }

[Column(TypeName = "nvarchar(MAX)")]
[MaxLength]
public string Comment { get; set; }

However using Microsoft.EntityFrameworkCore.SqlServer 2.1.0 I cant do it like this and I can't use Conventions either.

I could solve datetime like this but if I try to do the same for strings the migration says type: "nvarchar(256)", maxLength: 128 if I use data annotations for example. How can I solve this?

foreach (var property in modelBuilder.Model.GetEntityTypes()
    .SelectMany(t => t.GetProperties())
    .Where(p => p.ClrType == typeof(DateTime)))
{
    property.Relational().ColumnType = "datetime2(0)";
}
Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • Why putting differtent size in nvarchar and maxlength? – CodeNotFound Jun 13 '18 at 15:28
  • @CodeNotFound That is the problem, the migration generates code like this in EF core: `Comment = table.Column(type: "nvarchar(256)", maxLength: 128, nullable: false),`. This did not happen in EF 6.1.3. – Ogglas Jun 13 '18 at 15:33
  • By the way, I don't know why you don't just use nvarchar(128) instead of nvarchar(MAX)? – CodeNotFound Jun 13 '18 at 15:54

1 Answers1

30

Update (EF Core 6.0+):

The ability to specify different type mapping defaults finally has been added in EF Core 6.0 via the so called Pre-convention model configuration, so the code now would be something like this:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    base.ConfigureConventions(configurationBuilder);

    configurationBuilder.Properties<string>()
        //.AreUnicode(false)
        //.AreFixedLength()
        .HaveMaxLength(256);
}

More examples are provided in the documentation link.

Important Note: However, as mentioned by @PeterB in comments and verified in EF Core issue tracker, for some reason this configuration has higher precedence (same as model builder fluent API), so you won't be able to override these "defaults" with data annotations (fluent configuration in OnModelCreating will still do). So you might need to use the original approach below in case you rely on data annotations for overriding defaults.

Original:

There are several attributes indirectly affecting the column type of a string property - MaxLength (e.g. varchar(256) vs varchar(MAX), IsUnicode (e.g. nvarchar vs varchar) and IsFixedLength (e.g. char vs varchar).

The current model API is inconsistent. The first is accessible through GetMaxLength and SetMaxLength, the second - via IsUnicode and IsUnicode, and there is no public model API for the third one (only fluent API).

So to set the MaxLength you could use:

foreach (var property in modelBuilder.Model.GetEntityTypes()
    .SelectMany(t => t.GetProperties())
    .Where(p => p.ClrType == typeof(string)))
{
    if (property.GetMaxLength() == null)
        property.SetMaxLength(256);
}

which is not fully correct, because null in this case has dual meaning - not specified and MAX.

The correct way requires using EF Core internal API, which provides much more configuration methods, in particular allowing you to pass ConfigurationSource enum value along with the attribute value. ConfigurationSource enum values define the priority of the configuration - with Convention being the lowest, then DataAnnotation and finally Explicit being the highest. The whole idea is that the lower priority configuration do not overwrite the configuration already set by a higher priority. All public fluent API use Explcit, while in our case the Convention perfectly fits (since we are simulating conventional default).

So if you accept the warning "This API supports the Entity Framework Core infrastructure and is not intended to be used directly from your code. This API may change or be removed in future releases.", add

using Microsoft.EntityFrameworkCore.Metadata.Internal;

and use

foreach (var property in modelBuilder.Model.GetEntityTypes()
    .SelectMany(t => t.GetProperties())
    .Where(p => p.ClrType == typeof(string)))
{
    ((Property)property).Builder
        .HasMaxLength(256, ConfigurationSource.Convention);
}
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • May I enquire if there has been any update on this answer, this does indeed work but as it's an internal method it may not be supported in future updates so is there any better approach one should use instead? – Steven Price May 02 '21 at 10:19
  • 2
    @StevenPrice When it changes, or better when EF Core exposes publicly conventions, then the answer will be updated. Currently even the latest EFC 5.x does not provide public API for such tasks. There is public `IConventionPropertyBuilder` interface, but no public way to get it, as `PropertyBuilder` class (which implements it) constructor is also marked as "internal API". – Ivan Stoev May 02 '21 at 10:36
  • @IvanStoev Really good answer as always. A clarification for anyone that finds this is that the `DbContext` needs a `DbSet` for this to work like `public DbSet Products { get; set; }`. You will get `type: "nvarchar(max)` instead of `type: "nvarchar(256)` otherwise. – Ogglas Sep 17 '21 at 23:25
  • @IvanStoev I just upgraded to NuGet `Microsoft.EntityFrameworkCore 6.0.0` and then `AsProperty` does not work anymore. `'IMutableProperty' does not contain a definition for 'AsProperty' and no accessible extension method 'AsProperty' accepting a first argument of type 'IMutableProperty' could be found (are you missing a using directive or an assembly reference?)`. What is the correct way using .NET 6? – Ogglas Nov 09 '21 at 20:31
  • @Ogglas But of course, they can't miss the opportunity to break some "internal" (or even public) API :( The good thing is that they added (finally!) a better alternative. Answer updated (also the original code to use cast instead of the removed extension method which was doing exactly the same thing). – Ivan Stoev Nov 10 '21 at 06:04
  • @IvanStoev Haha of course not! Thank you for the new example and documentation link! – Ogglas Nov 10 '21 at 14:31
  • 1
    Pre-convention model configuration breaks `StringLength` and `MaxLength` attributes. The lecagy solution works as expected. – PeterB Jul 29 '22 at 07:34
  • @PeterB Quite annoying if true (haven't checked), because by idea conventions must be with less priority than attributes and explicit fluent configuration. – Ivan Stoev Jul 29 '22 at 10:34
  • 1
    @IvanStoev: That is what I thought, but [the experts state otherwise](https://github.com/dotnet/efcore/issues/28541#issuecomment-1198992230) :-( – PeterB Jul 29 '22 at 12:52