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

Non-nullable json array fields are translating to nullable in migration files #35540

Open
HardbassEnjoyer opened this issue Jan 28, 2025 · 4 comments

Comments

@HardbassEnjoyer
Copy link

Bug description

I think there is an issue, but i don't know if this issue related to this repo or to npgsql. When i describe a column field as Object[] with default value [] and creating a migration - this field in a migration file described as nullable unlike i was expect. I have a simple repro example. When i run dotnet ef migrations add Test it generate following migration file:

using System;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace Test.App2.Migrations
{
    /// <inheritdoc />
    public partial class Test : Migration
    {
        /// <inheritdoc />
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "table",
                columns: table => new
                {
                    id = table.Column<Guid>(type: "uuid", nullable: false),
                    value = table.Column<string>(type: "text", nullable: false),
                    data_array = table.Column<string>(type: "jsonb", nullable: true),
                    data_object = table.Column<string>(type: "jsonb", nullable: false)
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_table", x => x.id);
                });
        }

        /// <inheritdoc />
        protected override void Down(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.DropTable(
                name: "table");
        }
    }
}

If it is not an issue can you show me any workarounds to describe data_array as non nullable?

Your code

// See https://aka.ms/new-console-template for more information

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

Console.WriteLine("Hello, World!");

public class Context(DbContextOptions<Context> options) : DbContext(options)
{
    public DbSet<Table> Tables { get; set; }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Table>()
            .OwnsMany(e => e.DataArray, b => b.ToJson("data_array"));

        modelBuilder.Entity<Table>()
            .OwnsOne(e => e.DataObject, b => b.ToJson("data_object"));
    }
}

public class ContextFactory : IDesignTimeDbContextFactory<Context>
{
    public Context CreateDbContext(string[] args)
    {
        const string connStr = "Host=localhost;Port=5432;Database=test;Username=test;Password=test;";

        var optionsBuilder = new DbContextOptionsBuilder<Context>();
        optionsBuilder.UseNpgsql(connStr);

        return new Context(optionsBuilder.Options);
    }
}

[Table("table")]
public class Table
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    [Column("id")]
    public Guid Id { get; set; }

    [Column("value")]
    public string Value { get; set; } = default!;

    public TableData[] DataArray { get; set; } = [];

    public TableData DataObject { get; set; } = default!;
}

public class TableData
{
    [JsonPropertyName("data1")]
    public string Data1 { get; set; }
    
    [JsonPropertyName("data2")]
    public int Data2 { get; set; }
    
    [JsonPropertyName("data3")]
    public decimal Data3 { get; set; }
}

Stack traces


Verbose output

Using project '/home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj'.
Using startup project '/home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj'.
Writing '/home/ivan/Repositories/test/src/Test/Test.App2/obj/Test.App2.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=/tmp/tmpbQDDcJ.tmp /verbosity:quiet /nologo /home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj
Writing '/home/ivan/Repositories/test/src/Test/Test.App2/obj/Test.App2.csproj.EntityFrameworkCore.targets'...
dotnet msbuild /target:GetEFProjectMetadata /property:EFProjectMetadataFile=/tmp/tmpMbBPWd.tmp /verbosity:quiet /nologo /home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj
Build started...
dotnet build /home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj /verbosity:quiet /nologo /p:PublishAot=false
/home/ivan/Repositories/test/src/Test/Test.App2/Program.cs(57,19): warning CS8618: свойство "Data1", не допускающий значения NULL, должен содержать значение, отличное от NULL, при выходе из конструктора. Возможно, стоит объявить свойство как допускающий значения NULL. [/home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj]

Сборка успешно завершена.

/home/ivan/Repositories/test/src/Test/Test.App2/Program.cs(57,19): warning CS8618: свойство "Data1", не допускающий значения NULL, должен содержать значение, отличное от NULL, при выходе из конструктора. Возможно, стоит объявить свойство как допускающий значения NULL. [/home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj]
    Предупреждений: 1
    Ошибок: 0

