備忘録

備忘録

ASP.NETでnullが明示的に指定されたかを取得する方法

Ⅰ. はじめに

タイトルの通り「ASP.NETでnullが明示的に指定されたかを取得する方法」です。

Ⅱ. 前提条件

  • .NET 7.0以上

Ⅲ. 手順

1. プログラムを書く

OptionalConverter.cs

// https://stackoverflow.com/questions/71024060
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

public readonly struct Optional<T>
{
  public Optional(T? value)
  {
    this.HasValue = true;
    this.Value = value;
  }

  public bool HasValue { get; }
  public T? Value { get; }
  public static implicit operator Optional<T>(T value) => new Optional<T>(value);
  public override string ToString() => this.HasValue ? (this.Value?.ToString() ?? "null") : "unspecified";
}

public class OptionalConverter : JsonConverterFactory
{
  public override bool CanConvert(Type typeToConvert)
  {
    if (!typeToConvert.IsGenericType) { return false; }
    if (typeToConvert.GetGenericTypeDefinition() != typeof(Optional<>)) { return false; }
    return true;
  }

  public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
  {
    Type valueType = typeToConvert.GetGenericArguments()[0];

    return (JsonConverter)Activator.CreateInstance(
      type: typeof(OptionalConverterInner<>).MakeGenericType(new Type[] { valueType }),
      bindingAttr: BindingFlags.Instance | BindingFlags.Public,
      binder: null,
      args: null,
      culture: null
    )!;
  }

  private class OptionalConverterInner<T> : JsonConverter<Optional<T>>
  {
    public override Optional<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
      T? value = JsonSerializer.Deserialize<T>(ref reader, options);
      return new Optional<T>(value);
    }

    public override void Write(Utf8JsonWriter writer, Optional<T> value, JsonSerializerOptions options) =>
      JsonSerializer.Serialize(writer, value.Value, options);
  }
}

Program.cs

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
    .AddJsonOptions(o => o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault)
    .AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new OptionalConverter()));

Controllers/ApiController

public class TestRequest
{
  [JsonPropertyName("value")]
  [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
  public Optional<string?> Value { get; set; }
}

[HttpPost("test")]
public IActionResult Test(TestRequest request)
{
  Console.WriteLine($"HasValue:{request.Value.HasValue}, Value:{request.Value.Value}");
  return Ok();
}

実行結果

リクエスト内容 出力
{ "value": "abc" } HasValue:True, Value:abc
{ "value": null } HasValue:True, Value:
{ } HasValue:False, Value:

Swagger対応について

  • MapTypeで明示的に指定する事で対応できる
  • Optional<T>に対しては面倒な方法しかない
    ※Tはプリミティブでは無い型。自作クラスなど。
builder.Services.AddSwaggerGen(x =>
{
  x.MapType<Optional<string>>(() => new OpenApiSchema { Type = "string" });
});
Before
After