Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fluent API with fixed length varchar #1058

Closed
Toemsel opened this issue Apr 3, 2020 · 3 comments
Closed

Fluent API with fixed length varchar #1058

Toemsel opened this issue Apr 3, 2020 · 3 comments

Comments

@Toemsel
Copy link

Toemsel commented Apr 3, 2020

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

@mguinness
Copy link
Collaborator

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.

@lauxjpn
Copy link
Collaborator

lauxjpn commented Apr 3, 2020

@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:

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:

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");
            }
        }
    }
}

@Toemsel
Copy link
Author

Toemsel commented Apr 3, 2020

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants