Pomelo.entityframeworkcore.mysql: Fluent API with fixed length varchar

Created on 3 Apr 2020  ·  3Comments  ·  Source: PomeloFoundation/Pomelo.EntityFrameworkCore.MySql

Steps to reproduce

image

Tag Model

public class Tag : BaseModel
{
    public string Name { get; set; }

    public DateTime Creation { get; set; }

    public int UsageCount { get; set; }

    public int Likes { get; set; }

    public int Dislikes { get; set; }

    public bool IsActive { get; set; }

    public override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Tag>(entity =>
        {
            entity.HasKey(t => t.Id);
            entity.Property(t => t.Id).ValueGeneratedOnAdd();

            entity.Property(t => t.Name).IsFixedLength(true).HasColumnType("VARCHAR(32)").HasMaxLength(32).IsRequired();
            entity.Property(t => t.Creation).HasDefaultValue(DateTime.Now).IsRequired();
            entity.Property(t => t.UsageCount).HasDefaultValue(0).IsRequired();
            entity.Property(t => t.Likes).HasDefaultValue(0).IsRequired();
            entity.Property(t => t.Dislikes).HasDefaultValue(0).IsRequired();
            entity.Property(t => t.IsActive).HasDefaultValue(true).IsRequired();
        });
    }
}

DbContext

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var currentModel in Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsSubclassOf(typeof(BaseModel)) && !t.IsAbstract))
            ((BaseModel)Activator.CreateInstance(currentModel)).OnModelCreating(modelBuilder);

        base.OnModelCreating(modelBuilder);
    }

The issue

        using (var context = new TagContext())
        {
            await context.Tags.InsertAsync(new Tag() { Name = "asdfasdfasdfasdfasdfadsfasdf;jklasdlasdfafsdlj;kafdsljkfsd;ljkf;sdlj;fkjlsd;jklfds" });
            await context.SaveAsync();
        }

image

It doesn't matter how I do define the "name" column with the fluet API, the insert won't trim the property. I did try:

            entity.Property(t => t.Name).IsFixedLength(true).HasColumnType("VARCHAR(32)").HasMaxLength(32).IsRequired();
            entity.Property(t => t.Name).IsFixedLength(true).HasMaxLength(32).IsRequired();
            entity.Property(t => t.Name).HasMaxLength(32).IsRequired();
            entity.Property(t => t.Name).IsFixedLength(true).HasColumnType("VARCHAR(32)").IsRequired();
            entity.Property(t => t.Name).HasColumnType("VARCHAR(32)").IsRequired();

the modeldef gets called flawlessly.

Exception message: Exception thrown: 'Microsoft.EntityFrameworkCore.DbUpdateException' in System.Private.CoreLib.dll
Stack trace:    at Microsoft.EntityFrameworkCore.Update.ReaderModificationCommandBatch.<ExecuteAsync>d__29.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__8.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.EntityFrameworkCore.Update.Internal.BatchExecutor.<ExecuteAsync>d__8.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__93.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.<SaveChangesAsync>d__97.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Pomelo.EntityFrameworkCore.MySql.Storage.Internal.MySqlExecutionStrategy.<ExecuteAsync>d__7`2.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.EntityFrameworkCore.DbContext.<SaveChangesAsync>d__54.MoveNext()
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at DBCT.Persistence.Repository.EntityFramework.EntityFrameworkUnitOfWork.<SaveAsync>d__5.MoveNext() in C:\Users\thoma\source\repos\DBCT\DBCT.Persistence\Repository\EntityFramework\EntityFrameworkUnitOfWork.cs:line 22

Further technical details

MySQL version:
image

Operating system:
image

Pomelo.EntityFrameworkCore.MySql version: 3.1.1
Microsoft.AspNetCore.App version: .NET Core 3.1

closed-question type-question

Most helpful comment

@Toemsel In addition to what @mguinness said, also be aware that varchar(n) is not of fixed length. It is a variable length store type. It's fixed length equivalent however is char(n).

But changing it from varchar(n) to char(n) will not make any difference in regards to your exception.
This is by design.

If you want to validate your data first, here are some articles about it:

If you just want to truncate strings before sending them to the database, it might be enough for you to use a Value Converter, that just truncates client-side strings:

```c#
entity
.Property(t => t.Name)
.HasMaxLength(32)
.IsRequired()
.HasConversion(
v => v.Substring(0, Math.Min(v.Length, 32)),
v => v);


If you want to automate this, based on whether `MaxLength` has been defined or not, you can do that as well. Take a look at the following console app:

