Efcore: To support REAL "Custom Type"

Created on 10 Jul 2020  ·  19Comments  ·  Source: dotnet/efcore

https://stackoverflow.com/questions/62791669/entity-framework-custom-type-as-property/62795061#62795061


Entity Framework doesn't support REAL "Custom Type".
"Custom Type" in Entity Framework means "a bag of properties" instead of "one type". This is different from what I expected as "Custom Type".

e.g Custom Type: I want Metadata to store as string in database
``` C#
// Custom Type:
public class Metadata {
public int Field1 {get; }
public string Filed2 {get; }
public double Field3 {get; }

    private Metadata(int xxx /* other params */ )
    {
        // to assign all
    }

    public static Metadata Parse (string pattern) {
       // ... to parse from pattern
       // return new Metadata(xxx);
    }

    public override string ToString(){
       // ... string like "abc.id]afe[dfe]fefa"
       // using a special encoding
    }
}

// Custom Type Converter
class MetadataConverter : ValueConverter
{
public MetadataConverter(ConverterMappingHints? hints = null) :
base(data => data.ToString(), data => Metadata.Parse(data), mappingHints)
{
}
}

// Entity Class:
public class Student{
public string Name{get; set; }
// to store as string
public MetaData Meta{get; set; }
}

If I add `[Own]` attribute to `Metadata`, there will be three properties: `int Field1`, `string Field2`, `double Field3`.
But I want to store itself as "one string"
Metadata works as entity, not a property. It is "a bag properties".


``` C#
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
            {
                // "Metadata" appears here as an Entity
                _logger.Info("Model Entity: {0}", entityType);
                foreach (IMutableProperty property in entityType.GetProperties())
                {
                     // I want "Metadata" appears here as a property.

I want Metadata apears as a property in IMutableProperty property in entityType.GetProperties()

I can apply an attribute like CustomType( type of converter) to Metadata.

``` C#
[CustomType(typeof(MetadataConverter))]
public class Metadata {
...
}

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes())
        {
            foreach (IMutableProperty property in entityType.GetProperties())
            {
                 // I want "Metadata" appears here as a property.

```

closed-question customer-reported

All 19 comments

Did you configure Metadata as property with custom value converter? See documentation

Did you configure Metadata as property with custom value converter? See documentation

Yes, of course. But it is about Enum type. With enum type, primitive types or other predefined types such as DateTime, GUID, it works fine.

It is not for my "Custom Type" Metadata

@HalfLegend to clarify, custom value converters can be used to convert from any type to any type, not just enums (that's just what the docs example shows). So you can have your Metadata class be converted to a string column using whatever representation you want. One fairly common thing is to serialize arbitrary instances to JSON, and parse them back, for example.

@roji Yes, Yes, Yes. It SHOULD BE like what you said. This is what I expected!!!

But it isn't!

The interface ValueConverter is designed to convert from any type to any type, no problem. The interface has the ablity to do so, does not mean it has the chance to do so. Because I CANNOT APPLY a coverter to an entity type.

If you have the solution, please give me a short demo how to apply the value converter to an entity type. Thanks a lot.

https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.metadata.imutableentitytype?view=efcore-3.1
'IMutableProperty.GetProperties()' is documted as: "This API only returns scalar properties and does not return navigation properties"

Does that mean, the current design ingores "Custom type" or known as "scalar properties" to support value converter ?

Because I CANNOT APPLY a coverter to an entity type.

@HalfLegend can you provide more details on why you're trying to apply a value converter to an entity type? Entity types are usually mapped to database tables (i.e. each property on the entity gets mapped to a database column). Value converters are meant for use with any non-entity types, to convert them to any type that the database supports (e.g. string), and which thus can be stored in a single database column.

Does that mean, the current design ingores "Custom type" or known as "scalar properties" to support value converter ?

From the point of view of EF Core, any .NET type which is mapped to a database column via a value converter is considered a scalar (you can thing of "scalar" here as something which fits in a simple column).

To try all this out, simply use the code samples in the value conversion docs, but instead of the EquineBeast enum just substitute your own arbitrary .NET type (which isn't an entity type).

@roji As a matter of fact, I don't want my Metadata to be an entity. It is not a table at all. It is just a simple type like Guid.
It is EF core who treated it as an entity.

I want it to be a non-entity type.
I want it to be a property that could use ValueConverter.
But how?

I just want my "Custom Type" Metadata to act like a simple type like Guid, DataTimeOffset.
Why "Guid" is not treated as "scalar" but my Metadata is a "scalar property"?

@roji Thanks very much

substitute your own arbitrary .NET type (which isn't an entity type).

I have to use .NET Type?
Does that mean, EF Core doesn't support "Custom Type"?

But "Custom Type" is a very ordinary and simple behavior, don't you think so?

What do you mean by a "custom type" that isn't a .net type? Can you please provide a small code sample of what you want to do?

Guid and DateTimeOffset are already supported natively by most database providers, what's your reason for wanting to set a value converter with these? Again, a code sample would be helpful.

@roji

What do you mean by a "custom type" that isn't a .net type? Can you please provide a small code sample of what you want to do?

Yes? My Metadata for example, in the question.
The reson is I want to store Metadata as string, json for example.

Your Metadata above is a .NET type, isn't it?

@HalfLegend Here's an example that saves and queries with custom serialization. (For the sake of the example, I just serialized the object to and from JSON.)

```C#
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Microsoft.Extensions.Logging;

public class Metadata
{
public int Field1 {get; set; }
public string Filed2 { get; set; }
public double Field3 {get; set; }

private Metadata()
{
}

public Metadata(int field1, string filed2, double field3)
{
    Field1 = field1;
    Filed2 = filed2;
    Field3 = field3;
}

public static Metadata Parse (string pattern) 
    => System.Text.Json.JsonSerializer.Deserialize<Metadata>(pattern);

public override string ToString() 
    => System.Text.Json.JsonSerializer.Serialize(this);

}

public class MetadataConverter : ValueConverter
{
public MetadataConverter() :
base(data => data.ToString(), data => Metadata.Parse(data))
{
}
}

public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public Metadata Meta { get; set; }
}

public static class Program
{
public static void Main()
{
using (var context = new SomeDbContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();

        context.Add(new Student { Meta = new Metadata(1, "2", 3.0)});

        context.SaveChanges();
    }

    using (var context = new SomeDbContext())
    {
        var student = context.Set<Student>().First();
    }
}

}

public class SomeDbContext : DbContext
{
private static readonly ILoggerFactory
Logger = LoggerFactory.Create(x => x.AddConsole()); //.SetMinimumLevel(LogLevel.Debug));

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>().Property(e => e.Meta).HasConversion(new MetadataConverter());
}

 protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
     => optionsBuilder
         .UseLoggerFactory(Logger)
         .EnableSensitiveDataLogging()
         .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=ConTest;ConnectRetryCount=0");

}
```

What? why closed my question? It is a solution, but not a good one.

@ajcvickers You mean:
Though “ It's right there in the class definition as a property: public Date CreateDate{get; set; } ”, I could not enumerate it from the properties collection. (Because it is an entity type in the collection)
Though " I could not enumerate it from the properties collection", I can still use it with modelBuilder.Entity<Student>().Property(e => e.Meta).HasConversion(new MetadataConverter()); ?

You mean, the item is not in the collection, but you could get it from the collection?

I don't think this solved my problem. it is a weird behavior, and it is so weak.

I want this behavior, not a compromise solution

@HalfLegend I personally haven't been able to understand what it is that you're looking for (see my questions above), and why @ajcvickers's example isn't sufficient for you.

Please take the time to write a full code sample (runnable console program) that shows what you'd like to do.

Thanks very much for repying me.

First, I think C# is a "perfect" language which attract me so much. I love C# and I love Entity Framework.
It is not only a language to support my project, It is also a work of art.

It is not only my art, it is your art. It is the art of OURS.

Back to the solution:
Current behavior:
I could use this only when I know what the certain property is.
The item is NOT IN the collection, BUT I could get the certain item from the collection.

What I expected:
I need a collection that contains the property, SO I could get the item from the collection.
This two behaviors should MATCH.

The problem is obvious: for those I could only use enumeration, for those I don't know the certain property.
I don't know the certain property Property(e => e.Meta), I need a reflection to handle this.

For example:
One person is writing the business code:
``` C#
[Conversion(MetadataConverter)]
public class Metadata {
public int Field1 {get; }
public string Filed2 {get; }
public double Field3 {get; }

    private Metadata(int xxx /* other params */ )
    {
        // to assign all
    }


    public static Metadata Parse (string pattern) {
       // ... to parse from pattern
       // return new Metadata(xxx);
    }

    public override string ToString(){
       // ... string like "abc.id]afe[dfe]fefa"
       // using a special encoding
    }
}

class MetadataConverter : ValueConverter<Metadata, string>
{
    public MetadataConverter(ConverterMappingHints? hints = null) :
        base(data => data.ToString(), data => Metadata.Parse(data), mappingHints)
    {
    }
}

Another person is writing the **_framework_**:
He **does not know** and **does not needs to know** what the entity type is. He also doesn't know which property needs this behavior.
It is the first persion to decided which property should be converted. 

What he knows is an attribute `Conversion`:
``` C#
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct)]
    public class ConversionAttribute : Attribute
    {
        public Type ConverterType { get; }

        public ConversionAttribute(Type converterType)
        {
            ConverterType = converterType;
        }
    }

C# protected override void OnModelCreating(ModelBuilder modelBuilder) { foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) { foreach (IMutableProperty property in entityType.GetProperties()) { // I want "Metadata" appears here as a property. // So I could reflect whether it has the attribute "Conversion" and to apply the converter

Am I clear?

@HalfLegend OnModelCreating is the place where you build the model for EF to use. This includes telling EF which properties are mapped. The model is built up using information you provide about the mappings. Initially, EF Core maps Metadata as an entity type because that's the default for types referenced from other types. It's only after you tell EF to map this as a property that the model changes. At this point the Meta property will show in the collection you are looking that.

Was this page helpful?
0 / 5 - 0 ratings