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

时间:2019-07-18 14:23:07

标签: 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);
    }

您可能会假设有两种以上的方法具有非常相似的部分。

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

对我来说,这些早期退出是异常而不是返回值,这是有意义的。假设出于争论的目的,我定义了一个包装IActionResult的异常:

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

    public IActionResult ActionResult { get; }
}

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

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

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

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

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

我还必须将返回类型还原为IActionResultthere are reasons I may prefer not to do that。我能想到的解决该问题的唯一方法就是更加具体地说明异常和catch,但这似乎只是将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);
        }
    }

1 个答案:

答案 0 :(得分:5)

您可以执行一些简单的操作。此时无需太过忙。

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

第二,您可以在适当的情况下明智地使用route constraints。例如,对于您的itemNumber为肯定:

[HttpGet("{accountId}/item/{itemNumber:min(0)}")]

尽管,老实说,我不确定/XXXX/item/-1之类的东西是否一开始就可以使用。无论如何,指定一个最小值将覆盖您。

第三,您的CanAccessAccount检查实际上应该通过内置于ASP.NET Core的resource-based authorization处理。

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