2013-06-10 27 views
5

Ho un progetto ASP.NET WebApi su cui sto lavorando. Il capo desidera che i ritorni supportino la "risposta parziale", il che significa che sebbene il modello di dati possa contenere 50 campi, il client dovrebbe essere in grado di richiedere campi specifici per la risposta. Il motivo è che se stanno implementando per esempio un elenco semplicemente non hanno bisogno del sovraccarico di tutti i 50 campi, potrebbero semplicemente volere il Nome, il Cognome e l'Id per generare l'elenco. Finora ho implementato una soluzione usando un Resolver Contratto personalizzato (DynamicContractResolver) in modo tale che quando arriva una richiesta ci sto sbirciando attraverso un filtro (FieldListFilter) nel metodo OnActionExecuting e determini se è presente un campo chiamato "FieldList" e quindi se lo è sto sostituendo l'attuale ContractResolver con una nuova istanza del mio DynamicContractResolver e passo la lista dei campi al costruttore.ASP.NET WebApi e risposte parziali

Alcuni codice di esempio

DynamicContractResolver.cs

protected override IList<JsonProperty> CreateProperties(Type type, Newtonsoft.Json.MemberSerialization memberSerialization) 
    { 
     List<String> fieldList = ConvertFieldStringToList(); 

     IList<JsonProperty> properties = base.CreateProperties(type, memberSerialization); 
     if (fieldList.Count == 0) 
     { 
      return properties; 
     } 
     // If we have fields, check that FieldList is one of them. 
     if (!fieldList.Contains("FieldList")) 
      // If not then add it, FieldList must ALWAYS be a part of any non null field list. 
      fieldList.Add("FieldList"); 
     if (!fieldList.Contains("Data")) 
      fieldList.Add("Data"); 
     if (!fieldList.Contains("FilterText")) 
      fieldList.Add("FilterText"); 
     if (!fieldList.Contains("PageNumber")) 
      fieldList.Add("PageNumber"); 
     if (!fieldList.Contains("RecordsReturned")) 
      fieldList.Add("RecordsReturned"); 
     if (!fieldList.Contains("RecordsFound")) 
      fieldList.Add("RecordsFound"); 
     for (int ctr = properties.Count-1; ctr >= 0; ctr--) 
     { 
      foreach (string field in fieldList) 
      { 
       if (field.Trim() == properties[ctr].PropertyName) 
       { 
        goto Found; 
       } 
      } 
      System.Diagnostics.Debug.WriteLine("Remove Property at Index " + ctr + " Named: " + properties[ctr].PropertyName); 
      properties.RemoveAt(ctr); 
     // Exit point for the inner foreach. Nothing to do here. 
     Found: { } 
     } 
     return properties; 
    } 

FieldListFilter.cs

public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) 
    { 
     if (!actionContext.ModelState.IsValid) 
     { 
      throw new HttpResponseException(HttpStatusCode.BadRequest); 
     } 
     // We need to determine if there is a FieldList property of the model that is being used. 
     // First get a reference to the model. 
     var modelObject = actionContext.ActionArguments.FirstOrDefault().Value; 
     string fieldList = string.Empty; 
     try 
     { 
      // Using reflection, attempt to get the value of the FieldList property 
      var fieldListTemp = modelObject.GetType().GetProperty("FieldList").GetValue(modelObject); 
      // If it is null then use an empty string 
      if (fieldListTemp != null) 
      { 
       fieldList = fieldListTemp.ToString(); 
      } 
     } 
     catch (Exception) 
     { 
      fieldList = string.Empty; 
     } 

     // Update the global ContractResolver with the fieldList value but for efficiency only do it if they are not the same as the current ContractResolver. 
     if (((DynamicContractResolver)GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver).FieldList != fieldList) 
     { 
      GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new DynamicContractResolver(fieldList); 
     } 
    } 

posso quindi inviare una richiesta con il contenuto JSON payload cercando, come ad esempio:

{ 
    "FieldList":"NameFirst,NameLast,Id", 
    "Data":[ 
    { 
     "Id":1234 
    }, 
    { 
     "Id":1235 
    } 
    ] 
} 

e riceverò una risposta in questo modo:

{ 
    "FieldList":"NameFirst,NameLast,Id", 
    "Data":[ 
    { 
     "NameFirst":"Brian", 
     "NameLast":"Mueller", 
     "Id":1234 
    }, 
    { 
     "NameFirst":"Brian", 
     "NameLast":"Mueller", 
     "Id":1235 
    } 
    ] 
} 

