Json.Net: метод Html не восстанавливается
Я столкнулся с проблемой, когда созданный мной вспомогательный html-метод ASP.NET MVC не "регенерируется" при каждом вызове.
Цель вспомогательного метода заключается в создании объектов Javascript для использования в среде angularjs. Например, вот фрагмент кода, где используется вспомогательный метод (вызывается из тега script HTML-страницы):
var app = angular.module( "appName", ["ui.bootstrap"] );
app.controller( 'appCtrl', function( $scope ) {
$scope.model = @Html.ToJavascript( Model, new string[] { "FirstName", "LastName", "ID", "Role" } );
} );
Модель - это экземпляр класса, который имеет множество свойств, но я только хочу, чтобы FirstName, LastName, ID и Role были сериализованы в объект javascript.
Вспомогательный метод ToJavascript() определяется в классе statis следующим образом:
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() );
}
}
Это использует Json.NET для фактической сериализации.
Одна из многих интересных особенностей Json.NET заключается в том, что она позволяет на лету определять, какие поля сериализуются. Вот что делает SpecificFieldsResolver. Я определил это следующим образом:
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;
}
}
Меня смущает то, как вызывается CreateProperty(). В частности, кажется, что он вызывается только один раз для каждого типа сериализуемого объекта.
Это проблема, потому что в другом файле cshtml у меня есть другой вызов ToJavascript(), который пытается сериализовать объект того же типа, но с другими полями, которые будут выведены из сериализации:
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" } );
} );
Category и VoterID также являются допустимыми полями класса. Но ToJavascript() не разделяет их. Вместо этого он только сериализует поля, определенные в первом вызове ToJavascript()... даже если этот вызов происходит в другом файле cshtml. Как будто SpecificFieldsResolver запоминает объекты JsonProperty, которые он создает.
Мысли?
Обновить
Спасибо dbc за точную диагностику и предложение обходного пути. Я немного адаптировал его, потому что полагаюсь на разрешение имен верблюдов в Json.NET в нескольких средствах распознавания:
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 );
}
}
Теперь каждый распознаватель, такой как мой SpecificFieldsResolver, который является производным от MaoDefaultContractResolver, автоматически наследует верблюжий корпус, но избегает проблемы с кэшированием, идентифицированной dbc.
1 ответ
Это похоже на ошибку с CamelCasePropertyNamesContractResolver
, Его базовый класс, DefaultContractResolver
, имеет два конструктора: конструктор без параметров и DefaultContractResolver (Boolean)
версия (только что устарела в Json.NET 7.0). Этот параметр имеет следующее значение:
shareCache
Тип: System.Boolean
Если установлено значение true,
DefaultContractResolver
будет использовать кэшированный общий доступ с другими распознавателями того же типа. Совместное использование кэша значительно улучшит производительность с несколькими экземплярами распознавателя, поскольку дорогостоящее отражение произойдет только один раз. Этот параметр может вызвать неожиданное поведение, если предполагается, что разные экземпляры распознавателя дают разные результаты. При значении false настоятельно рекомендуется использовать повторноDefaultContractResolver
случаи сJsonSerializer
,
По умолчанию false
,
К сожалению, конструктор по умолчанию для CamelCasePropertyNamesContractResolver
устанавливает значение в 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
};
}
}
Кроме того, нет второго конструктора с shareCache
вариант. Это ломает твои SpecificFieldsResolver
,
В качестве обходного пути вы можете получить свой резольвер из DefaultContractResolver
и использовать CamelCaseNamingStrategy
сделать отображение имени:
public class IndependentCamelCasePropertyNamesContractResolver : DefaultContractResolver
{
public IndependentCamelCasePropertyNamesContractResolver()
: base()
{
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = true,
OverrideSpecifiedNames = true
};
}
}
public class SpecificFieldsResolver : IndependentCamelCasePropertyNamesContractResolver
{
// Remainder unchanged
}
Обратите внимание, что если вы используете версию Json.NET до 9.0, CamelCaseNamingStrategy
не существует. Вместо этого вложенный кладж CamelCasePropertyNamesContractResolver
можно использовать для сопоставления имен:
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);
}
}