3
[Table("LegalEntity")]
[ModelMetadataType(typeof(LegalEntityMeta))]
public class LegalEntity : Entity<long>
{
}

public class LegalEntityMeta
{
    [JsonProperty(PropertyName = "LegalEntityId")]
    public long Id { get; set; }

    [JsonProperty(PropertyName = "LegalEntityName")]
    public string Name { get; set; }
}

In the Startup.cs ....

        services
            .AddCors(options =>
            {
                options.AddPolicy("CorsPolicy",
                    builder => builder.AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowCredentials());
            })
            .AddAutoMapper(typeof(Startup))
            .AddMvcCore()
            .AddJsonFormatters()
            .AddApiExplorer();

My expectation is to see json with attributes legalEntityId and legalEntityName yet the json produced has id and name as attributes. Can someone pleas help me with how to change the json attributes? Thanks Anand

6
  • LegalEntity doesn't have an Id or Name property. Do those belong to the base class? Commented Nov 7, 2017 at 17:54
  • Yes, those are inherited from the base class Commented Nov 7, 2017 at 18:04
  • Shot in the dark because I don't use this personally, but if memory serves, I think the class you apply ModelMetadataType to must be partial. Commented Nov 7, 2017 at 18:40
  • It's also worth mentioning that that attribute exists to support validation, primarily. It's entirely possible Json.Net simply doesn't pay attention to it, when serializing. Commented Nov 7, 2017 at 18:43
  • If it wasn't a derived class and id and name attribute would have been renamed with the JsonProperty attribute. I hope Json.Net would support JsonProperty for derived attributes some other way if not with ModelMetadataType - otherwise that would be a BIG hole which is improbable Commented Nov 7, 2017 at 18:53

3 Answers 3

6

Json.NET currently has no support for Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute. In Issue #1349: Add support for ModelMetadataType for dotnetcore like supported MetadataTypeAttribute in previous versions a request to implement support for it was declined.

Json.NET does support System.ComponentModel.DataAnnotations.MetadataTypeAttribute, albeit with some limitations described in this answer, however even if this attribute were present in .Net core (not sure it is) it would not help you, because you are trying to use the metadata type of a derived class to rename the properties in a base type, which is not an intended usage for metadata type information. I.e. the following works out of the box (in full .Net):

[System.ComponentModel.DataAnnotations.MetadataType(typeof(EntityMeta))]
public class Entity<T>
{
    public T Id { get; set; }

    public string Name { get; set; }
}

public class EntityMeta
{
    [JsonProperty(PropertyName = "LegalEntityId")]
    public long Id { get; set; }

    [JsonProperty(PropertyName = "LegalEntityName")]
    public string Name { get; set; }
}

But the following does not:

[System.ComponentModel.DataAnnotations.MetadataType(typeof(LegalEntityMeta))]
public class LegalEntity : Entity<long>
{
}

public class LegalEntityMeta
{
    [JsonProperty(PropertyName = "LegalEntityId")]
    public long Id { get; set; }

    [JsonProperty(PropertyName = "LegalEntityName")]
    public string Name { get; set; }
}

Why doesn't Json.NET allow derived type metadata information to modify base type contracts? You would have to ask Newtonsoft, but guesses include:

  1. Json.NET is a contract-based serializer where each type specifies its contract through attributes. It's not intended that one type could rewrite the contract of a second type.

  2. DataContractJsonSerializer and DataContractSerializer work the same way.

  3. Doing so would violate the Liskov substitution principle.

So, what are your options?

  1. You could serialize a DTO in place of your LegalEntity, and use something like to map between then:

    public class LegalEntityDTO
    {
        [JsonProperty(PropertyName = "LegalEntityId")]
        public long Id { get; set; }
    
        [JsonProperty(PropertyName = "LegalEntityName")]
        public string Name { get; set; }
    }
    
  2. You could create a custom JsonConverter for LegalEntity with the necessary logic.

  3. You could create a custom contract resolver with the necessary logic, similar to the one here, for instance the following:

    using System.Reflection;
    
    public class ModelMetadataTypeAttributeContractResolver : DefaultContractResolver
    {
        public ModelMetadataTypeAttributeContractResolver()
        {
            // Default from https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Formatters.Json/JsonSerializerSettingsProvider.cs
            this.NamingStrategy = new CamelCaseNamingStrategy();
        }
    
        const string ModelMetadataTypeAttributeName = "Microsoft.AspNetCore.Mvc.ModelMetadataTypeAttribute";
        const string ModelMetadataTypeAttributeProperty = "MetadataType";
    
        protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
        {
            var properties = base.CreateProperties(type, memberSerialization);
    
            var propertyOverrides = GetModelMetadataTypes(type)
                .SelectMany(t => t.GetProperties())
                .ToLookup(p => p.Name, p => p);
    
            foreach (var property in properties)
            {
                var metaProperty = propertyOverrides[property.UnderlyingName].FirstOrDefault();
                if (metaProperty != null)
                {
                    var jsonPropertyAttribute = metaProperty.GetCustomAttributes<JsonPropertyAttribute>().FirstOrDefault();
                    if (jsonPropertyAttribute != null)
                    {
                        property.PropertyName = jsonPropertyAttribute.PropertyName;
                        // Copy other attributes over if desired.
                    }
                }
            }
    
            return properties;
        }
    
        static Type GetModelMetadataType(Attribute attribute)
        {
            var type = attribute.GetType();
            if (type.FullName == ModelMetadataTypeAttributeName)
            {
                var property = type.GetProperty(ModelMetadataTypeAttributeProperty);
                if (property != null && property.CanRead)
                {
                    return property.GetValue(attribute, null) as Type;
                }
            }
            return null;
        }
    
        static Type[] GetModelMetadataTypes(Type type)
        {
            var query = from t in type.BaseTypesAndSelf()
                        from a in t.GetCustomAttributes(false).Cast<System.Attribute>()
                        let metaType = GetModelMetadataType(a)
                        where metaType != null
                        select metaType;
            return query.ToArray();
        }
    }
    
    public static partial class TypeExtensions
    {
        public static IEnumerable<Type> BaseTypesAndSelf(this Type type)
        {
            while (type != null)
            {
                yield return type;
                type = type.BaseType;
            }
        }
    }
    

    Sample .Net fiddle.

    To serialize directly, do:

    var settings = new JsonSerializerSettings
    {
        ContractResolver = new ModelMetadataTypeAttributeContractResolver(),
    };
    
    var json = JsonConvert.SerializeObject(entity, Formatting.Indented, settings);
    

    To install the contract resolver into Asp.Net Core see here.

    Note I wrote this using full .Net 4.5.1 so it is just a prototype. .Net Core uses a different reflection API, however if you install System.Reflection.TypeExtensions as described here I believe it should work.

