Json.Net: Html Helper Method non rigenerante

Sto incontrando un problema in cui un metodo helper ASP.NET MVC html che ho creato non viene “rigenerato” ogni volta che viene chiamato.

Lo scopo del metodo helper è creare oggetti Javascript da utilizzare in un framework angularjs. Ad esempio, ecco uno snippet di codice in cui viene utilizzato il metodo helper (chiamato da un tag script di una pagina html):

var app = angular.module( "appName", ["ui.bootstrap"] ); app.controller( 'appCtrl', function( $scope ) { $scope.model = @Html.ToJavascript( Model, new string[] { "FirstName", "LastName", "ID", "Role" } ); } ); 

Model è un’istanza di una class che ha una varietà di proprietà, ma voglio solo FirstName, LastName, ID e Role per essere serializzato su un object javascript.

Il metodo helper ToJavascript () è definito in una class statis come segue:

  public static HtmlString ToJavascript( this HtmlHelper helper, object toConvert, string[] includedFields = null, Formatting formatting = Formatting.Indented, ReferenceLoopHandling loopHandling = ReferenceLoopHandling.Ignore ) { using( var stringWriter = new StringWriter() ) using( var jsonWriter = new JsonTextWriter( stringWriter ) ) { var serializer = new JsonSerializer() { // Let's use camelCasing as is common practice in JavaScript ContractResolver = new SpecificFieldsResolver( includedFields ), Formatting = formatting, ReferenceLoopHandling = loopHandling, }; // We don't want quotes around object names jsonWriter.QuoteName = false; serializer.Serialize( jsonWriter, toConvert ); return new HtmlString( stringWriter.ToString() ); } } 

Ciò utilizza Json.NET per eseguire la serializzazione effettiva.

Una delle molte funzioni interessanti di Json.NET è che ti consente di definire, al volo, quali campi vengono serializzati. Questo è ciò che fa SpecificFieldsResolver. L’ho definito come segue:

 public class SpecificFieldsResolver : CamelCasePropertyNamesContractResolver { private string[] _included; public SpecificFieldsResolver( string[] included ) { _included = included; } protected override JsonProperty CreateProperty( MemberInfo member, MemberSerialization memberSerialization ) { JsonProperty prop = base.CreateProperty( member, memberSerialization ); bool inclField = ( _included == null ) || _included.Contains( member.Name, StringComparer.CurrentCultureIgnoreCase ); prop.ShouldSerialize = obj => inclField; return prop; } } 

Ciò che mi confonde è il modo in cui CreateProperty () viene chiamato. In particolare, sembra essere chiamato solo una volta per ogni tipo di object serializzato.

Questo è un problema perché in un altro file cshtml ho un’altra chiamata a ToJavascript () che sta tentando di serializzare lo stesso tipo di object, ma con campi diversi da emettere dalla serializzazione:

 var app = angular.module( "app2Name", ["ui.bootstrap"] ); app.controller( 'app2Ctrl', function( $scope ) { $scope.model = @Html.ToJavascript( Model, new string[] { "FirstName", "LastName", "ID", "Role", "Category", "VoterID" } ); } ); 

Categoria e VoterID sono anche campi di class validi. Ma ToJavascript () non li seralizza. Invece, serializza solo i campi definiti nella prima chiamata a ToJavascript () … anche se quella chiamata avviene in un diverso file cshtml. È come se SpecificFieldsResolver ricorda gli oggetti JSONProperty che crea.

Pensieri?

Aggiornare

Grazie a dbc per diagnosticare esattamente cosa c’era di sbagliato e suggerire una soluzione alternativa. L’ho adattato leggermente perché mi affido alla risoluzione del nome del caso cammello di Json.NET in diversi resolver:

 public class CamelCaseNameMapper : CamelCasePropertyNamesContractResolver { public string ToCamelCase( string propertyName ) { return ResolvePropertyName( propertyName ); } } public class MaoDefaultContractResolver : DefaultContractResolver { private CamelCaseNameMapper _mapper = new CamelCaseNameMapper(); protected override string ResolvePropertyName( string propertyName ) { return _mapper.ToCamelCase( propertyName ); } } 

Ora ogni resolver, come my SpecificFieldsResolver, che deriva da MaoDefaultContractResolver eredita automaticamente l’involucro cammello ma evita il problema di memorizzazione nella cache identificato da dbc.

Questo sembra essere un bug con CamelCasePropertyNamesContractResolver . La sua class base, DefaultContractResolver , ha due costruttori: un costruttore senza parametri e una versione DefaultContractResolver (Boolean) (appena resa obsoleta in Json.NET 7.0). Questo parametro ha il seguente significato:

shareCache

  • Tipo: System.Boolean

    Se impostato su true, DefaultContractResolver utilizzerà una cache condivisa con altri resolver dello stesso tipo. La condivisione della cache migliorerà in modo significativo le prestazioni con più istanze del resolver, poiché una riflessione costosa avverrà solo una volta. Questa impostazione può causare comportamenti imprevisti se si suppone che diverse istanze del resolver producano risultati diversi. Se impostato su false, si consiglia vivamente di riutilizzare le istanze DefaultContractResolver con JsonSerializer .

Il valore predefinito è false .

Sfortunatamente, il costruttore predefinito per CamelCasePropertyNamesContractResolver imposta il valore su true :

 public class CamelCasePropertyNamesContractResolver : DefaultContractResolver { public CamelCasePropertyNamesContractResolver() #pragma warning disable 612,618 : base(true) #pragma warning restore 612,618 { NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true, OverrideSpecifiedNames = true }; } } 

Inoltre, non esiste un secondo costruttore con l’opzione shareCache . Questo rompe il tuo campo SpecificFieldsResolver .

Come soluzione alternativa, è ansible ottenere il resolver da DefaultContractResolver e utilizzare CamelCaseNamingStrategy per eseguire il mapping dei nomi:

 public class IndependentCamelCasePropertyNamesContractResolver : DefaultContractResolver { public IndependentCamelCasePropertyNamesContractResolver() : base() { NamingStrategy = new CamelCaseNamingStrategy { ProcessDictionaryKeys = true, OverrideSpecifiedNames = true }; } } public class SpecificFieldsResolver : IndependentCamelCasePropertyNamesContractResolver { // Remainder unchanged } 

Si noti che se si utilizza una versione di Json.NET precedente alla 9.0, CamelCaseNamingStrategy non esiste. Invece è ansible utilizzare un CamelCasePropertyNamesContractResolver nidificato con CamelCasePropertyNamesContractResolver per mappare i nomi:

 public class IndependentCamelCasePropertyNamesContractResolver : DefaultContractResolver { class CamelCaseNameMapper : CamelCasePropertyNamesContractResolver { // Purely to make the protected method public. public string ToCamelCase(string propertyName) { return ResolvePropertyName(propertyName); } } readonly CamelCaseNameMapper nameMapper = new CamelCaseNameMapper(); protected override string ResolvePropertyName(string propertyName) { return nameMapper.ToCamelCase(propertyName); } }