EFCorePowerTools icon indicating copy to clipboard operation
EFCorePowerTools copied to clipboard

Easy column overrides

Open wijnsema opened this issue 2 years ago • 5 comments

It should be easy to override column scaffolding, for example for enum columns.

Instead it's such a hassle that I gave up. There are some hints in issue 572, but this is not enough information for a 'user'.

Without this feature this tools has no real value over the Microsoft tools, at least not for me.

wijnsema avatar Aug 09 '22 16:08 wijnsema

Any good suggestions for how to implement this?

ErikEJ avatar Aug 09 '22 17:08 ErikEJ

Sure Erik,

Sorry for being so blunt with the 1 star review, but I genuinely feel this tools is missing too much features to be a Power Tool. It is my personal opinion, but please don't take it personally! I can only admire the work you put into this.

Now to my suggestion:

Back in the old days, just before Microsoft decided the Entity Framework had to be replaced bij the EF Core (which was missing essential features like group by, geometry and lazy loading and took years to reach the former functionality) I was using T4 with a .tt file. This file was very similar to the efpt.config.json of the tools.

But it had more features, it had actual C# code, for example:

    // Column modification*****************************************************************************************************************
    // Use the following list to replace column byte types with Enums.
    // As long as the type can be mapped to your new type, all is well.
    //EnumsDefinitions.Add(new EnumDefinition { Schema = "dbo", Table = "match_table_name", Column = "match_column_name", EnumType = "name_of_enum" });
    //EnumsDefinitions.Add(new EnumDefinition { Schema = "dbo", Table = "OrderHeader", Column = "OrderStatus", EnumType = "OrderStatusType" }); // This will replace OrderHeader.OrderStatus type to be an OrderStatusType enum

    // Use the following function if you need to apply additional modifications to a column
    // eg. normalise names etc.
    UpdateColumn = (Column column, Table table) =>
    {
        // Example
        //if (column.IsPrimaryKey && column.NameHumanCase == "PkId")
        //    column.NameHumanCase = "Id";

        // .IsConcurrencyToken() must be manually configured. However .IsRowVersion() can be automatically detected.
        //if (table.NameHumanCase.Equals("SomeTable", StringComparison.InvariantCultureIgnoreCase) && column.NameHumanCase.Equals("SomeColumn", StringComparison.InvariantCultureIgnoreCase))
        //    column.IsConcurrencyToken = true;

        // Remove table name from primary key
        //if (column.IsPrimaryKey && column.NameHumanCase.Equals(table.NameHumanCase + "Id", StringComparison.InvariantCultureIgnoreCase))
        //    column.NameHumanCase = "Id";

        // Remove column from poco class as it will be inherited from a base class
        //if (column.IsPrimaryKey && table.NameHumanCase.Equals("SomeTable", StringComparison.InvariantCultureIgnoreCase))
        //    column.Hidden = true;

        // Apply the "override" access modifier to a specific column.
        // if (column.NameHumanCase == "id")
        //    column.OverrideModifier = true;
        // This will create: public override long id { get; set; }

        // Perform Enum property type replacement
        var enumDefinition = EnumsDefinitions.FirstOrDefault(e =>
            (e.Schema.Equals(table.Schema, StringComparison.InvariantCultureIgnoreCase)) &&
            (e.Table.Equals(table.Name, StringComparison.InvariantCultureIgnoreCase) || e.Table.Equals(table.NameHumanCase, StringComparison.InvariantCultureIgnoreCase)) &&
            (e.Column.Equals(column.Name, StringComparison.InvariantCultureIgnoreCase) || e.Column.Equals(column.NameHumanCase, StringComparison.InvariantCultureIgnoreCase)));

        if (enumDefinition != null)
        {
            column.PropertyType = enumDefinition.EnumType;
            if(!string.IsNullOrEmpty(column.Default))
                column.Default = "(" + enumDefinition.EnumType + ") " + column.Default;
        }

        return column;
    };

Or for table renaming:

    // Table renaming *********************************************************************************************************************
    // Use the following function to rename tables such as tblOrders to Orders, Shipments_AB to Shipments, etc.
    // Example:
    TableRename = (name, schema) =>
    {
        // Example
        //if (name.StartsWith("tbl"))
        //    name = name.Remove(0, 3);
        //name = name.Replace("_AB", "");

        // If you turn pascal casing off (UsePascalCase = false), and use the pluralisation service, and some of your
        // tables names are all UPPERCASE, some words ending in IES such as CATEGORIES get singularised as CATEGORy.
        // Therefore you can make them lowercase by using the following
        // return Inflector.MakeLowerIfAllCaps(name);

        // If you are using the pluralisation service and you want to rename a table, make sure you rename the table to the plural form.
        // For example, if the table is called Treez (with a z), and your pluralisation entry is
        //     new CustomPluralizationEntry("Tree", "Trees")
        // Use this TableRename function to rename Treez to the plural (not singular) form, Trees:
        // if (name == "Treez") return "Trees";

        return name;
    };

In short, if there would be overridable functions for the steps in the scaffolding this would make it really flexible, powerful!

wijnsema avatar Aug 09 '22 19:08 wijnsema

Thanks for the suggestion - this will be possible soon with EF Core 7 due to support for (wait for it) .... T4 templates!

Renaming is currently possible using individual renames or a regex.

