2

It makes a few hour that I'm trying to figure this one out, but could find a solution.

I have an entity with a List<string> property and I'd like to data seeding it. I succeeding using modelBuilder.Entity<T>.HasData(), but it doesn't work with dotnet ef migrations.

Here is my entity:

public class MyEntity
{
    public int Id { get; set; }

    public List<string> Names { get; set; }
}

And here's my DbContext.OnModelCreating override:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<MyEntity>()
        .HasData(
            new MyEntity { Id = 1, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 2, Names = new List<string> { "text1", "text2", "text3" } },
            new MyEntity { Id = 3, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 4, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 5, Names = new List<string> { "text1", "text2", "text3" } });
}

This works if I run my app, it creates the table and seed it with the wanted values. But if I run dotnet ef migrations add AddMyEntities --project src/MyProject --startup-project src/MyProject.Api, I get this output:

Build started...
Build succeeded.
System.NotSupportedException: The type mapping for 'List<string>' has not implemented code literal generation.
   at Microsoft.EntityFrameworkCore.Storage.CoreTypeMapping.GenerateCodeLiteral(Object value)
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.UnknownLiteral(Object value)
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.Literal(Object[,] values)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationOperationGenerator.Generate(InsertDataOperation operation, IndentedStringBuilder builder)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationOperationGenerator.Generate(String builderName, IReadOnlyList`1 operations, IndentedStringBuilder builder)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGenerator.GenerateMigration(String migrationNamespace, String migrationName, IReadOnlyList`1 upOperations, IReadOnlyList`1 downOperations)
   at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
The type mapping for 'List<string>' has not implemented code literal generation.

How can I seed a simple List<string> property?


Edit: I think this question is different from other questions such as Entity Framework - Code First - Can't Store List because my program works fine if I launch it on a blank DB (using PostgreSQL, it creates a Names column of type text[], as you would expect). The problem is when I use the dotnet ef migration tool.

Creak
  • 4,021
  • 3
  • 20
  • 25
  • Unless I missed some improvement to EF, entities can't have collections of simple types (int, string, etc). If I did miss an new update, I'd love to see how it stores that in the DB, someone point me to that. – gilliduck Feb 14 '21 at 02:37
  • 1
    Good answer with possible solutions below: https://stackoverflow.com/a/20712012/186861 – SeeSharp Feb 14 '21 at 04:29
  • Does this answer your question? [Entity Framework - Code First - Can't Store List](https://stackoverflow.com/questions/20711986/entity-framework-code-first-cant-store-liststring) – atiyar Feb 14 '21 at 14:17
  • This is not exactly the same, the code in my question works if I run on a blank DB (the column type is `text[]`), it's only the migration tool that doesn't work. – Creak Feb 14 '21 at 19:38
  • Looks like something that's not supported by EF + Postgresql. Seeding properties like this is somewhere in-between scalar properties (supported by EF) and navigation properties (not supported). Not sure if EF's seeding code has handles to let data providers add supported data types. – Gert Arnold Feb 15 '21 at 08:27
  • See this ticket on the dotConnect for PostgreSQL forum, it's on their roadmap: https://forums.devart.com/viewtopic.php?f=3&t=44479&sid=147a706e6deeca8bb59c2c4c61317052 – Gert Arnold Mar 04 '21 at 08:01

1 Answers1

3

EDIT: the OP mentions that it's using PostgreSQL with the corresponding EF provider, so the problem is not a missing value converter.

The issue seems to be related to a limitation in EF Core itself. The migration scaffolding process currently uses a built-in, hardcoded CSharpHelper class which knows how to generate C# literals for a closed set of types. For example, it knows how to generate object[] literals, but not IEnumerable<>. (Ref.: dotnet/efcore#12979). It seems that now it's possible for providers to add generation for code literals for other types, and I could found that it was added for some types in npgsql/efcore.pg#0deb6a23, but not for List<> as far as I understsand... but I can be wrong since I haven't tested any of this myself.

My suggestion? Either use string[] or ditch HasData and provide some custom seeding logic.


OLD: The problem is not with the HasData functionality itself. The underlying problem is that EF Core does not natively know how to store a List<string> into the database, thus it fails.

What you can do is to define a custom ValueConverter that instructs EF Core how to store that specific property into your database column (You can check EF Core docs about this topic).

Probably the best way to go would be define this conversion using Newtonsoft JSONConvert:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<MyEntity>()
        .Property(p => p.Names)
        .HasConversion(
            listOfStringNames => JsonConvert.SerializeObject(listOfStringNames),
            namesJsonRepresentation => JsonConvert.DeserializeObject<List<string>>(namesJsonRepresentation));
        .HasData(
            new MyEntity { Id = 1, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 2, Names = new List<string> { "text1", "text2", "text3" } },
            new MyEntity { Id = 3, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 4, Names = new List<string> { "text1", "text2" } },
            new MyEntity { Id = 5, Names = new List<string> { "text1", "text2", "text3" } });
}

As a side note, I would like to mention that even though HasData is nice, in my personal experience was better to just write my own DatabaseInitializer class that would seed the data I need with any custom logic I want, without any of the limitations that HasData has (Ref.: https://learn.microsoft.com/en-us/ef/core/modeling/data-seeding#limitations-of-model-seed-data)

dglozano
  • 6,369
  • 2
  • 19
  • 38
  • I did try that, but then the column type becomes `text`, while if I don't add a converter, the column type is `text[]` (as expected). The peculiar thing about this problem is that it works if I run it on a blank DB, it's only the dotnet EF migration tool that does not work. – Creak Feb 14 '21 at 19:40
  • @Creak what DB are you using? AFAIK, no SQL provider has support for storing an array of strings (text[]), therefore you have to store them as a single string using some sort of delimiter, or serialized as JSON. Also, how do you know it works on an empty database? If you can't apply the migration you won't be able to find out – dglozano Feb 14 '21 at 19:48
  • I'm using PostgreSQL... That may be why it works when I run the program but not when I do the migration with dotnet ef. I guess dotnet ef doesn't check the DB provider? – Creak Feb 14 '21 at 20:04
  • @Creak hmm just found out that PosgreSQL has support for array data types It seems that you have it properly configured according to https://www.npgsql.org/efcore/mapping/array.html . Have you double checked that you are using the NpgSQL provider for EFCore, and not the SQL Server one , right? – dglozano Feb 14 '21 at 20:18
  • @Creak which version of npsql provider are you using? You might want to try defining your property in the Entity as `string[]` instead of `List`, after reading through some issues it seemed that worked in some cases – dglozano Feb 14 '21 at 21:53
  • For the provider, I got Npgsql.EntityFrameworkCore.PostgreSQL 5.0.2. – Creak Feb 15 '21 at 12:39
  • @Creak could you post the whole code for you DbContext configuration? Including the part where you setup the provider – dglozano Feb 15 '21 at 12:58
  • Here's my DbContext and the class extension I use to configure it: https://gist.github.com/MightyCreak/5dd1afefb6a94057630fc1a717b1136f – Creak Feb 27 '21 at 13:40