In my HTTP controller using model binding, how can I determine if a request model property, is NULL
because it was explicitly set to NULL
by the API client, or is NULL
because it was missing in the original HTTP request?
TLDR:
How can I differentiate/discern/(tell the difference) between NULL
and UNDEFINED
in HTTP requests bound to models using model binding, where both NULL
and UNDEFINED
become just NULL
.
-
Better question: why do you care?Ian Kemp - SO dead by AI greed– Ian Kemp - SO dead by AI greed2025年07月29日 14:58:45 +00:00Commented Jul 29 at 14:58
-
@IanKemp-SOdeadbyAIgreed --- To avoid clobbering data in HTTP update requests.olfek– olfek2025年07月29日 15:15:11 +00:00Commented Jul 29 at 15:15
2 Answers 2
This is just wrong design.
As can be read here
A PATCH request performs a partial update to an existing resource. The client specifies the URI for the resource. The request body specifies a set of changes to apply to the resource.
On the other side, PUT request should actually contain all fields that are fetched from API and are used to update the entire entity.
To summarize, it would be best to align with RFC: PATCH Method for HTTP, and as recommended for example here , it should be array of operations:
[
{ "op": "replace", "path": "property1", "value": "NewValue"},
{ "op": "replace", "path": "property2", "value": 10}
]
Implementation:
8 Comments
value
property, when value
is NULL
or UNDEFINED
it becomes NULL
when mapped to a strongly typed data structure, so how do you know whether the NULL
was intentional? As for your PATCH
/PUT
concern, again, the same problem applies, in PUT
the request should contain all fields, ok, but how can we make sure that it actually did? And in PATCH
, a NULL
property for example meaning to delete it from the database, how can we make sure it was intentional? Finally, I'm not using PATCH
or PUT
at all, only POST
, oh no!.NET Core MVC Web API [NULL vs UNDEFINED]
For - <TargetFramework>net7.0</TargetFramework>
or newer.
Step 1 of 3
// https://stackoverflow.com/a/79718639
using System.Linq.Expressions;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace XXX.XXX;
public class JsonKeys<T> where T : class
{
private string[] _jsonKeys = Array.Empty<string>();
public bool HasKey<TReturn>(Expression<Func<T, TReturn>> expression) =>
expression.Body is MemberExpression { Member.MemberType: MemberTypes.Property } memberExpression &&
_jsonKeys.Contains(memberExpression.Member.Name, StringComparer.InvariantCultureIgnoreCase);
public void SetJsonKeys(string[] jsonKeys)
{
if (_jsonKeys.Length > 0) throw new InvalidOperationException($"{nameof(jsonKeys)} is already set");
_jsonKeys = jsonKeys;
}
}
public class JsonKeysInputFormatter : SystemTextJsonInputFormatter
{
public JsonKeysInputFormatter(
JsonOptions options,
ILogger<SystemTextJsonInputFormatter> logger
) : base(options, logger)
{
}
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
var httpContext = context.HttpContext;
var memoryStream = new MemoryStream();
await httpContext.Request.Body.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
httpContext.Request.Body = memoryStream;
var inputFormatterResult = await base.ReadRequestBodyAsync(context);
memoryStream.Seek(0, SeekOrigin.Begin);
var jsonNode = await JsonSerializer.DeserializeAsync<JsonNode>(memoryStream, SerializerOptions);
AddKeyExistence(inputFormatterResult.Model, jsonNode);
await memoryStream.DisposeAsync();
return inputFormatterResult;
}
private static void AddKeyExistence(object? model, JsonNode? jsonNode)
{
if (model == null || jsonNode == null || model.GetType().IsArray || !model.GetType().IsClass) return;
// https://learn.microsoft.com/dotnet/fundamentals/reflection/how-to-examine-and-instantiate-generic-types-with-reflection
var modelsGenericType = model.GetType().BaseType?.GetGenericArguments().SingleOrDefault();
if (modelsGenericType == null) return;
if (!model.GetType().IsAssignableTo(typeof(JsonKeys<>).MakeGenericType(modelsGenericType))) return;
var setJsonKeysMethod = typeof(JsonKeys<>).GetMethod(nameof(JsonKeys<object>.SetJsonKeys));
if (setJsonKeysMethod == null) return;
var modelSetJsonKeysMethod = model.GetType().GetMethod(
setJsonKeysMethod.Name,
BindingFlags.Public | BindingFlags.Instance,
setJsonKeysMethod.GetParameters().Select(a => a.ParameterType).ToArray()
);
if (modelSetJsonKeysMethod == null) return;
JsonObject jsonObject;
try
{
jsonObject = jsonNode.AsObject();
}
catch (Exception e)
{
Console.WriteLine(e);
return;
}
var modelType = model.GetType();
foreach (var propertyInfo in modelType.GetProperties())
{
if (propertyInfo.GetMethod == null) continue;
var getValue = propertyInfo.GetMethod.Invoke(model, null);
if (!jsonObject.TryGetPropertyValue(propertyInfo.Name, out var value)) continue;
AddKeyExistence(getValue, value);
}
modelSetJsonKeysMethod.Invoke(model, new object[] { jsonObject.Select(a => a.Key).ToArray() });
}
}
Step 2 of 3
// https://stackoverflow.com/a/79718639
using dotenv.net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Options;
WebApplication? app = null;
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddControllers(mvcOptions =>
{
ArgumentNullException.ThrowIfNull(app);
var loggerFactory = app.Services.GetRequiredService<ILoggerFactory>();
var jsonOptions = app.Services.GetRequiredService<IOptions<JsonOptions>>();
// https://learn.microsoft.com/aspnet/core/web-api/advanced/custom-formatters?view=aspnetcore-7.0
mvcOptions.InputFormatters.Insert(0,
new JsonKeysInputFormatter(jsonOptions.Value, loggerFactory.CreateLogger<SystemTextJsonInputFormatter>()));
});
// ...
app = builder.Build();
Step 3 of 3
// https://stackoverflow.com/a/79718639
using XXX.XXX;
using Microsoft.AspNetCore.Mvc;
namespace YYY.YYY.YYY;
public class Nested : JsonKeys<Nested>
{
public string? NestedOne { get; set; }
}
public class MyRequest : JsonKeys<MyRequest>
{
public string? One { get; set; }
public string? Two { get; set; }
public string? Three { get; set; }
public Nested? Nested { get; set; }
public int[]? Four { get; set; }
}
[ApiController]
[Route("api/dev")]
public class DevTestJsonKeysController : ControllerBase
{
[HttpPost(nameof(TestJsonKeys))]
public async Task<IActionResult> TestJsonKeys([FromBody] MyRequest request)
{
request.HasKey(x => x.Nested);
request.Nested?.HasKey(x => x.NestedOne);
return Ok();
}
}
2 Comments
Explore related questions
See similar questions with these tags.