web-dev-qa-db-fra.com

Liaison de modèle personnalisé API Web d'un objet abstrait complexe

Ceci est une question difficile. J'ai un problème avec la liaison d'un modèle JSON. J'essaie de résoudre polymorphiquement l'alliance fournie avec le type d'enregistrement auquel elle résoudra (je souhaite pouvoir ajouter de nombreux types d'enregistrement à l'avenir). J'ai tenté d'utiliser l'exemple suivant pour résoudre mon modèle lors de l'appel du noeud final. Toutefois, cet exemple ne fonctionne que pour les applications MVC et non les API Web.

J'ai essayé de l'écrire en utilisant IModelBinder et BindModel (HttpActionContext actionContext, ModelBindingContext bindingContext). Cependant, je ne trouve pas l'équivalent de ModelMetadataProviders dans l'espace de noms System.Web.Http.

Appréciez toute aide que n'importe qui peut donner.

J'ai une application Web API 2 qui a la structure d'objet suivante.

public abstract class ResourceRecord
{
    public abstract string Type { get; }
}

public class ARecord : ResourceRecord
{
    public override string Type
    {
        get { return "A"; }
    }

    public string AVal { get; set; }

}

public class BRecord : ResourceRecord
{
    public override string Type
    {
        get { return "B"; }
    }

    public string BVal { get; set; }
}

public class RecordCollection
{
    public string Id { get; set; }

    public string Name { get; set; }

    public List<ResourceRecord> Records { get; }

    public RecordCollection()
    {
        Records = new List<ResourceRecord>();
    }
}

Structure JSON

{
  "Id": "1",
  "Name": "myName",
  "Records": [
    {
      "Type": "A",
      "AValue": "AVal"
    },
    {
      "Type": "B",
      "BValue": "BVal"
    }
  ]
}
5
garyamorris

Après quelques recherches, j'ai découvert que les fournisseurs de métadonnées n'existaient pas dans WebAPI. Pour créer une liaison avec des objets abstraits complexes, vous devez écrire le vôtre.

J'ai commencé par écrire une nouvelle méthode de liaison de modèle, avec l'utilisation d'un sérialiseur JSon de nom de type personnalisé, puis j'ai mis à jour mon noeud final pour utiliser le classeur personnalisé. Il convient de noter que ce qui suit ne fonctionnera qu'avec les demandes dans le corps, vous devrez écrire quelque chose d'autre pour les demandes dans l'en-tête. Je suggérerais une lecture du chapitre 16 de Expert Web API ASP.NET 2 d’Adam Freeman pour les développeurs MVC et la liaison d’objets complexes.

J'ai pu sérialiser mon objet à partir du corps de la demande en utilisant le code suivant.

Configuration WebAPI

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Services.Insert(typeof(ModelBinderProvider), 0,
            new SimpleModelBinderProvider(typeof(RecordCollection), new JsonBodyModelBinder<RecordCollection>()));
    }
}

Classeur de modèle personnalisé

public class JsonBodyModelBinder<T> : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext,
        ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(T))
        {
            return false;
        }

        try
        {
            var json = ExtractRequestJson(actionContext);

            bindingContext.Model = DeserializeObjectFromJson(json);

            return true;
        }
        catch (JsonException exception)
        {
            bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);

            return false;
        }


        return false;
    }

    private static T DeserializeObjectFromJson(string json)
    {
        var binder = new TypeNameSerializationBinder("");

        var obj = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings
        {
            TypeNameHandling = TypeNameHandling.Auto,
            Binder = binder
        });
        return obj;
    }

    private static string ExtractRequestJson(HttpActionContext actionContext)
    {
        var content = actionContext.Request.Content;
        string json = content.ReadAsStringAsync().Result;
        return json;
    }
}

Liaison de sérialisation personnalisée

public class TypeNameSerializationBinder : SerializationBinder
{
    public string TypeFormat { get; private set; }

    public TypeNameSerializationBinder(string typeFormat)
    {
        TypeFormat = typeFormat;
    }

    public override void BindToName(Type serializedType, out string assemblyName, out string typeName)
    {
        assemblyName = null;
        typeName = serializedType.Name;
    }

    public override Type BindToType(string assemblyName, string typeName)
    {
        string resolvedTypeName = string.Format(TypeFormat, typeName);

        return Type.GetType(resolvedTypeName, true);
    }
}

Définition du point final

    [HttpPost]
    public void Post([ModelBinder(BinderType = typeof(JsonBodyModelBinder<RecordCollection>))]RecordCollection recordCollection)
    {
    }
13
garyamorris

La classe TypeNameSerializationBinder n'est plus nécessaire ainsi que la configuration WebApiConfig.

Tout d'abord, vous devez créer une énumération pour le type d'enregistrement:

public enum ResourceRecordTypeEnum
{
    a,
    b
}

Ensuite, changez votre champ "Type" dans ResourceRecord pour qu'il soit l'énumération que nous venons de créer:

public abstract class ResourceRecord
{
    public abstract ResourceRecordTypeEnum Type { get; }
}

Maintenant, vous devriez créer ces 2 classes:

Model Binder

public class ResourceRecordModelBinder<T> : IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(T))
            return false;

        try
        {
            var json = ExtractRequestJson(actionContext);
            bindingContext.Model = DeserializeObjectFromJson(json);
            return true;
        }
        catch (JsonException exception)
        {
            bindingContext.ModelState.AddModelError("JsonDeserializationException", exception);
            return false;
        }
    }

    private static T DeserializeObjectFromJson(string json)
    {
        // This is the main part of the conversion
        var obj = JsonConvert.DeserializeObject<T>(json, new ResourceRecordConverter());
        return obj;
    }

    private string ExtractRequestJson(HttpActionContext actionContext)
    {
        var content = actionContext.Request.Content;
        string json = content.ReadAsStringAsync().Result;
        return json;
    }
}

Classe de conversion

public class ResourceRecordConverter : CustomCreationConverter<ResourceRecord>
{
    private ResourceRecordTypeEnum _currentObjectType;

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var jobj = JObject.ReadFrom(reader);
        // jobj is the serialized json of the reuquest
        // It pulls from each record the "type" field as it is in requested json,
        // in order to identify which object to create in "Create" method
        _currentObjectType = jobj["type"].ToObject<ResourceRecordTypeEnum>();
        return base.ReadJson(jobj.CreateReader(), objectType, existingValue, serializer);
    }

    public override ResourceRecord Create(Type objectType)
    {
        switch (_currentObjectType)
        {
            case ResourceRecordTypeEnum.a:
                return new ARecord();
            case ResourceRecordTypeEnum.b:
                return new BRecord();
            default:
                throw new NotImplementedException();
        }
    }
}

Manette

[HttpPost]
public void Post([ModelBinder(BinderType = typeof(ResourceRecordModelBinder<RecordCollection>))] RecordCollection recordCollection)
{ 
}
1
MasterPiece