Search code examples
c#entity-framework-core.net-8.0t4

Use T4 to replace HasDefaultValue with HasDefaultValueSql in EF Core 8


I'm trying to upgrade from .NET 7 to .NET 8. I have a database-first EF setup. Some values are type changed to enum values via my T4.

In .NET 7, this all works beautifully. Some of these values also have default constraints in our database. In EF Core 8, the scaffolder decides that instead of using HasDefaultValueSql (which would be correct in 100% of instances, since the default value lives in the database), it should use HasDefalutValue, which is not correct for the now-enum values. Same issue this guy on GitHub is having, for greater context.

Is there a way in the T4 or otherwise to programmatically replace the HasDefaultValue with HasDefaultValueSql?

The way this is currently, this is a breaking change for us, despite not being listed on the EF Core 8 breaking changes list.

Before you suggest it, yes, I know removing the default constraints entirely is a solution, however, that is not something the powers that be want to do, or that I really want to do for that matter.

I tried removing the HasDefaultValue line entirely, which I was able to do from the T4, however, that does not fix the issue. I tried to do a string replace of HasDefaultValue with HasDefaultValueSql in the T4 template, but that appeared to do nothing:

propertyFluentApiCalls.Method.Replace("HasDefaultValue", "HasDefaultValueSql");

Solution

  • I figured it out. What was messing with me at first was that I had forgotten that strings are immutable, so my line of code in my question of course didn't change anything, so thanks to Robert Harvey for suggesting code that made me realize that. Here is the code I ended up with. The tricky part was iterating through each method with the ChainedCall attribute of the FluentApiCodeFragment class, because otherwise it would only change HasDefaultValue if it was the first method of the property in the DbContext definition. Iterating through each method ensures all HasDefaultValue uses are found.

    var propertyFluentApiCalls = property.GetFluentApiCalls(annotationCodeGenerator)
        ?.FilterChain(c => !(Options.UseDataAnnotations && c.IsHandledByDataAnnotations)
                           && !(c.Method == "IsRequired" && Options.UseNullableReferenceTypes && !property.ClrType.IsValueType));
    
    if (propertyFluentApiCalls == null)
    {
        continue;
    }
    
    var fluentApiCalls = propertyFluentApiCalls;
    
    while (fluentApiCalls.Method.Contains("HasDefaultValue") && !fluentApiCalls.Method.Contains("HasDefaultValueSql") || fluentApiCalls.ChainedCall != null)
    {
        if (!(fluentApiCalls.Method.Contains("HasDefaultValue") &&
              !fluentApiCalls.Method.Contains("HasDefaultValueSql"))) 
        // If method is not one we're looking for, we want to go to the next one or finish.
        {
            if (fluentApiCalls.ChainedCall == null) 
            // We need to iterate through the ChainedCalls to find any that are HasDefaultValue. 
            // If we've reached the end (ChainedCall == null), then we break.
            {
                break;
            }
            
            fluentApiCalls = fluentApiCalls.ChainedCall; 
            // Going to the next method.
            continue;
        }
    
        fluentApiCalls.Method = fluentApiCalls.Method.Replace("HasDefaultValue", "HasDefaultValueSql");
    
        if (property.FindTypeMapping().ClrType == typeof(System.Boolean))
        {
            if (fluentApiCalls.Arguments[0].ToString() == "True")
            {
                fluentApiCalls.Arguments[0] = "((1))";
            }
            else
            {
                fluentApiCalls.Arguments[0] = "((0))";
            }
        }
        else if (property.FindTypeMapping().ClrType == typeof(System.Int16)
                 || property.FindTypeMapping().ClrType == typeof(System.Int32)
                 || property.FindTypeMapping().ClrType == typeof(System.Decimal)
                 || property.FindTypeMapping().ClrType == typeof(System.Double))
        {
            fluentApiCalls.Arguments[0] = $"(({fluentApiCalls.Arguments[0]}))";
        }
        else if (property.FindTypeMapping().ClrType == typeof(System.String))
        {
            fluentApiCalls.Arguments[0] = $"('{fluentApiCalls.Arguments[0]}')";
        }
    }