Исключите тип из проверки модели (пример DbGeography), чтобы избежать исключения InsufficientExecutionStackException.

ОБНОВЛЕНИЕ: для версии tl;dr пропустить вниз


У меня есть довольно простой подкласс JsonConverter, который я использую с веб-API:

public class DbGeographyJsonConverter : JsonConverter
{
    public override bool CanConvert(Type type)
    {
        return typeof(DbGeography).IsAssignableFrom(type);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var value = (string)reader.Value;

        if (value.StartsWith("POINT", StringComparison.OrdinalIgnoreCase))
        {
            return DbGeography.PointFromText(value, DbGeography.DefaultCoordinateSystemId);
        }
        else if (value.StartsWith("POLYGON", StringComparison.OrdinalIgnoreCase))
        {
            return DbGeography.FromText(value, DbGeography.DefaultCoordinateSystemId);
        }
        else //We don't want to support anything else right now.
        {
            throw new ArgumentException();
        }
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, ((DbGeography)value).AsText());
    }
}

Проблема в том, что после возврата ReadJson приложение никогда не возвращает связанный объект методу действия, поскольку он застревает в бесконечном цикле проверки.

Вот вершина стека вызовов, когда я приостанавливаю выполнение:

System.Web.Http.dll!System.Web.Http.Metadata.Providers.AssociatedMetadataProvider.GetMetadataForPropertiesImpl.AnonymousMethod__0() Строка 40 C# System.Web.Http.dll!System.Web.Http.Metadata.ModelMetadata.Model.get( ) Строка 85 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, объектный контейнер ) Строка 94 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateProperties(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) Строка 156 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateNodeAndChildren(System.Web.Http.Metadata.ModelMetadata метаданные, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, obj ect container) Строка 130 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateElements(System.Collections.IEnumerable model, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) Строка 176 C#

После этого шаблон вызовов DefaultBodyModelValidator.Validation* повторяется снова и снова. Каждый раз, когда я приостанавливаю выполнение, оно оказывается примерно на одной глубине, поэтому не кажется, что оно становится рекурсивно глубже.

Если я заставлю JsonConverter вернуть null, управление вернется к методу действия контроллера API, как я предполагаю, потому что нечего проверять.

У меня не осталось мозговых соков, чтобы понять это. Что я делаю неправильно?


ОБНОВЛЕНИЕ: С несколько пополненными мозговыми соками я прошел большую часть кода, и оказалось, что при проверке модели DefaultBodyModelValidator углубляется в SqlTypesAssembly и где-то застревает в цикле чтения атрибутов. . На самом деле мне все равно, где именно, потому что я не хочу начинать с DefaultBodyModelValidator углубления в экземпляры типа DbGeography.

Нет причин для валидации модели углубляться в класс DbGeography. Мне нужно выяснить, как заставить метод MediaTypeFormatterCollection.IsTypeExcludedFromValidation возвращать true для typeof(DbGeography), что заставит DefaultBodyModelValidator выполнять неглубокую проверку любых экземпляров DbGeography. Итак, теперь возникает вопрос: как исключить тип из проверки модели? Метод ShouldValidateType DefaultBodyModelValidator помечен как виртуальный, но нет ли простого способа добавить исключенный тип при запуске?


person joelmdev    schedule 12.11.2013    source источник


Ответы (5)


Я не знаю, является ли эта проблема ошибкой или ограничением веб-API, но вот мой обходной путь:

Во-первых, нам нужно создать подкласс DefaultBodyModelValidator и переопределить метод ShouldValidateType.

public class CustomBodyModelValidator : DefaultBodyModelValidator
{
    public override bool ShouldValidateType(Type type)
    {
        return type!= typeof(DbGeography) && base.ShouldValidateType(type);
    }
}

Теперь в методе Application_Start global.asax добавьте

GlobalConfiguration.Configuration.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());

и это все. Поверхностная проверка теперь будет выполняться для экземпляров типа DbGeography, и все будет хорошо связываться.

