【问题标题】:How (and where) to validate API input against a database如何(以及在​​何处)针对数据库验证 API 输入
【发布时间】:2020-10-27 21:35:28
【问题描述】:

我正在创建一个 dotnet core 3.1 API 以在现有系统中工作。 [dbo].[MyTable] 表有两个具有唯一约束的字段:[ID] 和 [HumanReadableID]。 [ID] 是一个 GUID,将在发布新资源时由 API 确定。 [HumanReadableID] 必须由用户确定,因此 API 在执行插入之前必须首先通过查询数据库来确保其唯一性。

我尝试使用自定义属性进行此验证,并在控制器方法接受的 DTO 上实现 IValidatableObject,但我无法将我的存储库注入这两个对象中的任何一个。

目前,控制器尝试使用存储库进行插入,如果 [HumanReadableID] 不唯一,则会引发自定义异常,然后 . . .这是代码

[Route("[controller]")]
[ApiController]
public class MyThingsController : ControllerBase
{

   ///  Other methods

   [HttpPost(Name = "CreateMyThing")]
   public async Task<IActionResult> CreateMyThing(MyThingCreateDto myThingToCreate)
   {
       try
       {
            MyThingEntity entityToCreate = _mapper.Map<MyThingEntity>(myThingToCreate);
            MyThingEntity entityToReturn = await _myRepository.InsertThing(entityToCreate);
            MyThingDto myThingToReturn = _mapper.Map<MyThingDto>(entityToReturn);

            return CreatedAtRoute(
                "GetMyThing",
                new {id = myThingToReturn.Id},
                myThingToReturn);
       }
       catch (DuplicateHumanReadableIdException e)
       {
           ModelState.AddModelError("HumanReadableID",$"HumanReadableID {entityToCreate.HumanReadableId} is already used by site {e.IdOfThingWithHumanReadableId}");
           return Conflict(ModelState);
       }
   }
}

这部分可行,但由此产生的错误的 Content-Type 为 application/json 而不是 application/problem+json。此外,这只是感觉控制器在这里承担了太多责任。

我使用 IValidatableObject 或 DataAnnotations 执行的验证缺少什么?如果做不到这一点,我必须进一步操作控制器的 ModelState 中的哪些内容才能让它发出符合标准的谴责?

【问题讨论】:

  • 我更喜欢将IValidatableObject 定义为“对象数据本身是有效的”。有了这个定义,对象本身是有效的——我们只是不能保存它。另外我认为 dtos 应该是 POCO,你永远不应该在这里使用依赖注入。
  • @ChristophLütjen 谢谢。事实上,我也有同样的感受(虽然我没有能力将这些感受用语言表达出来,所以也谢谢你)。然后你会说这种检查 BELONGS 在控制器中吗?我仍然觉得我已经开始的范式可能会失控,根据捕获各种自定义异常来改变回报。 PUT 和 PATCH 动词也必须实现同样的逻辑。
  • 关于“控制器是正确的位置”,我认为这是通常的“它取决于”。如果你说“动作就是我处理用例的地方”,是的——为什么不呢。您可以添加外观服务或考虑结果对象以简化操作(var result = myServicec.Insert(obj); return this.MyConvertInternalResultObjectToErrorDtoHelper(result); - 但您也可以定义应在控制器之外处理用例,例如中介模式 github.com/jbogard/MediatR

标签: c# validation asp.net-core asp.net-apicontroller


【解决方案1】:

关于如何实现IValidatableObject,你可以关注:

public class ValidatableModel : IValidatableObject
{
    public int Id { get; set; }
    public int HumanReadableID { get; set; }
    public string Name { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        var service = validationContext.GetService<ICheckHumanReadableID>();
        var flag = service.IfExsitHumanId(((ValidatableModel)validationContext.ObjectInstance).HumanReadableID);
        if(flag)
        {
            yield return new ValidationResult(
               $"The HumanReadableID is not unique.",
               new[] { nameof(HumanReadableID) });
        }           
    }
}

型号:

public class Human
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

服务:

public interface ICheckHumanReadableID
{
    bool IfExsitHumanId(int id);
}

public class CheckHumanReadableID : ICheckHumanReadableID
{
    private readonly YourDbContext _context;
    public CheckHumanReadableID(YourDbContext context)
    {
        _context = context;
    }
    public bool IfExsitHumanId(int id)
    {
        var list = _context.Human.Select(a => a.Id).ToList();
        var flag = list.Contains(id);
        return flag;
    }
}

测试方法:

[HttpPost]
public async Task<ActionResult<ValidatableModel>> PostValidatableModel([FromForm]ValidatableModel validatableModel)
{
    if(ModelState.IsValid)
    {
        _context.ValidatableModel.Add(validatableModel);
        await _context.SaveChangesAsync();
        return CreatedAtAction("GetValidatableModel", new { id = validatableModel.Id }, validatableModel);

    }
    return BadRequest(ModelState);
}

数据库上下文:

public class YourDbContext: DbContext
{
    public YourDbContext(DbContextOptions<YourDbContext> options)
        : base(options)
    {
    }

    public DbSet<ValidatableModel> ValidatableModel { get; set; }
    public DbSet<Human> Human { get; set; }
}

Startup.cs:

services.AddDbContext<YourDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("YourConnectionString")));
services.AddScoped<ICheckHumanReadableID, CheckHumanReadableID>();

结果:

【讨论】:

  • 哦!我认为这里的关键是我可以在实现IValidatableObject时通过ValidationContext从IoC容器中获取依赖项!
  • 就是这样。如果我的回答对您有帮助,您能否也接受作为回答?请参阅:How to accept as answer。谢谢。
猜你喜欢
  • 1970-01-01
  • 2014-12-25
  • 1970-01-01
  • 2021-03-28
  • 1970-01-01
  • 2011-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多