Прошло времени 00:00:00.88
Build succeeded.
dotnet exec --depsfile /home/ivan/Repositories/test/src/Test/Test.App2/bin/Debug/net8.0/Test.App2.deps.json --additionalprobingpath /home/ivan/.nuget/packages --runtimeconfig /home/ivan/Repositories/test/src/Test/Test.App2/bin/Debug/net8.0/Test.App2.runtimeconfig.json /home/ivan/.dotnet/tools/.store/dotnet-ef/8.0.6/dotnet-ef/8.0.6/tools/net8.0/any/tools/netcoreapp2.0/any/ef.dll migrations add Test --assembly /home/ivan/Repositories/test/src/Test/Test.App2/bin/Debug/net8.0/Test.App2.dll --project /home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj --startup-assembly /home/ivan/Repositories/test/src/Test/Test.App2/bin/Debug/net8.0/Test.App2.dll --startup-project /home/ivan/Repositories/test/src/Test/Test.App2/Test.App2.csproj --project-dir /home/ivan/Repositories/test/src/Test/Test.App2/ --root-namespace Test.App2 --language C# --framework net8.0 --nullable --working-dir /home/ivan/Repositories/test/src/Test/Test.App2 --verbose
Using assembly 'Test.App2'.
Using startup assembly 'Test.App2'.
Using application base '/home/ivan/Repositories/test/src/Test/Test.App2/bin/Debug/net8.0'.
Using working directory '/home/ivan/Repositories/test/src/Test/Test.App2'.
Using root namespace 'Test.App2'.
Using project directory '/home/ivan/Repositories/test/src/Test/Test.App2/'.
Remaining arguments: .
The Entity Framework tools version '8.0.6' is older than that of the runtime '8.0.11'. Update the tools for the latest features and bug fixes. See https://aka.ms/AAc1fbw for more information.
Finding DbContext classes...
Finding IDesignTimeDbContextFactory implementations...
Found IDesignTimeDbContextFactory implementation 'ContextFactory'.
Found DbContext 'Context'.
Finding application service provider in assembly 'Test.App2'...
Finding Microsoft.Extensions.Hosting service provider...
No static method 'CreateHostBuilder(string[])' was found on class 'Program'.
No application service provider was found.
Finding DbContext classes in the project...
Using DbContext factory 'ContextFactory'.
Using context 'Context'.
Finding design-time services referenced by assembly 'Test.App2'...
Finding design-time services referenced by assembly 'Test.App2'...
No referenced design-time services were found.
Finding design-time services for provider 'Npgsql.EntityFrameworkCore.PostgreSQL'...
Using design-time services from provider 'Npgsql.EntityFrameworkCore.PostgreSQL'.
Finding IDesignTimeServices implementations in assembly 'Test.App2'...
No design-time services were found.
The index {'TableId'} was not created on entity type 'Table.DataArray#TableData (TableData)' as the properties are already covered by the index {'TableId', 'Id'}.
The property 'Table.DataArray#TableData (TableData).TableId' was created in shadow state because there are no eligible CLR members with a matching name.
The property 'Table.DataArray#TableData (TableData).Id' was created in shadow state because there are no eligible CLR members with a matching name.
The property 'Table.DataObject#TableData (TableData).TableId' was created in shadow state because there are no eligible CLR members with a matching name.
Writing migration to '/home/ivan/Repositories/test/src/Test/Test.App2/Migrations/20250128134308_Test.cs'.
Writing model snapshot to '/home/ivan/Repositories/test/src/Test/Test.App2/Migrations/ContextModelSnapshot.cs'.
'Context' disposed.
Done. To undo this action, use 'ef migrations remove'

EF Core version

8.0.11

Database provider

Npgsql.EntityFrameworkCore.PostgreSQL

Target framework

.NET 8.0

Operating system

Ubuntu 22.04.4 LTS

IDE

JetBrains Rider 2024.1.3

@roji
Copy link
Member

roji commented Jan 30, 2025

Confirmed, owned JSON arrays seem to always be nullable. Minimal repro on SQL Server:

Minimal repro
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

await using var context = new MyContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

public class MyContext : DbContext
{
    public DbSet<Table> Tables { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Server=localhost;Database=test;User=SA;Password=Abcd5678;Connect Timeout=60;ConnectRetryCount=0;Encrypt=false")
            .LogTo(Console.WriteLine, LogLevel.Information)
            .EnableSensitiveDataLogging();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Table>()
            .OwnsMany(e => e.DataArray, b => b.ToJson("data_array"));

        modelBuilder.Entity<Table>()
            .OwnsOne(e => e.DataObject, b => b.ToJson("data_object"));
    }
}

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

    public TableData[] DataArray { get; set; }
    public TableData DataObject { get; set; }
}

public class TableData
{
    public string SomeData { get; set; }
}

Putting on the backlog for now. The workaround is to manually make the column non-nullable in migrations after creation.

@maumar
Copy link
Contributor

maumar commented Feb 3, 2025

you can explicitly set the navigation as required:

modelBuilder.Entity<Table>().Navigation(x => x.DataArray).IsRequired();

However, this throws an exception (CoreStrings.NonUniqueRequiredDependentNavigation):

Unable to create a 'DbContext' of type 'MyContext'. The exception ''Table.DataArray' cannot be configured as required since it was configured as a collection.'

So this seems to be by design, at least when JSON is implemented as owned types. @AndriySvyryd

@roji
Copy link
Member

roji commented Feb 3, 2025

I think we're conflating requiredness of the navigation in the model (I think collection navigations are indeed never required, you can always have zero) with requiredness of the JSON column holding the collection. Even if there are zero owned entity types, the database representation of that (I think) is still a non-null JSON column with an empty JSON array string representation ([]).

@AndriySvyryd
Copy link
Member

Yeah, looks like a bug

@AndriySvyryd AndriySvyryd self-assigned this Feb 3, 2025
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

4 participants