ASP.NET Core Metadatatype Attribute Not Working

ASP.NET Core MetaDataType Attribute not working

ASP.NET Core uses

Microsoft.AspNetCore.Mvc **ModelMetadataType** 

instead of

System.ComponentModel.DataAnnotations.**MetadataType** 

source

Try changing your attribute to [ModelMetadataType(typeof(ComponentModelMetaData))]

How to use ASP.Net core ModelMetadata attribute

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 automapper 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.

MetadataType don't work with partial class in ASP.NET MVC

Ok in fact I'm in ASP.NET Core so it's not MetadataType but ModelMetadataTypeAttribute

Loading MetaData in Aspnet Core

So, after I've asked on the ASP.Net Core forum, thanks to Edward Z answer, I did found a way to make it work.

Basically, instead of creating a partial class with ModelMetadataType, I've created a Class containing all the property that need to be validated (so, I will have a class of this kind for each class created by scaffolding). Something like this.

// Class generated by scaffolding
public partial class Users
{
public int UserId { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public string Email { get; set; }
}

// Class I did create. Notice that the name could be anything
// but should contain at least "Users", which is the name of
// the table generated by scaffolding.
// Also notice that isUnique is a Custom Attribute I've wrote.
public class UsersMetaData
{
[MinLength(2, ErrorMessage = " too short")]
public string UserName { get; set; }
[MinLength(2, ErrorMessage = " too short psw")]
public string Password { get; set; }
[IsUnique(ErrorMessage = "Email already exists")]
public string Email { get; set; }
}

After that, I created a mapper which map an instance of the class auto-generated by scaffolding, to the relative class I've craeted. Something like this:

public static object MapEntity(object entityInstance)
{
var typeEntity = entityInstance.GetType();
var typeMetaDataEntity = Type.GetType(typeEntity.FullName + "MetaData");

if (typeMetaDataEntity == null)
throw new Exception();

var metaDataEntityInstance = Activator.CreateInstance(typeMetaDataEntity);

foreach (var property in typeMetaDataEntity.GetProperties())
{
if (typeEntity.GetProperty(property.Name) == null)
throw new Exception();

property.SetValue(
metaDataEntityInstance,
typeEntity.GetProperty(property.Name).GetValue(entityInstance));
}

return metaDataEntityInstance;
}

Notice that, in the first lines, starting from the type of the instance (which is Users), I retrieve the corresponding class I've created, by adding to that name the string "Metadata". Then, what I do is just to create an instance of the UserMetaData class assigning the same value of the Users instance.

Finally, when this mapper is called? In SaveChanges(), when I do the validation. That's because the validation is made on the UserMetaData class.

public override int SaveChanges()
{
var entities = from e in ChangeTracker.Entries()
where e.State == EntityState.Added
|| e.State == EntityState.Modified
select e.Entity;
foreach (var entity in entities)
{
var metaDataEntityInstance = EntityMapper.MapEntity(entity);
var validationContext = new ValidationContext(metaDataEntityInstance);
Validator.ValidateObject(
metaDataEntityInstance,
validationContext,
validateAllProperties: true);
}

return base.SaveChanges();
}

It's kinda tricky, but it all works!



Related Topics



Leave a reply



Submit