Sign up to request clarification or add additional context in comments.

2 Comments

thanks so much! I learned so much from your response and I used the your contract resolver and it works like a charm .AddJsonOptions(options => { options.SerializerSettings.ContractResolver = new ModelMetadataTypeAttributeContractResolver();})
@Anand - glad to help. You may want to cache the serializer for best performance.
0

This is a solution for System.Text.Json

Some class generate with DB first:

public class Entity<T>
{
    public T Id { get; set; }

    public string Name { get; set; }
}

Create MetadataBasedConverter.cs

using System.Reflection;
using System.Text.Json;

namespace Mynamespace.JsonConverter;

public class MetadataBasedConverter : JsonConverter<object>
{
    private PropertyInfo[] GetCombinedProperties(Type type)
    {
        var metadataAttr = type.GetCustomAttribute<MetadataTypeAttribute>();
        var metadataProps = metadataAttr?.MetadataClassType.GetProperties() ?? Array.Empty<PropertyInfo>();
        var originalProps = type.GetProperties();

        return originalProps.Union(metadataProps).ToArray();
    }
    public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using (JsonDocument doc = JsonDocument.ParseValue(ref reader))
        {
            object result = (object)Activator.CreateInstance(typeToConvert)!;

            var combinedProps = GetCombinedProperties(typeToConvert);

            foreach (JsonProperty prop in doc.RootElement.EnumerateObject())
            {
                foreach (var property in combinedProps)
                {
                    var metaAttr = property.GetCustomAttribute<JsonPropertyNameAttribute>();
                    var nameToUse = metaAttr?.Name ?? property.Name;

                    if (prop.Name.Equals(nameToUse, StringComparison.OrdinalIgnoreCase))
                    {
                        var actualProperty = typeToConvert.GetProperty(property.Name);
                        if (actualProperty != null)
                        {
                            object? value = JsonSerializer.Deserialize(prop.Value.GetRawText(), actualProperty.PropertyType);
                            actualProperty.SetValue(result, value);
                            break;
                        }
                    }
                }
            }

            return result;
        }
    }

    public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        var combinedProps = GetCombinedProperties(value.GetType());

        foreach (var property in combinedProps)
        {
            var metaAttr = property.GetCustomAttribute<JsonPropertyNameAttribute>();
            var nameToUse = metaAttr?.Name ?? property.Name;

            var actualProperty = value.GetType().GetProperty(property.Name);
            if (actualProperty != null)
            {
                writer.WritePropertyName(nameToUse);
                JsonSerializer.Serialize(writer, actualProperty.GetValue(value), actualProperty.PropertyType, options);
            }
        }
        writer.WriteEndObject();
    }

    public override bool CanConvert(Type typeToConvert)
    {
        return typeof(object).IsAssignableFrom(typeToConvert);
    }
}

Create a partial class, and add a MetadataTypeAttribute and MetadataBasedConverter

[MetadataType(typeof(Metadata))]
[JsonConverter(typeof(MetadataBasedConverter))]
public partial class Entity<T>
{
    internal sealed class Metadata
    {
        [JsonPropertyName("LegalEntityId")]
        public long Id { get; set; }

        [JsonPropertyName("LegalEntityName")]
        public string Name { get; set; }
    }
}

Comments

-1

Switch to Newtonsoft.Json will help:

  1. Add nuget package Microsoft.AspNetCore.Mvc.Newtonsoft.Json

  2. In Startup.cs -> ConfigureServices (Read https://www.ryadel.com/en/use-json-net-instead-of-system-text-json-in-asp-net-core-3-mvc-projects/ for more information)

    services.AddControllers().AddNewtonsoftJson();

  3. Replace using System.Text.Json.Serialization and use MetadataType instead of ModelMetadataType:

    using Newtonsoft.Json;
    namespace YourDbDataNamespace
    {
        [MetadataType(typeof(UserMetadata))]
        public partial class User {}
    
        public class UserMetadata
        {
            [JsonProperty(PropertyName = "LegalEntityId")]
            int Id { get; set; }
    
            [JsonIgnore]
            public string PasswordHash { get; set; }
        }
    }
    

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.