如何更改 .NET Core 3.1 API 中的路由错误

Posted

技术标签:

【中文标题】如何更改 .NET Core 3.1 API 中的路由错误【英文标题】:How to change routing errors in .NET Core 3.1 API 【发布时间】:2020-09-23 03:17:21 【问题描述】:

我正在将一个 API 从 ASP .NET 4.7 移植到 .NET CORE 3.1,到目前为止,我已经设法完成了大部分工作,但我遇到了一个特定的错误。 我有以下端点

    [HttpGet]
    [Route("contacts/id/")]
    public IActionResult GetContactByID(Guid? id)
    
        if (!id.HasValue)
            return BadParameter("id");

        //Some irrelevant logic here
        Contact foundContact = GetContactFromRepository(id);
        return Ok(foundContact);
    

通过在旧 API 中提供无效的 Guid(“contacts/6cb735b1-b2f0-45d1-a5b5-3a161a5b8c5eTESTINVALIDGUID”) - 将到达端点并且检查 ID 是否具有值的 IF 语句将提供预期的错误响应格式。目前在 CORE 3.1 中,当在相同的请求中,我在 Postman 中得到以下响应:

"errors": 
    "id": [
        "The value '6cb735b1-b2f0-45d1-a5b5-3a1615555555asd' is not valid."
    ]
,
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "|90331587-428887e84126845c."

执行永远不会到达方法。但是控制器的构造函数被调用了。

我已经覆盖了大部分错误消息,并且我同时移植了多个 API 版本(每个版本都有不同的响应)。但我无法弄清楚这个错误来自哪里(以及如何处理它,以便请求真正到达控制器方法 - 以便我可以返回正确的响应)。

【问题讨论】:

.Net Core 似乎无法将字符串解析为 Guid 对象,因为它是无效的,并且框架只是向您返回了 BadRequest 响应。您的代码 if(...) 的功能现在由 .Net Core Framework 完成。除非您输入有效的 Guid,否则该方法将永远不会被调用。 ^^ 换句话说:“这不是一个错误。这是一个功能!”但我明白,你宁愿有自己的错误响应。所以问题将是“如何进入验证过程,以便解析失败的 Guid 传递给 'ok' 和 null 而不是返回 400”,对吗? 是的,您的建议似乎准确地总结了我的需要。 【参考方案1】:

在 ASP.NET Core 中,如果指定 Action Method 的参数类型为Guid,并期望路由获取Guid 值,则它应该是有效的Guid 值,否则路由不会工作。 .Net Core 框架不会解析无效值。

我在 .Net Core 3.1 中测试过以下代码:

    [HttpGet]
    [Route("sitemap/id/")]
    public IActionResult GetSitemap(Guid id)
    
         // ToDo- Perform some operation
    

路线:

http://localhost:6003/api/sitemap/6cb735b1-b2f0-45d1-a5b5-3a1615555555 // Works fine, Valid Request

http://localhost:6003/api/sitemap/6cb735b1-b2f0-45d1-a5b5-3a1615555555aab //Invalid Guid, Bad Request

作为传递 Guid 的替代方法,您可以将参数类型更改为字符串并在操作方法中处理类型检查:

        [HttpGet]
        [Route("sitemap/id/")]
        public IActionResult GetSitemap(string id)
        
            if (id.Length == 36)
            
                Guid value = Guid.Parse(id);
            
            else
            
                return BadRequest();
            
         

【讨论】:

这在技术上是可行的,但在某种程度上它似乎是一个肮脏的解决方案。因为有了这个,我将不得不更改签名 + 为许多不同控制器中的许多方法添加额外的验证,这很容易出现人为错误。我相信会有一种更“正确”的方式在管道中的某个地方实现它一次,之后它将应用于整个 API。 准确地说——“要么遵守框架规则,要么改变设计。”如果我们需要寻找好的架构和设计方法,我猜路由和调用登录需要根据框架进行重组,否则会抛出异常。【参考方案2】:

我认为这篇Validating and passing controller-level parameters with ASP.NET MVC attribute routing 的文章可以解决您的问题。如果您可以使用非常有用的参数ActionExecutingContextActionExecutedContext 明智地覆盖OnActionExecutingOnActionExecuted 方法,那么ActionFilterAttribute 类非常好。

【讨论】:

【参考方案3】:

我想通了! 我对 ModelBinders 进行了更多研究,发现拥有 CustomModelBinder 可以完成工作。

public class GuidModelBinder : IModelBinder

    public Task BindModelAsync(ModelBindingContext bindingContext)
    
        if (bindingContext == null)
        
            throw new ArgumentNullException(nameof(bindingContext));
        

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        


        Guid parsedGuid;
        if (Guid.TryParse(valueProviderResult.FirstValue, out parsedGuid))
        
            bindingContext.Result = ModelBindingResult.Success(parsedGuid);
        
        else
        
            bindingContext.Result = ModelBindingResult.Failed();
        
        return Task.CompletedTask;

    

始终返回“Task.CompletedTask”确保即使 ModelBindingResult 失败也会触发控制器操作。并且在测试时它完全按照预期工作 - 控制器方法被分配给 Guid? Id - Null 的值。

为了不必在每个 ActionMethod 上添加 ModelBinder 属性,我创建了一个 CustomModelBinderProvider 以确保 CustomBinder 仅适用于 Nullable Guids(这是我需要的):

public class GuidModelBinderProvider : IModelBinderProvider

    public IModelBinder GetBinder(ModelBinderProviderContext context)
    
        if (context == null)
        
            throw new ArgumentNullException(nameof(context));
        

        if (context.Metadata.ModelType == typeof(Guid?))
        
            return new GuidModelBinder();
        

        return null;
    

要添加提供程序,我将以下内容放入 Startup.cs

 services.AddControllers(op =>
            op.ModelBinderProviders.Insert(0, new GuidModelBinderProvider());
        )

【讨论】:

以上是关于如何更改 .NET Core 3.1 API 中的路由错误的主要内容,如果未能解决你的问题,请参考以下文章

我需要在启动时在 ASP .Net Core 3.1 Web API 中执行异步方法

如何更改 ASP.NET Core API 中的默认控制器和操作?

.Net Core 3.1 API 中的刷新令牌 Azure 广告身份验证

将项目从 2.2 更新到 3.1(缺少程序集)或如何在 .NET Core 中使用 API POST 请求时,PostAsJsonAsync()在 .Net Core 3.1 中不起作用 [重复]

如何从 Asp.NET Core 3.1 启动类访问 launchSettings.json 中的 `applicationUrl` 属性?

ASP.Net Core 3.1 - Web API 中的 Post 参数始终为 NULL