dotnet / efcore

EF Core is a modern object-database mapper for .NET. It supports LINQ queries, change tracking, updates, and schema migrations.
https://docs.microsoft.com/ef/
MIT License
13.61k stars 3.14k forks source link

Support inheritence with JSON POCO mapping #27779

Open roji opened 2 years ago

roji commented 2 years ago

System.Text.Json has polymorphic deserialization in 7.0. We should implement a similar scheme here with $type, so that JSON documents we produce are compatible with that.

Originally requested in https://github.com/npgsql/efcore.pg/issues/2321

maumar commented 2 years ago

related: https://github.com/dotnet/efcore/issues/9630

atrauzzi commented 1 year ago

Copied from my original issue


Support in EF7 and upcoming work in EF8 for JSON is coming along nicely, and I was wondering if there might be room to continue expanding on it.

This specific idea is to support of interfaces or otherwise abstract types in JSON columns, through the use of a type hint that gets embedded with the data.

I've accomplished this in the past by writing my own .HasConversion, accompanied by some custom System.Text.Json [de]serialization voodoo. This ended up being quite useful for working with the data after it was retrieved, but obviously didn't fully integrate with EF in terms of being able to filter on properties defined by the contracts.

An example of how this could be useful is for storing arrays/collections as part of a known JSON type:

{
    "configurations": [
        {
            "_discriminator": "configuration-type-1",
            "specificToAll": "All types would have this.",
            "specificToOne": "Only type one would have this."
        },
        {
            "_discriminator": "configuration-type-2",
            "specificToAll": "All types would have this.",
            "specificToTwo": "Only type two would have this."
        }
    ],
    "singleConfiguration": {
        "_discriminator": "configuration-type-3",
        "specificToAll": "All types would have this.",
        "specificToThree": "Only type three would have this."
    }
}

This could be deserialized into something like:

public class ApplicationSettings
{
    public IList<Configuration> Configurations { get; set; } = new();
    public Configuration SingleConfiguration { get; set; }
}

public interface Configuration
{
    public string SpecificToAll { get; }
}

public class ConfigurationTypeOne : Configuration
{
    public string SpecificToAll { get; set; }

    public string SpecificToOne { get; set; }
}

public class ConfigurationTypeTwo : Configuration
{
    public string SpecificToAll { get; set; }

    public string SpecificToTwo { get; set; }
}

public class ConfigurationTypeThree : Configuration
{
    public string SpecificToAll { get; set; }

    public string SpecificToThree { get; set; }
}

New EF-based JSON filtering would only allow filtering on the fields it has reason to expect, so in this case, SpecificToAll. But that alone would be quite powerful as this technique allows for a lot of dynamism in the schema without requiring migrations.

marchy commented 1 year ago

This would be extremely useful.

Currently anything other than simple "complex" structures aren't supported by EF Owned Types (either multi-column-mapped or JSON-column-mapped), limiting their use and having to fall back to string columns and managing our own JSON.

ajcvickers commented 1 month ago

Note for team: should we have a separate issue for this in Cosmos?

roji commented 1 month ago

@ajcvickers I think so... Though we need to more clearly agree on what this means - at this point I think it refers to TPH-style inheritance for complex types.

hahn-kev commented 1 month ago

I've done this myself using the technique mentioned above of having a custom converter. One major issue I ran into is that System.Text.Json requires that the first property be the $type property (docs), however postgres (and I would assume others) didn't round trip the sql where the $type property was first, they don't guarantee the order of fields at all usually since it shouldn't matter. I hacked around this, but it still caused problems and would need to be solved for this to work. Not sure why System.Text.Json has that requirement but removing that requirement would probably make this simpler to implement.

roji commented 1 month ago

One major issue I ran into is that System.Text.Json requires that the first property be the $type property [...]

This limitation has already been removed for .NET 9.0 (issue).

roji commented 1 week ago

Note #28592, which is also about mapping multiple types to the same JSON document (or sub-document), but where the types aren't in a hierarchy - also via the use of a discriminator ($type). We may want to implement these two features together.