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

Ian Kemp
  • 28,293
  • 19
  • 112
  • 138
Anand
  • 1,387
  • 2
  • 26
  • 48
  • `LegalEntity` doesn't have an `Id` or `Name` property. Do those belong to the base class? – dbc Nov 07 '17 at 17:54
  • Yes, those are inherited from the base class – Anand Nov 07 '17 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`. – Chris Pratt Nov 07 '17 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. – Chris Pratt Nov 07 '17 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 – Anand Nov 07 '17 at 18:53
  • This is how we do it in netcore: https://stackoverflow.com/a/54852107/5508250 – Javier Contreras Mar 10 '22 at 04:27

2 Answers2

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • 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 Nov 08 '17 at 16:07
  • @Anand - glad to help. You may want to [cache the serializer for best performance](https://stackoverflow.com/q/33557737/3744182). – dbc Nov 08 '17 at 17:13
0

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; }
        }
    }
    
KarMau
  • 5
  • 3