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

Easy column overrides #1472

Closed
wijnsema opened this issue Aug 9, 2022 · 20 comments
Closed

Easy column overrides #1472

wijnsema opened this issue Aug 9, 2022 · 20 comments
Labels
enhancement New feature or request reveng

Comments

@wijnsema
Copy link

wijnsema commented Aug 9, 2022

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.

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 9, 2022

Any good suggestions for how to implement this?

@wijnsema
Copy link
Author

wijnsema commented Aug 9, 2022

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!

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 10, 2022

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.

@wijnsema
Copy link
Author

That's great news! Did not know that.

I suppose you are going to support this?

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 11, 2022

Of course I will.

Will you consider updating your review?

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 12, 2022

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

@ErikEJ ErikEJ closed this as not planned Won't fix, can't repro, duplicate, stale Aug 12, 2022
@ErikEJ ErikEJ mentioned this issue Aug 12, 2022
13 tasks
@ErikEJ ErikEJ reopened this Aug 17, 2022
@ErikEJ
Copy link
Owner

ErikEJ commented Aug 17, 2022

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

@wijnsema
Copy link
Author

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.

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 17, 2022

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

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

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 18, 2022

I will try to come up with a sample

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 19, 2022

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!;" : "" #>

@wijnsema
Copy link
Author

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.

@ErikEJ
Copy link
Owner

ErikEJ commented Aug 19, 2022

You are welcome, glad you like it.

@ErikEJ ErikEJ mentioned this issue Sep 7, 2022
16 tasks
@MarvinNorway
Copy link

MarvinNorway commented Jan 1, 2023

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!;" : "" #>
<#

@ErikEJ
Copy link
Owner

ErikEJ commented Jan 2, 2023

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

@MarvinNorway
Copy link

Because the database column is nullable...?

@ErikEJ
Copy link
Owner

ErikEJ commented Jan 2, 2023

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

@ErikEJ ErikEJ mentioned this issue Mar 7, 2023
@yitzolsberg
Copy link

yitzolsberg commented Sep 1, 2023

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...

@dtoland
Copy link

dtoland commented Oct 6, 2023

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.

@5andr0
Copy link

5andr0 commented Dec 6, 2024

For anyone wondering: This does not work with Npgsql.EntityFrameworkCore.PostgreSQL
The enum columns are just dropped and not passed to t4
See npgsql/EFCore.PG/Scaffolding/Internal/NpgsqlDatabaseModelFactory.cs#L389

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request reveng
Projects
None yet
Development

No branches or pull requests

6 participants