从模型验证中排除一种类型(例如DbGeography),以避免InsufficientExecutionStackException。

24

更新: 简短版本请跳到底部


我有一个相当简单的JsonConverter子类,我正在Web 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返回后,应用程序从未将绑定对象返回到操作方法,因为它似乎被卡在无限验证循环中。
这是我暂停执行时调用堆栈的顶部:
当我暂停执行时,每次看起来都在大约相同的深度处,因此似乎没有递归更深。
如果我强制JsonConverter返回null,控制返回到API控制器操作方法,我假设是因为没有要验证的内容。
我没有足够的脑汁来解决这个问题。我做错了什么?
更新:脑汁有些恢复,我已经逐步执行了大部分代码,似乎在验证模型时,DefaultBodyModelValidator正在向下钻入SqlTypesAssembly并陷入循环读取属性的某个地方。我不想找出确切的位置,因为我不想让DefaultBodyModelValidator开始钻进DbGeography类型实例。
模型验证没有理由深入DbGeography类。我需要弄清楚如何使MediaTypeFormatterCollection.IsTypeExcludedFromValidation方法对typeof(DbGeography)返回true,这将导致DefaultBodyModelValidator对任何DbGeography实例执行浅层验证。那么现在问题是 - 如何排除模型验证中的一种类型?DefaultBodyModelValidator的ShouldValidateType方法标记为虚拟方法,但是否没有一种简单的方法在启动时添加一个排除类型?
5个回答

37
无论这个问题是Web API的一个bug还是一个限制,我不清楚。但是这里是我的解决方法: 首先,我们需要从`DefaultBodyModelValidator`派生子类,并覆盖`ShouldValidateType`方法。
public class CustomBodyModelValidator : DefaultBodyModelValidator
{
    public override bool ShouldValidateType(Type type)
    {
        return type!= typeof(DbGeography) && base.ShouldValidateType(type);
    }
}

现在在 global.asax 的 Application_Start 方法中添加

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

就是这样了。现在将对DbGeography类型实例进行浅层验证,一切都会很好地绑定。


根本原因很可能是问题#1024,它改变了无限图中循环的行为 - 访问的对象现在仅通过引用进行比较(即使具有自定义哈希码/等于)。如果您的某些属性会动态生成新实例,则可能会进入无限递归,因为会生成新节点并且不会被检测为先前访问过的(新实例=不同的引用)。TypeHelper.IsSimpleType和ShouldValidateType(DefaultBodyModelValidator.cs line123)中的类型不会被递归遍历。 - jahav
我在将这个代码放入我的 Application_Start 中遇到了问题 -- 是否有其他地方可以放置它?问题在于 GlobalConfiguration.Configuration 在 global.asax 加载时还没有准备好。 - jocull
2
在我们的情况下,我们有一个自定义的HttpConfiguration作为区域注册的一部分。解决方案是使用它,并执行HttpConfiguration.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator()); - jocull
2
这在Web API 5.2.3中仍然存在,但上述修复方法并不能解决它。有人有其他建议吗?从服务中删除所有ModelValidators可以解决问题,但是那样就不会执行任何验证。我至少需要数据注释验证。我尝试创建自己的自定义验证程序提供程序,它扩展了DataAnnotationsModelValidatorProvider并覆盖了GetValidators以在模型元数据类型为DbGeography时返回一个空列表,但似乎也不起作用! - Breeno
1
我相当确定这不起作用是因为我在我的模型上使用了DataAnnotation验证属性,因此DataAnnotationModelValidatorProvider被添加到我的服务列表中 - 如果我清除所有类型为ModelValidatorProvider的服务(如https://dev59.com/7GYq5IYBdhLWcg3w2EFi/),它可以工作,但那样我就没有任何验证了。我还尝试过子类化DataAnnotationModelValidatorProvider以返回任何与DbGeography匹配的ModelMetadata类型的0个验证器,但也不起作用!我也尝试过与上述内容一起使用,仍然没有成功。有人有什么想法吗? - Breeno
显示剩余2条评论

3

joelmdev的回答让我朝着正确的方向前进,但是在我的MVC和WebApi 5.2.3配置中,当将新验证器放置在Global.asax中时,它不会被调用。

解决方法是将其与其他WebApi路由一起放入WebApiConfig.Register方法中:config.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());


谢谢。这个解决方案对我很有效。在MVC应用程序中,这是正确的方式,在纯WEB.API中,接受的解决方案可行。 - Igor Kleinerman

1

我遇到了完全相同的问题,但是针对自定义类型。经过很多研究后,结合这个线程的知识,问题变得相当合理。自定义类有一个公共只读属性,该属性返回另一个相同类的实例。验证器遍历类的所有属性(即使您根本不进行任何验证),并获取值。如果您的类返回相同类的新实例,则会一次又一次地发生这种情况...... 看起来Geography类中的StartPoint属性也有这个问题。 https://msdn.microsoft.com/en-us/library/system.data.spatial.dbgeography.startpoint(v=vs.110).aspx


0

如果您在Mvc应用程序中遇到了同样的问题,您可能会对此答案感兴趣。它不适合每个人,但由于我只关心纬度和经度,我的最终解决方案不是忽略DbGeography属性,而是指示模型绑定器如何正确验证它们,或者至少如何按照我想要的方式进行验证。这种方法允许标准验证属性正常工作,并允许我使用常规的Html控件来控制纬度和经度。我将它们作为一对进行验证,因为它们最终被绑定到您的模型类上的单个DbGepgraphy属性,因此仅其中一个不足以被视为有效。另外,我认为这需要引用与所使用的SQL Server目标版本相对应的Microsoft.SqlServer.Types Nuget包,但您可能已经引用了它。

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;
    }
}

然后在 Global.asax.cs 中的 Application_Start() 方法中:

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地图,例如,如果地图脚本在选择坐标时也再次设置隐藏字段的值。


0

如果您在WebAPI 5.2.3中遇到此问题,对于我最有效的修复方法就是使用本文描述的技巧的一个变体:https://dev59.com/GWox5IYBdhLWcg3w5IVv#40534310

基本上,我在我的模型中使用了自定义必需属性,针对DbGeography属性,在MVC中它会像正常的Required Attribute一样工作,但在WebApi中,我添加了一个额外的属性适配器,始终将客户端验证规则列表替换为新的、空的列表,以便在WebApi模型绑定中不执行任何验证。

此答案唯一缺失的部分是如何替换WebApi中现有的ModelValidatorProvider:

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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接