person joelmdev    schedule 13.11.2013
comment
Основной причиной, скорее всего, является ошибка №1024, изменившая поведение циклов в бесконечном графе - посещенные объекты. теперь сравниваются только по ссылке (даже с пользовательским хэш-кодом/равным). Если некоторые из ваших свойств генерируют новые экземпляры на лету, они могут войти в бесконечную рекурсию, потому что новые узлы генерируются, а не обнаруживаются как ранее посещенные (новые экземпляры = разные ссылки). Типы из TypeHelper.IsSimpleType и ShouldValidateType (DefaultBodyModelValidator.cs, строка 123) не обрабатываются рекурсивно. - person jahav; 20.07.2015
comment
У меня возникли проблемы с переносом этого в мой Application_Start -- есть ли другое место, куда его можно было бы положить? Проблема в том, что GlobalConfiguration.Configuration не готов к моменту загрузки global.asax - person jocull; 24.07.2015
comment
В нашем случае у нас был пользовательский HttpConfiguration как часть регистрации области. Решение состояло в том, чтобы использовать это и сделать HttpConfiguration.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator()); - person jocull; 24.07.2015
comment
Это все еще происходит в Web API 5.2.3, но указанное выше исправление не устраняет проблему. У кого-нибудь есть другие предложения? Удаление всех ModelValidators из сервисов работает, но тогда проверка не выполняется. Мне нужно как минимум проверить аннотацию данных. Я попытался создать свой собственный поставщик валидатора, который расширяет DataAnnotationsModelValidatorProvider и переопределяет GetValidators, чтобы возвращать пустой список, когда тип метаданных модели — DbGeography, но это тоже не работает! - person Breeno; 01.03.2017
comment
Почти уверен, что это не работает, потому что я использую атрибуты проверки DataAnnotation в своих моделях, и поэтому DataAnnotationModelValidatorProvider добавляется в мой список сервисов — он работает, если я очищаю все сервисы типа ModelValidatorProvider (согласно stackoverflow.com/q/14147299), но тогда я вообще не получаю проверки. Я также пробовал создавать подклассы DataAnnotationModelValidatorProvider, чтобы возвращать 0 валидаторов для любого типа ModelMetadata, который соответствует DbGeography, но это тоже не работает! И я пробовал это в сочетании с вышеперечисленным тоже, все равно без радости. У кого-нибудь есть идеи?? - person Breeno; 02.03.2017
comment
@Брино, ты когда-нибудь заставлял это работать? Я сталкиваюсь с той же проблемой. - person jtate; 06.02.2020
comment
@jtate Я опубликовал 2 новых ответа на этот вопрос: один описывает, как я решил эту проблему в WebApi, а другой — как я решил ее в приложении Mvc. Надеюсь, вы найдете это полезным. - person Breeno; 08.02.2020

Ответ joelmdev привел меня в правильном направлении, но с моей конфигурацией WebApi в MVC и WebApi 5.2.3 новый валидатор не будет вызываться при размещении в Global.asax.

Решение состояло в том, чтобы поместить его в мой метод WebApiConfig.Register с другими маршрутами WebApi: config.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());

person Sherwin F    schedule 12.10.2017
comment
Спасибо. Это решение сработало для меня. В приложениях MVC это путь, в чистом WEB.API принятое решение работает. - person Igor Kleinerman; 15.07.2018

Просто была точно такая же проблема, но затем с пользовательским типом. После довольно большого исследования это оказалось вполне логичным со знанием этой темы. Пользовательский класс имел общедоступное свойство только для чтения, которое возвращало другой экземпляр того же класса. Валидатор просматривает все свойства класса (даже если вы вообще не выполняете проверку) и получает значение. Если ваш класс возвращает новый экземпляр того же класса, это происходит снова и снова и... Похоже, что свойство StartPoint в классе Geography имеет ту же самую проблему. https://msdn.microsoft.com/en-us/library/system.data.spatial.dbgeography.startpoint(v=vs.110).aspx

person Nico Timmerman    schedule 13.03.2017

Если у вас возникла такая же проблема с приложением Mvc, вам может быть интересен этот ответ. Это подойдет не всем, но, поскольку меня интересовали исключительно широта и долгота, мое окончательное решение этой проблемы состояло не в игнорировании свойств DbGeography, а в том, чтобы проинструктировать компоновщик модели, как правильно их проверять или, по крайней мере, как я хотел. подтверждено. Этот подход позволяет работать стандартным свойствам атрибутов проверки и позволяет мне использовать обычные элементы управления Html для широты и долготы. Я проверяю их как пару, так как в конечном итоге они привязаны к одному свойству DbGepgraphy в вашем классе модели, поэтому одного из них недостаточно, чтобы считаться действительным. Кроме того, я думаю, что для этого требуется ссылка на пакет Microsoft.SqlServer.Types Nuget, который соответствует используемой целевой версии SQL Server, но вы, вероятно, уже ссылаетесь на него.

