Reputation: 377
I'm creating a dotnet core 3.1 API to work within an existing system. The [dbo].[MyTable] table has two fields with UNIQUE constraints: [ID] & [HumanReadableID]. [ID] is a GUID that will be determined by the API when new resources are POSTed. [HumanReadableID] must be determined by the user, so the API must first ensure its uniqueness by querying the database before performing the insert.
I've tried doing this validation using a custom attribute and also implementing IValidatableObject
on the DTO accepted by the controller method, but I haven't been able to inject my repository into either of those two objects.
For now, the controller attempts the insert using the repository, which throws a custom exception if [HumanReadableID] is non-unique, and then . . . well here's the code
[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);
}
}
}
This partly works, but the error that comes out of it has a Content-Type of application/json
rather than application/problem+json
. Furthermore this just feels like the controller has too much responsibility here.
What is my implementation of validation using IValidatableObject or DataAnnotations missing? Failing that, what in the controller's ModelState must I manipulate further to allow it to hand out standards-compliant reprimands?
Upvotes: 1
Views: 1552
Reputation: 36705
For how to implement IValidatableObject,you could follow:
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) });
}
}
}
Model:
public class Human
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
Services:
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;
}
}
Test method:
[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);
}
DbContext:
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>();
Upvotes: 2