```c#
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Storage;

namespace IssueConsoleTemplate
{
    public class IceCream
    {
        public int IceCreamId { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public DbSet<IceCream> IceCreams { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseMySql(
                    "server=127.0.0.1;port=3306;user=root;password=;database=Issue1058",
                    b => b.ServerVersion(new ServerVersion("8.0.20-mysql")))
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<IceCream>(
                entity =>
                {
                    entity.Property(e => e.Name)
                        .HasMaxLength(5)
                        .IsRequired();
                });

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                foreach (var property in entityType.GetProperties())
                {
                    var maxLength = property.GetMaxLength().GetValueOrDefault();
                    if (maxLength > 0)
                    {
                        property.SetValueConverter(new ValueConverter<string,string>(
                            v => v.Substring(0, Math.Min(v.Length, maxLength)),
                            v => v));
                    }
                }
            }
        }
    }

    internal class Program
    {
        private static void Main()
        {
            using (var context = new Context())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
            }

            using (var context = new Context())
            {
                context.IceCreams.Add(new IceCream {Name = "Vanilla"});
                context.SaveChanges();
            }

            using (var context = new Context())
            {
                var iceCreams = context.IceCreams.ToList();

                Debug.Assert(iceCreams.Count == 1);
                Debug.Assert(iceCreams[0].Name == "Vanil");
            }
        }
    }
}

All 3 comments

See maximum length documentation for Entity Properties:

Entity Framework does not do any validation of maximum length before passing data to the provider. It is up to the provider or data store to validate if appropriate. For example, when targeting SQL Server, exceeding the maximum length will result in an exception as the data type of the underlying column will not allow excess data to be stored.

@Toemsel In addition to what @mguinness said, also be aware that varchar(n) is not of fixed length. It is a variable length store type. It's fixed length equivalent however is char(n).

But changing it from varchar(n) to char(n) will not make any difference in regards to your exception.
This is by design.

If you want to validate your data first, here are some articles about it:

If you just want to truncate strings before sending them to the database, it might be enough for you to use a Value Converter, that just truncates client-side strings:

```c#
entity
.Property(t => t.Name)
.HasMaxLength(32)
.IsRequired()
.HasConversion(
v => v.Substring(0, Math.Min(v.Length, 32)),
v => v);


If you want to automate this, based on whether `MaxLength` has been defined or not, you can do that as well. Take a look at the following console app:

```c#
using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;
using Pomelo.EntityFrameworkCore.MySql.Storage;

namespace IssueConsoleTemplate
{
    public class IceCream
    {
        public int IceCreamId { get; set; }
        public string Name { get; set; }
    }

    public class Context : DbContext
    {
        public DbSet<IceCream> IceCreams { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder
                .UseMySql(
                    "server=127.0.0.1;port=3306;user=root;password=;database=Issue1058",
                    b => b.ServerVersion(new ServerVersion("8.0.20-mysql")))
                .UseLoggerFactory(
                    LoggerFactory.Create(
                        b => b
                            .AddConsole()
                            .AddFilter(level => level >= LogLevel.Information)))
                .EnableSensitiveDataLogging()
                .EnableDetailedErrors();
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<IceCream>(
                entity =>
                {
                    entity.Property(e => e.Name)
                        .HasMaxLength(5)
                        .IsRequired();
                });

            foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            {
                foreach (var property in entityType.GetProperties())
                {
                    var maxLength = property.GetMaxLength().GetValueOrDefault();
                    if (maxLength > 0)
                    {
                        property.SetValueConverter(new ValueConverter<string,string>(
                            v => v.Substring(0, Math.Min(v.Length, maxLength)),
                            v => v));
                    }
                }
            }
        }
    }

    internal class Program
    {
        private static void Main()
        {
            using (var context = new Context())
            {
                context.Database.EnsureDeleted();
                context.Database.EnsureCreated();
            }

            using (var context = new Context())
            {
                context.IceCreams.Add(new IceCream {Name = "Vanilla"});
                context.SaveChanges();
            }

            using (var context = new Context())
            {
                var iceCreams = context.IceCreams.ToList();

                Debug.Assert(iceCreams.Count == 1);
                Debug.Assert(iceCreams[0].Name == "Vanil");
            }
        }
    }
}

@mguinness oh, well, I thought fluent/annotations would not only exist for validation/creation/migration only. Nice to know and thanks for the hint.

@lauxjpn Thanks for letting me know about the value converter. That helps a lot!

PS: Yes, varchar is dynamic from 0-32 in my case. It was a poor choice of words.

Thank you guys!

Was this page helpful?
0 / 5 - 0 ratings