credo che utilizzando il ContractResolver potrebbe incorrere in problemi di threading. Se lo cambio per una richiesta, sarà valida per tutte le richieste successive fino a quando qualcuno non la cambierà su un'altra richiesta (sembra così attraverso i test) Se è così, allora non vedo l'utilità per il mio scopo.

In sintesi, sto cercando un modo per disporre di modelli di dati dinamici in modo che l'output di una richiesta sia configurabile dal client su richiesta in base alle richieste. Google implementa questo nella loro web api e lo chiamano "risposta parziale" e funziona alla grande. La mia implementazione funziona, ma a un certo punto temo che sarà interrotta per più richieste simultanee.

Suggerimenti? Suggerimenti?

+0

Solo per FYI ... controlla il supporto della funzione '$ select' per JSON formattatter che sarà disponibile nella prossima versione: https://aspnetwebstack.codeplex.com/wikipage?title=%24select%20and%20%24expand%20support&referringTitle = Specifiche –

+0

Non credo di avere il tempo di aspettare la prossima versione. Abbiamo una demo in meno di 2 mesi e ho molto da implementare. Più precisamente, il mio metodo è fondamentalmente rotto. Avevo implementato sulla base di suggerimenti e tutorial da tutto il Web, ma ho solo alcune domande sulla base di ciò che non comprendo appieno. –

+0

se crei una nuova istanza di 'DynamicContractResolver' su ogni richiesta e la usi come' ContractResolver', allora non ci dovrebbe essere un problema di concorrenza. – muratgu

risposta

5

Una soluzione più semplice che potrebbe funzionare.

Creare una classe modello con tutti i 50 membri con tipi nullable. Assegna valori ai membri richiesti. Basta restituire il risultato nel modo normale.

Nel proprio WebApiConfig.Register() è necessario impostare la gestione del valore nullo.

config.Formatters.JsonFormatter.SerializerSettings = 
     new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }; 
+0

Ora questa sembra un'alternativa interessante che non avevo considerato. Esaminerò subito questo aspetto, concordo sul fatto che semplificherebbe parte del processo e sposterebbe la logica nel livello di traduzione dei dati dell'API, dove potrei avere anche un maggiore controllo. –

+0

Ho implementato questa soluzione e ho eliminato il mio DynamicContractResolver. Questo sembra funzionare molto bene e ha il vantaggio aggiuntivo di essere più leggibile, più comprensibile e dandomi un po 'di flessibilità aggiuntiva per avere cose come "campi di restituzione obbligatori" e così via. Grazie per l'assistenza. –

+0

config.Formatters.JsonFormatter.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; – codebased

1

Non toccare la configurazione. È necessario il risolutore del contratto per ogni richiesta. Puoi usarlo nel tuo metodo di azione come questo.

public class MyController : ApiController 
{ 
    public HttpResponseMessage Get() 
    { 
     var formatter = new JsonMediaTypeFormatter(); 
     formatter.SerializerSettings.ContractResolver = 
       new DynamicContractResolver(new List<string>() 
         {"Id", "LastName"}); // you will get this from your filter 

     var dto = new MyDto() 
       { FirstName = "Captain", LastName = "Cool", Id = 8 }; 

     return new HttpResponseMessage() 
     { 
      Content = new ObjectContent<MyDto>(dto, formatter) 
     }; 
     // What goes out is {"LastName":"Cool","Id":8} 
    } 
} 

In questo modo, vi state incastra JSON tipo di contenuto per i messaggi di risposta, ma si è già preso questa decisione utilizzando una funzionalità specifica Json.NET. Inoltre, nota che stai creando un nuovo JsonMediaTypeFormatter. Quindi, tutto ciò che si configura a quello nella configurazione come la mappatura del tipo di supporto non sarà comunque disponibile con questo approccio.

+0

Badri, non è vero che toccando la configurazione globale mi sto chiudendo in Json. Ho già provato questo inviando le intestazioni della richiesta accettare: application/xml e restituisce xml indipendentemente dal contractresolver perché è bypassato sul percorso di ritorno e utilizza il risolutore xml. Nemmeno io voglio chiudermi in json, è un requisito del progetto che l'utente finale sia in grado di recuperare json o xml su richiesta. Il capo non si preoccupa dei contenuti parziali se scelgono xml, comunque non sono interessato a quel lato del processo a questo punto. –