お疲れ様です。放浪軍師です。今回は Azure Functions の Http Trigger のバリデーションについてです。Webアプリを早く作りたいのに色々と調べる事が多くて進まねーな!!!
Http Trigger のバリデーション
受け取った値を検査して、通してよいかどうかを判別することをバリデーションと呼んだりするのですが、Http Trigger で受け取った値はどうやればバリデーションを行う事ができるのかがわからなかったので調べてみました。
System.ComponentModel.DataAnnotations を使用する
一般的には以下のように System.ComponentModel.DataAnnotations を用いるようです。簡単で良いですね。
using System.ComponentModel.DataAnnotations; // ←これを追加using System.Text.Json.Serialization; namespace Data { publicclassCompany() { publicenum CategoryDatas { Admin, User, PowerUser, Customer, } [JsonPropertyName("id")] publicstring? Id { get; set; } [Required] // ←これを追加 [JsonPropertyName("name")] publicstring? Name { get; set; } } }
using Api.Validators.Companies; using Data; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using System.ComponentModel.DataAnnotations; // 追加using System.Text.Json; usingstatic System.Runtime.InteropServices.JavaScript.JSType; namespace Api.HttpTriggers.Companies { publicclassPostCompany { privatereadonly ILogger<GetCompanies> _logger; privatereadonly CosmosClient _cosmosClient; public PostCompany(ILogger<GetCompanies> logger, CosmosClient cosmosClient) { _logger = logger; _cosmosClient = cosmosClient; } [Function(nameof(PostCompany))] publicasync Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route ="companies")] HttpRequest req) { _logger.LogInformation("C# HTTP trigger function processed a post request."); string requestBody =awaitnew StreamReader(req.Body).ReadToEndAsync(); var company = JsonSerializer.Deserialize<Company>(requestBody); if (company ==null) { returnnew BadRequestObjectResult("Invalid request payload."); } // -- 追加 --var validationResults =new List<ValidationResult>(); var validationContext =new ValidationContext(company, null, null); if (!Validator.TryValidateObject(company, validationContext, validationResults, true)) { returnnew BadRequestObjectResult(validationResults); } // -- ここまで -- company.Id = Guid.NewGuid().ToString(); company.CreatedAt = DateTime.UtcNow; var container = _cosmosClient.GetContainer(Environment.GetEnvironmentVariable("CosmosDb"), "companies"); var response =await container.CreateItemAsync(company, new PartitionKey((int)company.Category)); _logger.LogInformation($"{response.RequestCharge}RU 消費しました"); returnnew OkObjectResult(company); } } }
これだけでバリデーションを行ってくれます。レスポンスにメッセージを自動で付けてくれるので、非常に便利ですね!!!また、Required (必須) 以外にもいろいろ用意されているようです。
もっと自在にバリデーションしたい
しかし上記のやり方だと、クラスではない値をバリデーションする場合や、トリガーによってバリデーション内容を変えるなんて事が対応できないはずです。その場合どうするんだろうと思って調べてみました。…が、よさげな記事が見つからなかったので自分で考えてみました。
GitHub
前回使用したブランチを改修しました。
構成
まずディレクトリ構成を変更しました。こんな感じ。
Image may be NSFW.
Clik here to view.
HttpTrigers ディレクトリと Validators ディレクトリを作成し、それぞれを管理するようにしています。
Validator
Validator クラスでは、Validate メソッドにて検証したい値を渡して、その内容によってバリデーションを行い、問題がある場合は IReadOnlyList
using Data; using System.ComponentModel.DataAnnotations; namespace Api.Validators.Companies { publicclassPostCompanyValidator { public IReadOnlyList<ValidationResult> Validate(Company company) { var results =new List<ValidationResult>(); if (string.IsNullOrWhiteSpace(company.Name)) { results.Add(new ValidationResult($"{nameof(Company.Name)}は必須です。")); } if (company.Category ==null) { results.Add(new ValidationResult($"{nameof(Company.Category)}は必須です。")); } if (company.Name =="つけもの") { results.Add(new ValidationResult($"ただし{company.Name}テメーはダメだ。")); } if (company.Name =="キマリ") { results.Add(new ValidationResult($"{company.Name}は通さない。")); } return results; } } }
DI コンテナに登録
前述の Validator クラスは Program.cs にて DI コンテナに登録しておきます。かなり増えそうなので自動でやってくれると嬉しいんですけどね。情報お待ちしております。
using Api.Validators.Companies; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Cosmos.Fluent; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var host =new HostBuilder() .ConfigureFunctionsWebApplication() .ConfigureServices(services => { services.AddApplicationInsightsTelemetryWorkerService(); services.ConfigureFunctionsApplicationInsights(); services.AddSingleton(provider => { var connectionString = Environment.GetEnvironmentVariable("CosmosDBConnection"); var client =new CosmosClientBuilder(connectionString) .WithSerializerOptions(new() { PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase }) .Build(); return client; }); services.AddSingleton<PostCompanyValidator>(); //追加 }) .Build(); host.Run();
HttpTrigger
コンストラクタで先ほど登録した Validator クラスを DI して Validate メソッドを使用します。レスポンスに値が含まれていれば処理を中断してバリデーション内容を返す仕組みです。
using Api.Validators.Companies; using Data; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.Cosmos; using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; using System.Text.Json; usingstatic System.Runtime.InteropServices.JavaScript.JSType; namespace Api.HttpTriggers.Companies { publicclassPostCompany { privatereadonly ILogger<GetCompanies> _logger; privatereadonly CosmosClient _cosmosClient; privatereadonly PostCompanyValidator _validator; public PostCompany(ILogger<GetCompanies> logger, CosmosClient cosmosClient, PostCompanyValidator validator) { // コンストラクタで Validator を DI する _logger = logger; _cosmosClient = cosmosClient; _validator = validator; } [Function(nameof(PostCompany))] publicasync Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route ="companies")] HttpRequest req) { _logger.LogInformation("C# HTTP trigger function processed a post request."); string requestBody =awaitnew StreamReader(req.Body).ReadToEndAsync(); var company = JsonSerializer.Deserialize<Company>(requestBody); if (company ==null) { returnnew BadRequestObjectResult("Invalid request payload."); } // -- 追加 --var validationResults = _validator.Validate(company); if (validationResults.Any()) { returnnew BadRequestObjectResult(validationResults); } // -- ここまで -- company.Id = Guid.NewGuid().ToString(); company.CreatedAt = DateTime.UtcNow; var container = _cosmosClient.GetContainer(Environment.GetEnvironmentVariable("CosmosDb"), "companies"); var response =await container.CreateItemAsync(company, new PartitionKey((int)company.Category)); _logger.LogInformation($"{response.RequestCharge}RU 消費しました"); returnnew OkObjectResult(company); } } }
動作確認
Image may be NSFW.
Clik here to view.
こうすることによりトリガー別にバリデーションを自在に扱う事ができました。
単体テスト
ついでなので、validator の単体テストも書いてみましょう。
using Api.Validators.Companies; using Data; namespace Test.Api.Validators.Companies { [TestClass] publicclassPostCompanyValidatorTest { private PostCompanyValidator _validator =new(); [TestInitialize] publicvoid Setup() { } [TestMethod] publicvoid Validate_正常() { var company =new Company { Name ="Company", Category = Company.CategoryDatas.User }; var results = _validator.Validate(company); Assert.AreEqual(0, results.Count); } [TestMethod] publicvoid Validate_Nameが空() { var company =new Company { Name =string.Empty, Category = Company.CategoryDatas.User }; var results = _validator.Validate(company); Assert.AreEqual(1, results.Count); } [TestMethod] publicvoid Validate_Categoryがnull() { var company =new Company { Name ="Company", Category =null }; var results = _validator.Validate(company); Assert.AreEqual(1, results.Count); } [TestMethod] publicvoid Validate_ただしつけものテメーはダメだ() { var company =new Company { Name ="つけもの", Category = Company.CategoryDatas.User }; var results = _validator.Validate(company); Assert.AreEqual(1, results.Count); } [TestMethod] publicvoid Validate_キマリは通さないi() { var company =new Company { Name ="キマリ", Category = Company.CategoryDatas.User }; var results = _validator.Validate(company); Assert.AreEqual(1, results.Count); } } }
簡単ですね。
まとめ
こういう事は必須だと思うんですけど、なんで記事が見当たらないんでしょうか?もしかしたら私が見つけられていないだけで、もっと良い方法があるかもしれないのですね。ご存じの方は掲示板にでもご一報ください。