I would be very grateful if you would consider updating your review - having good reviews is important for me for many reasons.

ErikEJ avatar Aug 10 '22 05:08 ErikEJ

That's great news! Did not know that.

I suppose you are going to support this?

wijnsema avatar Aug 10 '22 22:08 wijnsema

Of course I will.

Will you consider updating your review?

ErikEJ avatar Aug 11 '22 04:08 ErikEJ

Thanks for the review update. I am closing this for now, and will confirm this a part of the EF Core 7 support

ErikEJ avatar Aug 12 '22 10:08 ErikEJ

@wijnsema I implemented this in the latest daily build, would be grateful if you could try it out.

ErikEJ avatar Aug 17 '22 08:08 ErikEJ

Nice work Erik!

I tried it with a test project and it seems to be working OK.

But it turns out I need to learn T4 first... Not that obvious to achieve what I want (enum columns). Did you create the included T4 yourself?

One other problem for me is that I need to step up to .NET7 preview, but my product is running .NET6 in production.

Anyway, great start.

wijnsema avatar Aug 17 '22 14:08 wijnsema

Yeah, there are some hurdles including tooling and learning curve.

These are the default templates provided by the EF Core team.

ErikEJ avatar Aug 17 '22 15:08 ErikEJ

I will try to come up with a sample

ErikEJ avatar Aug 18 '22 06:08 ErikEJ

To replace int values with enum types you can modify EntityType.t4 like this:

Add on line 22:

    var enumList = new List<(string EntityTypeName, string PropertyName, string? EnumType)>();
    // add your enums here:
    //enumList.Add( ("Customer", "Rating", "Rating") );
    //enumList.Add( ("Entity", "Property", "EnumType?") )

Then on line 107 add/update as follows:

        var enumTuple = enumList.SingleOrDefault(t => t.EntityTypeName == EntityType.Name && t.PropertyName == property.Name);
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType && enumTuple.EnumType == null;

        var propertyType = enumTuple.EnumType ?? code.Reference(property.ClrType);
#>
    public <#= propertyType #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>

ErikEJ avatar Aug 19 '22 05:08 ErikEJ

Wow Eric! I tried this out and it works really well!

Thanks quick replies!

This T4 is a great contribution, for me and for everyone I think.

wijnsema avatar Aug 19 '22 13:08 wijnsema

You are welcome, glad you like it.

ErikEJ avatar Aug 19 '22 13:08 ErikEJ

To replace int values with enum types you can modify EntityType.t4 like this:

@ErikEJ You probably meant to write propertyType, not propertyName? I have done as you said and it works really well, except for one thing: The properties are not nullable. So the code changes int? MyProperty { get; set; } to MyEnum MyProperty { get; set; }.

I was able to fix it like this, but I'm not sure if that's the best/right way to do it:

        usings.AddRange(code.GetRequiredUsings(property.ClrType));

        var enumTuple = enumList.SingleOrDefault(t => t.EntityTypeName == EntityType.Name && t.PropertyName == property.Name);
        var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && (enumTuple.EnumType != null || !property.ClrType.IsValueType);
        var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !property.ClrType.IsValueType;
        var propertyType = enumTuple.EnumType ?? code.Reference(property.ClrType);
#>
    public <#= propertyType #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#

MarvinNorway avatar Jan 01 '23 23:01 MarvinNorway

@MarvinNorway Looks fine, but why have a nullable enum in the first place?

ErikEJ avatar Jan 02 '23 06:01 ErikEJ

Because the database column is nullable...?

MarvinNorway avatar Jan 02 '23 07:01 MarvinNorway

@MarvinNorway Yeah, I get that. Thanks for the update to the sample

ErikEJ avatar Jan 02 '23 07:01 ErikEJ

Thanks @ErikEJ for your sample and @MarvinNorway for your addition.

One extra point: I think instead of

var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && (enumTuple.EnumType != null || !property.ClrType.IsValueType);

it should be

var needsNullable = property.IsNullable &&
    (
        (Options.UseNullableReferenceTypes && !property.ClrType.IsValueType)
        || enumTuple.EnumType is not null
    );

Or you could just do enumList.Add( ("Entity", "Property", "EnumType?") ) and use the original sample...

yitzolsberg avatar Sep 01 '23 11:09 yitzolsberg

Erik, I like the direction you are going, I use enums a lot as they create strongly typed items so, I get fewer runtime errors when values change (I get compile time errors :-) ). I did some stuff with an older tool and went to the trouble of building the t4 items to get what I needed. I wanted to drive the enums from the DB. As far as nulls go, I made it a standard to have a zero value (integer driven) item in every enum and it was always defined as 'Undefined' and it is the default value if null comes from the db. I am working against a product that has a lot of DB stuff in it and, I can't control it, but I can read it. Many settings are set by values, and I created some routines that create enums from those settings along with a default set of utilities that get spooled out when the code is generated (Value, Description etc.). I also use 'User Defined Types' with extended properties (Yes, I know not exactly what they were designed for but seem to work well) to create enums for my settings with a similar set of utilities with each generation. I am attaching a sample, if any of it is useful feel free to use etc. Gender_EnumAnomozied.txt EnumFromDBAnoymozied.txt One note: many of the values that come from the DB start with numbers, so I simply put "code_" at the front of them and remove it in the Code() method.

dtoland avatar Oct 06 '23 12:10 dtoland