EF Core Web API: foreign key validation

Viewed 746

Say I have these classes:

public class User
{
    public int? Id { get; set; }
    ...
}

public class Wallet
{
    public int? Id { get; set; }
    public string Title {get; set;}
    public User Owner { get; set; }
    ...
}

public class WalletCreationDTO 
{
    [Required]
    public string Title {get; set;}
    [Required]
    public int OwnerId { get; set; }
}

[HttpPost]
public async Task<ActionResult> CreateWallet(WalletCreationDto walletDTO) {...}

I must report any erroneous parameters to the frontend like so:

{  
    "errors": {  
        "ownerId": "User by Id does not exist",  
        "title": "Must not exceed 8 characters"  
    }  
}

How do I go about validating the OwnerId?

  • Manually querying the database seems ineffective
  • The default ASP.NET Core validation doesn't seem to have any annotations related to database access
2 Answers

I ended up using fluent validation as suggested. It's not the most efficient way to do this but I'm taking the tradeoff for pretty errors and simple code.

public class WalletCreateDTO
{
    [Required]
    [MaxLength(20)]
    public string Title { get; set; }

    [Required]
    public int OwnerId { get; set; }

    [Required]
    public string CurrencyCode { get; set; }
}

public class WalletCreateDTOValidator : AbstractValidator<WalletCreateDTO>
{
    public WalletCreateDTOValidator(Data.Database database)
    {
        this.RuleFor(w => w.OwnerId)
            .Must(ownerId => database.Users.Any(user => user.Id == ownerId))
            .WithMessage("User does not exist");
        this.RuleFor(w=> w.CurrencyCode)
            .Must(currencyCode => database.Currencies.Any(currency => currency.Code == currencyCode))
            .WithMessage("Currency does not exist");
    }
}

--- To anyone reading this in the future ---

NuGet package: FluentValidation.AspNetCore

Configuration:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers()
        .AddFluentValidation(config =>
        {
            config.RegisterValidatorsFromAssembly(typeof(WalletCreateDTO).Assembly);
        });
}

Note: This seems to register all the validators in the assembly. For example, I have all my DTOs in Api.Dtos directory/namespace, and it seems to register them all, which is convenient.

Also, there's swagger integration: MicroElements.Swashbuckle.FluentValidation

You can use this in simple mode.

        [HttpPost]
        public async Task<ActionResult> CreateWallet(WalletCreationDto walletDTO)
        {
            bool isValidOwnerId = /*validationLogic => can use service or Repository or etc... */

            if (isValidOwnerId == false)
                ModelState.AddModelError("OwnerId", "errorMessage");


            if (ModelState.IsValid)
            {
                //CreateWallet Loigc......
            }

            //we can get all errors from ModelState
            List<ModelError> allErrors = ModelState.Values.SelectMany(v => v.Errors).ToList();

            /*
             return .... 
            */
        }

I also suggest you read the following two links.

IValidatableObject

fluentvalidation : popular .NET library for building strongly-typed validation rules.

Related