using System;
using System.Data.Entity.Spatial;
using System.Web.Mvc;

...

public class CustomModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var latitudePropertyValue = bindingContext.ValueProvider.GetValue(string.Concat(bindingContext.ModelName, ".", nameof(DbGeography.Latitude)));
        var longitudePropertyValue = bindingContext.ValueProvider.GetValue(string.Concat(bindingContext.ModelName, ".", nameof(DbGeography.Longitude)));

        if (!string.IsNullOrEmpty(latitudePropertyValue?.AttemptedValue)
            && !string.IsNullOrEmpty(longitudePropertyValue?.AttemptedValue))
        {
            if (decimal.TryParse(latitudePropertyValue.AttemptedValue, out decimal latitude)
                && decimal.TryParse(longitudePropertyValue.AttemptedValue, out decimal longitude))
            {
                // This is not a typo - longitude does come before latitude here
                return DbGeography.FromText($"POINT ({longitude} {latitude})", DbGeography.DefaultCoordinateSystemId);
            }
        }

        return null;
    }
}

Затем пользовательский провайдер, который его использует:

using System;
using System.Data.Entity.Spatial;
using System.Web.Mvc;

...

public class CustomModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(Type modelType)
    {
        if (modelType == typeof(DbGeography))
        {
            return new CustomModelBinder();
        }

        return null;
    }
}

Затем в Application_Start() в Global.asax.cs:

ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());

Затем в моей модели это позволяет мне просто использовать обычный RequiredAttribute:

[Display(Name = "Location")]
[Required(ErrorMessage = "You must specify a location")]
public DbGeography Location { get; set; }

И, наконец, чтобы использовать это в представлении, я создал шаблоны отображения и редактора. В ~/Views/Shared/DisplayTemplates/DbGeography.cshtml:

@model System.Data.Entity.Spatial.DbGeography
@Html.LabelFor(m => m.Latitude), 
@Html.LabelFor(m => m.Longitude)

В ~/Views/Shared/EditorTemplates/DbGeography.cshtml:

@model System.Data.Entity.Spatial.DbGeography
@Html.TextBoxFor(m => m.Latitude), 
@Html.TextBoxFor(m => m.Longitude)

И затем я могу использовать этот шаблон редактора в любом другом обычном представлении, например:

@Html.LabelFor(m => m.Location)
@Html.EditorFor(m => m.Location)
@Html.ValidationMessageFor(m => m.Location, "", new { @class = "error-message" })

Вы также можете использовать скрытые поля в своем представлении редактора и добавить немного JavaScript в свой шаблон, например, для создания карты Google, при условии, что скрипт карты также снова устанавливает значения скрытых полей при выборе координаты.

person Breeno    schedule 08.02.2020

Если у вас возникла эта проблема в WebAPI 5.2.3, единственный способ исправить это, который сработал для меня, — это использовать вариант метода, который я описал здесь: https://stackoverflow.com/a/40534310/2001934

В основном я использовал настраиваемый обязательный атрибут для свойств DbGeography в своих моделях, который в MVC будет работать полностью как обычный обязательный атрибут, но в WebApi я добавил дополнительный адаптер атрибута, который всегда заменяет список проверки клиента. rules с новым пустым списком, чтобы в привязке модели WebApi для тех же свойств не выполнялась проверка.

В этом ответе не хватало только того, как заменить существующий ModelValidatorProvider в WebApi:

DataAnnotationsModelValidationFactory factory = (p, a) => new DataAnnotationsModelValidator(
    new List<ModelValidatorProvider>(), new CustomRequiredAttribute()
);

DataAnnotationsModelValidatorProvider provider = new DataAnnotationsModelValidatorProvider();
provider.RegisterAdapterFactory(typeof(CustomRequiredAttribute), factory);

GlobalConfiguration.Configuration.Services.Replace(typeof(ModelValidatorProvider), provider);
person Breeno    schedule 08.02.2020