efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Generation of Migrations for new Types / Tables differs in the default value generation from existing types

Open paule96 opened this issue 1 year ago • 1 comments

File a bug

There seems to be a bug in the generation of default values for boolean properties that result in a difference of the table structure in the database.

The bug always happens if you create new project and make an initial schema via code first in C#. Always when you have a property that should have a default value like that:

public bool InitBool { get; set; } = false;

The resulting migration will be:

InitBool = table.Column<bool>(type: "boolean", nullable: false),

See the included code link to the first commit

After the first migration, you can add in the same type, another property in the same way:

public bool AfterInitBool { get; set; } = false;

And the migration code will look like this:

migrationBuilder.AddColumn<bool>(name: "AfterInitBool", table: "SampleData", type: "boolean", nullable: false, defaultValue: false);

Now it will have a configured default value. But an important notice is that the migration snapshot doesn't know about the default value in both cases.

b.Property<bool>("AfterInitBool")
    .HasColumnType("boolean");

See the included code link to the second commit

Now if you create a completely new type and add a similar property, the migration will again not generate a column with default value.

See the included code link to the last commit

Include your code

The whole bug description refers to the reprodutiction repository that can be found here. Their are 3 cases that where tests that can be referenced by the following commits:

  • Initialize new DB context with a simple schema (commit)
  • Add to the existing type of the first commit, new propertiers (commit)
  • Add a new type (commit)

Include provider and version information

EF Core version: Database provider: Npgsql.EntityFrameworkCore.PostgreSQL & Microsoft.EntityFrameworkCore.SqlServer Target framework: .NET 8.0 Operating system: Windows 11 (22631.4112) IDE: Visual Studio 17.12.0 Preview 1.0 / PowerShell 7.4.5

paule96 avatar Sep 05 '24 15:09 paule96

@maumar do you need more information or assistance? It would be nice to have this fixed before the big ef 9.0 release, if it is not to complicated

paule96 avatar Oct 08 '24 12:10 paule96

@paule96 apologies for delayed response. I'm able to reproduce this. Currently the way to specify default value is with fluent api:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<AndAnotherSampleNewTable>(b =>
        {
            b.Property(x => x.AfterInitBool).HasDefaultValue(true);
            b.Property(x => x.AfterInitBoolWithAttributes).HasDefaultValue(true);
            b.Property(x => x.AfterInitBoolWithDefaultValueAttribute).HasDefaultValue(true);
        });
    }

Configuring default value using DefaultValueAttribute is tracked here: https://github.com/dotnet/efcore/issues/19546

When it comes to c# feature of initializing properties (public bool InitBool { get; set; } = false;) we don't think this is strong enough indicator of user's intent here. We want the default value to be specified more explicitly.

maumar avatar Oct 22 '24 00:10 maumar

closing as duplicate of https://github.com/dotnet/efcore/issues/19546

maumar avatar Oct 22 '24 00:10 maumar

@maumar I don't know if we should close this issue. Because even if the value assignment is not supported, it still changes effectively the database migration in some cases. So I guess the code path that does that, should be removed from the framework, if it like you said a not strong enough marker. And that is independent from #19546

paule96 avatar Oct 23 '24 06:10 paule96

@paule96 which scenario are you referring to? Those should have no impact on migrations whatsoever, e.g. for the second step, adding the following:

    public bool AfterInitBool { get; set; } = false;
    [NotNull]
    [Required]
    public bool AfterInitBoolWithAttributes { get; set; } = false;
    [DefaultValue(false)]
    public bool AfterInitBoolWithDefaultValueAttribute { get; set; }


    public bool AfterInitBoolDefaultTrue { get; set; } = true;
    [NotNull]
    [Required]
    public bool AfterInitBoolWithAttributesDefaultTrue { get; set; } = true;
    [DefaultValue(true)]
    public bool AfterInitBoolWithDefaultValueAttributeDefaultTrue { get; set; }


    public bool AfterInitBoolNoDefault { get; set; }
    [NotNull]
    [Required]
    public bool AfterInitBoolWithAttributesNoDefault { get; set; }
    [DefaultValue(17)]
    public bool AfterInitBoolWithDefaultValueAttributeMismatchedType { get; set; }

results in the following migration (note that default value in migration code is the same irrespective of property initializer. Basically, EF thinks default has not been set, so it uses the default value for the data type (which in this case is false)

        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBool",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolDefaultTrue",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolNoDefault",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithAttributes",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithAttributesDefaultTrue",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithAttributesNoDefault",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithDefaultValueAttribute",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithDefaultValueAttributeDefaultTrue",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

            migrationBuilder.AddColumn<bool>(
                name: "AfterInitBoolWithDefaultValueAttributeMismatchedType",
                table: "SampleData",
                type: "bit",
                nullable: false,
                defaultValue: false);

maumar avatar Oct 28 '24 20:10 maumar

Closely related to #27084.

ajcvickers avatar Nov 14 '24 13:11 ajcvickers