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.79k stars 3.19k 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 4 months ago

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

roji commented 4 months 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 4 months 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 4 months 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 2 months 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.

chrisc-ona commented 1 month ago

I need this, although with my model the discriminator not only needs to have a custom name (not $type) but is also a regular property of my type hierarchy used in our code base.

chrisc-ona commented 1 month ago

@atrauzzi @hahn-kev do you have any example conversion code that you could share?

hahn-kev commented 1 month ago

Yeah this is what I did. Super simple https://github.com/sillsdev/harmony/blob/main/src%2FSIL.Harmony%2FDb%2FEntityConfig%2FSnapshotEntityConfig.cs#L20-L25

atrauzzi commented 1 month ago

@chrisc-ona -- https://gist.github.com/atrauzzi/dde4f5e92fb783cb6847ff2b1c2d6710

Hopefully this is of some use. I'll repeat the usual disclaimer when I share it: It works, it seems to work well, but I'm sure there are optimizations or deeper integrations with EF that can be done to make it much better overall.

chrisc-ona commented 1 month ago

@hahn-kev @atrauzzi thanks. Unfortunately I'm not sure how well either of these approaches would work for my particular use case though since where polymorphism applies in our model is not at the column/top level document level, but a few levels deep. It looks like at least for now I'll have to continue treating the column as a string and explicitly convert to/from JSON.