【问题标题】:Exclude a type from model validation (example DbGeography) to avoid InsufficientExecutionStackException从模型验证中排除类型(例如 DbGeography)以避免 InsufficientExecutionStackException
【发布时间】:2013-11-12 23:05:27
【问题描述】:

更新:对于 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 metadata, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext, object container) 第 94 行 C# System.Web.Http.dll!System.Web.Http.Validation.DefaultBodyModelValidator.ValidateProperties(System.Web.Http.Metadata.ModelMetadata 元数据,System.Web.Http.Validation.DefaultBodyModelValidator.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 model, System.Web.Http.Validation.DefaultBodyModelValidator.ValidationContext validationContext) 第 176 行 C#

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

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

我没有足够的精力来解决这个问题。我做错了什么?


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

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

【问题讨论】:

    标签: c# asp.net asp.net-web-api json.net


    【解决方案1】:

    这个问题是 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 类型实例执行浅验证,并且一切都很好地绑定。

    【讨论】:

    • 根本原因很可能是issue #1024 改变了无限图中的循环行为 - 现在通过引用比较访问的对象(即使使用自定义哈希码/等于)。如果您的某些属性动态生成新实例,它可能会进入无休止的递归,因为生成了新节点并且未检测到之前访问过的节点(新实例 = 不同的引用)。 TypeHelper.IsSimpleType 和 ShouldValidateType (DefaultBodyModelValidator.cs line123) 中的类型不会递归遍历。
    • 我无法将它放入我的Application_Start —— 还有其他地方可以使用吗?问题是GlobalConfiguration.Configuration 在 global.asax 加载时还没有准备好
    • 我们有一个自定义的HttpConfiguration 作为区域注册的一部分。解决方案是使用它并执行HttpConfiguration.Services.Replace(typeof(IBodyModelValidator), new CustomBodyModelValidator());
    • 这仍然在 Web API 5.2.3 中发生,但上面的修复并没有解决它。有没有人有任何其他建议?从服务中删除所有 ModelValidators 是可行的,但随后不执行任何验证。我至少需要进行数据注释验证。我尝试创建自己的自定义验证器提供程序,它扩展 DataAnnotationsModelValidatorProvider 并覆盖 GetValidators 以在模型元数据类型为 DbGeography 时返回一个空列表,但这似乎也不起作用!
    • 很确定这不起作用,因为我在我的模型上使用 DataAnnotation 验证属性,因此 DataAnnotationModelValidatorProvider 被添加到我的服务列表中 - 如果我清除所有类型为 ModelValidatorProvider 的服务(根据stackoverflow.com/q/14147299),但我根本没有得到任何验证。我还尝试对 DataAnnotationModelValidatorProvider 进行子类化,以便为任何与 DbGeography 匹配的 ModelMetadata 类型返回 0 个验证器,但这也不起作用!而且我也结合上面的方法试过了,还是不开心。有人有什么想法吗??
    【解决方案2】:

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

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

    【讨论】:

    • 谢谢。这个解决方案对我有用。在 MVC 应用程序中,这是要走的路,在纯 WEB.API 中,公认的解决方案有效。
    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        【解决方案5】:

        如果您在 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);
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-01-17
          • 1970-01-01
          • 2018-06-18
          • 2013-01-17
          • 1970-01-01
          • 2020-12-29
          相关资源
          最近更新 更多