在AspNetCore API控制器中,是否有一种公认的方法来干燥出重复的验证代码?

psm*_*may 0 c# asp.net-core

上下文:我被要求维护的AspNetCore控制器包含类似于以下内容的方法:

    // Get api/Foo/ABCXXX/item/12345
    [HttpGet("{accountId}/item/{itemNumber}")]
    public async Task<ActionResult<ItemViewModel>> GetFoo([FromRoute] string accountId, [FromRoute] int itemNumber)
    {
        if (string.IsNullOrWhiteSpace(accountId))
        {
            return BadRequest("accountId must be provided");
        }

        if (itemNumber < 0)
        {
            return BadRequest("itemNumber must be positive");
        }

        if (!await CanAccessAccountAsync(accountId))
        {
            return Forbid();
        }

        // Returns null if account or item not found
        var result = _fooService.GetItem(accountId, itemNumber);

        if (result == null)
        {
            return NotFound();
        }

        return result;
    }

    // GET api/Foo/ABCXXX
    [HttpGet("{accountId}")]
    public async Task<ActionResult<IEnumerable<ItemViewModel>>> GetFoos([FromRoute] string accountId)
    {
        if (string.IsNullOrWhiteSpace(accountId))
        {
            return BadRequest("accountId must be provided");
        }

        if (!await CanAccessAccountAsync(accountId))
        {
            return Forbid();
        }

        // Returns null if account not found
        var results = _fooService.GetItems(accountId);

        if (results == null)
        {
            return NotFound();
        }

        return Ok(results);
    }
Run Code Online (Sandbox Code Playgroud)

您可能会假设有两种以上的方法,它们的组成部分非常相似。

看着这段代码会使我发痒-似乎有很多重复,但是重复的部分由于包含return语句而无法提取到自己的方法中。

对我来说,这些早期退出是异常而不是返回值,这是有意义的。说,为了论证,我定义了一个包装包裹的异常IActionResult

internal class ActionResultException : Exception
{
    public ActionResultException(IActionResult actionResult)
    {
        ActionResult = actionResult;
    }

    public IActionResult ActionResult { get; }
}
Run Code Online (Sandbox Code Playgroud)

然后,我可以提取一些特定的验证:

    private void CheckAccountId(string accountId)
    {
        if (string.IsNullOrWhiteSpace(accountId))
        {
            throw new ActionResultException(BadRequest("accountId must be provided"));
        }
    }

    private async Task CheckAccountIdAccessAsync(string accountId)
    {
        if (!await CanAccessAccountAsync(accountId))
        {
            throw new ActionResultException(Forbid());
        }
    }

    private void CheckItemNumber(int itemNumber)
    {
        if (itemNumber < 0)
        {
            throw new ActionResultException(BadRequest("itemNumber must be positive"));
        }
    }
Run Code Online (Sandbox Code Playgroud)

并重写控制器以使用它们:

    // Get api/Foo/ABCXXX/item/12345
    [HttpGet("{accountId}/item/{itemNumber}")]
    public async Task<IActionResult> GetFoo([FromRoute] string accountId, [FromRoute] int itemNumber)
    {
        try
        {
            CheckAccountId(accountId);
            CheckItemNumber(itemNumber);
            await CheckAccountIdAccessAsync(accountId);

            // Returns null if account or item not found
            var result = _fooService.GetItem(accountId, itemNumber);

            if (result == null)
            {
                return NotFound();
            }

            return Ok(result);
        }
        catch (ActionResultException e)
        {
            return e.ActionResult;
        }
    }

    // GET api/Foo/ABCXXX
    [HttpGet("{accountId}")]
    public async Task<IActionResult> GetFoos([FromRoute] string accountId)
    {
        try
        {
            CheckAccountId(accountId);
            await CheckAccountIdAccessAsync(accountId);

            // Returns null if account not found
            var results = _fooService.GetItems(accountId);

            if (results == null)
            {
                return NotFound();
            }

            return Ok(results);
        }
        catch (ActionResultException e)
        {
            return e.ActionResult;
        }
    }
Run Code Online (Sandbox Code Playgroud)

为了使其正常工作,我必须将控制器主体包装在中,try以解开异常的动作结果。

我还必须将返回类型还原为IActionResult这是我可能不愿意这样做的原因。我能想到的解决该问题的唯一方法就是对异常和catches 进行更具体的说明,但这似乎只是将WET-ness从验证代码转移到了代码catch块。

// Exceptions
internal class AccessDeniedException : Exception { ... }
internal class BadParameterException : Exception { ... }

    // Controller

    private void CheckAccountId(string accountId)
    {
        if (string.IsNullOrWhiteSpace(accountId))
        {
            throw new BadParameterException("accountId must be provided");
        }
    }

    private async Task CheckAccountIdAccessAsync(string accountId)
    {
        if (!await CanAccessAccountAsync(accountId))
        {
            throw new AccessDeniedException();
        }
    }

    private void CheckItemNumber(int itemNumber)
    {
        if (itemNumber < 0)
        {
            throw new BadParameterException("itemNumber must be positive");
        }
    }

    // Get api/Foo/ABCXXX/item/12345
    [HttpGet("{accountId}/item/{itemNumber}")]
    public async Task<IActionResult> GetFoo([FromRoute] string accountId, [FromRoute] int itemNumber)
    {
        try
        {
            ...
        }
        catch (AccessDeniedException)
        {
            return Forbid();
        }
        catch(BadParameterException e)
        {
            return BadRequest(e.Message);
        }
    }

    // GET api/Foo/ABCXXX
    [HttpGet("{accountId}")]
    public async Task<IActionResult> GetFoos([FromRoute] string accountId)
    {
        try
        {
            ...
        }
        catch (AccessDeniedException)
        {
            return Forbid();
        }
        catch (BadParameterException e)
        {
            return BadRequest(e.Message);
        }
    }
Run Code Online (Sandbox Code Playgroud)

Chr*_*att 5

您可以做一些简单的事情。在这一点上不需要太过分。

首先,检查是否accountId为null或空白是完全多余的。这是路线的一部分;如果没有东西卡在那儿,那您就不会到达这里。

其次,您可以在适当情况下明智地使用路由约束。例如,对于您itemNumber的积极态度:

[HttpGet("{accountId}/item/{itemNumber:min(0)}")]
Run Code Online (Sandbox Code Playgroud)

虽然,老实说,我不确定类似的东西/XXXX/item/-1是否一开始就可以工作。无论如何,指定最小值将覆盖您。

第三,您的CanAccessAccount支票实际上应该通过内置于ASP.NET Core 的基于资源的授权来处理。

总而言之,如果您使用已经可用的功能,那么实际上,您实际上不需要做很多额外的验证,而不必寻找某种“分解”的方法。