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);
    }
}
Другие вопросы по тегам