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

Support for mapping single property to multiple database columns #32503

Closed
GintarasDev opened this issue Dec 4, 2023 · 2 comments
Closed

Support for mapping single property to multiple database columns #32503

GintarasDev opened this issue Dec 4, 2023 · 2 comments

Comments

@GintarasDev
Copy link

GintarasDev commented Dec 4, 2023

What problem are you trying to solve?

Usually, we are storing Enums to the database using their string representation as that makes it more readable for our data analysts and other interested parties that are accessing the raw data from our database.

For one of our properties - we decided to use flag Enums as we want to allow selection of multiple values. Storing flag Enum string representation to the database seems to be quite straightforward. However, data gets stored in a format similar to Option0; Option4; Option5. This is great for human readability, however, this means that when our app is querying the database to get all rows for which Option4 is selected - it has to use SQL CONTAINS or LIKE to find which columns include our string value (and writing C# LINQ that would compile and translate to that is non-trivial, also performance of such approach may not be great).

Describe the solution you'd like

The solution to this problem seems to be mapping the same flags Enum property to 2 different columns in the database. One - binary representation that would be easy to filter using queries, and the other - string representation that would be easy to read for human.

In IEntityTypeConfiguration it could look something like this:

builder.Property(e => e.Options)
    .HasColumnName("OptionsValue");

builder.Property(e => e.Options)
    .HasAdditionalColumn("OptionsHumanReadable") // Indicates that this property should be mapped to another column as well.
    .HasConversion(value => value.ToString());

Currently adding 2 separate configurations for the column simply results in the last one added being used.

To ensure that the columns cannot be edited separately using raw SQL the additional columns could be implemented using stored computed columns.

For our specific case we created the following prototype solution:

public static PropertyBuilder<string> HasHumanReadableColumn<TEntity, TProperty>(
    this EntityTypeBuilder<TEntity> builder,
    Expression<Func<TEntity, TProperty>> propertyExpression) where TProperty : Enum where TEntity : class
    {
        var targetColumnName = builder.Property(propertyExpression).Metadata.GetColumnName();

        var values = (int[])Enum.GetValues(typeof(TProperty));
        var sql = GenerateSQLCase(values[1]);
        foreach (var value in values[2..])
        {
            sql += " + '; ' + " + GenerateSQLCase(value);
        }

        return builder.Property<string>($"{targetColumnName}_HumanReadable")
            .HasComputedColumnSql(sql, stored: true);

        string GenerateSQLCase(int value) => $"CASE WHEN ({targetColumnName} & {value}) = {value} THEN '{Enum.GetName(typeof(TProperty), value)}' ELSE '' END";
    }

This, of course, is very specific to our use case.
The only way to generate 2 columns for the same property without using HasComputedColumnSql and raw SQL seems to be using property with 2 backing fields. However, that seems to be a little invasive and for the use case where the second (human readable) column is only needed in database and will not be used in code - it seems a bit excessive.

Or maybe we're missing something here and there is an easier way of achieving this?

@roji
Copy link
Member

roji commented Dec 4, 2023

using property with 2 backing fields

This seems like a very reasonable solution to me - the human-readable backing field can be private so as not to show up on the type surface, etc. HasComputedColumnSql also seems like a decent approach if you want to keep this completely out of your .NET code (though I really think that's fine too).

We do have #31234 on the backlog for value-converting a single property to multiple columns in the database - that could make this possible. But given we shipped complex types in 8.0, we're unlikely to implement that any time soon. I'd advise implementing one of the above 2 strategies instead.

@roji
Copy link
Member

roji commented Dec 4, 2023

Duplicate of #31234

@roji roji marked this as a duplicate of #31234 Dec 4, 2023
@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Dec 6, 2023
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

3 participants