23

更新:对于 tl;dr 版本跳到底部


我有一个非常简单的 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返回后,应用程序永远不会将绑定对象返回给操作方法,因为它似乎陷入了无限验证循环。

这是我暂停执行时调用堆栈的顶部:

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 元数据, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, 对象容器) 第 94 行 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateProperties(System.Web.Http.Metadata.ModelMetadata 元数据, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) 第 156 行C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator。ValidateNodeAndChildren(System.Web.Http.Metadata.ModelMetadata metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, object container) 第 130 行 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator。 ValidateElements(System.Collections.IEnumerable 模型,System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext 验证上下文)第 176 行 C#

之后,调用的 DefaultBodyModelValidator.Validation* 模式一遍又一遍地重复。每次我暂停执行时,它似乎都处于大致相同的深度,因此它似乎并没有递归地变得更深。

如果我强制 JsonConverter 返回null,控制返回到 API 控制器操作方法,我假设因为没有什么要验证的。

我没有剩下的脑汁来解决这个问题。我究竟做错了什么?


更新:随着脑汁的补充,我已经逐步完成了大部分代码,并且似乎在验证模型时DefaultBodyModelValidator正在深入研究SqlTypesAssembly并陷入循环读取某处的属性。我真的不在乎找出确切的位置,因为我不希望从DefaultBodyModelValidator钻取DbGeography类型实例开始。

模型验证没有理由深入到DbGeography类中。我需要弄清楚如何让MediaTypeFormatterCollection.IsTypeExcludedFromValidation方法返回 true for typeof(DbGeography),这将导致对任何实例DefaultBodyModelValidator执行浅验证。DbGeography所以现在手头的问题是——如何从模型验证中排除一个类型?的ShouldValidateType方法DefaultBodyModelValidator标记为virtual,但是在启动时添加排除类型没有简单的方法吗?

4

5 回答 5

36

这个问题是错误还是 Web API 的限制,我不知道,但这是我的解决方法:

首先,我们需要继承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类型实例执行浅验证,并且一切都很好地绑定。

于 2013-11-13T20:56:21.863 回答
3

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

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

于 2017-10-12T17:22:29.127 回答
1

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

于 2017-03-13T19:04:28.510 回答
0

如果您在 WebAPI 5.2.3 中遇到此问题,解决此问题的唯一方法是使用我在此处描述的技术的变体:https ://stackoverflow.com/a/40534310/2001934

基本上,我在模型中的 DbGeography 属性上使用了一个自定义的必需属性,在 MVC 中它可以完全像普通的必需属性一样工作,但在 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);
于 2020-02-08T18:43:10.417 回答
0

如果您在 Mvc 应用程序中遇到同样的问题,您可能会对这个答案感兴趣。它并不适合所有人,但由于我对纬度和经度完全感兴趣,我对这个问题的最终解决方案不是忽略 DbGeography 属性,而是指示模型绑定器如何正确验证它们,或者至少我想要它们的方式验证。这种方法允许标准验证属性属性工作,并允许我使用常规的 Html 控件来控制纬度和经度。我将它们作为一对进行验证,因为最终它们被绑定到模型类上的单个 DbGepgraphy 属性,因此任何一个都不足以被认为是有效的。另外,我认为这需要参考Microsoft.SqlServer.Types与正在使用的 SQL Server 的目标版本相对应的 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 地图,前提是地图脚本还可以在选择坐标时再次设置隐藏字段的值。

于 2020-02-08T18:33:12.137 回答