Add project files.

alphons <alphons@heijden.com> 20 Jan 2026, 16:26
6bd47bfcc94393409e2e0497cb9275faaa9145c4
13 files changed
  • JsonBodyProvider.sln
  • JsonBodyProvider/JsonBodyCorrectLists.cs
  • JsonBodyProvider/JsonBodyExtensions.cs
  • JsonBodyProvider/JsonBodyProvider.csproj
  • JsonBodyProvider/JsonBodyValueProvider.cs
  • JsonBodyProvider/JsonBodyValueProviderFactory.cs
  • JsonBodyProviderTest/Controllers/FirstController.cs
  • JsonBodyProviderTest/JsonBodyProviderTest.csproj
  • JsonBodyProviderTest/JsonBodyProviderTest.http
  • JsonBodyProviderTest/Program.cs
  • JsonBodyProviderTest/Properties/launchSettings.json
  • JsonBodyProviderTest/appsettings.Development.json
  • JsonBodyProviderTest/appsettings.json
diff --git a/JsonBodyProvider.sln b/JsonBodyProvider.sln
new file mode 100644
index 0000000..70c709b
--- /dev/null
+++ b/JsonBodyProvider.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 18
+VisualStudioVersion = 18.2.11408.102 d18.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonBodyProvider", "JsonBodyProvider\JsonBodyProvider.csproj", "{0BE9A54C-2854-98FE-DF07-B01FC5B4BA4D}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonBodyProviderTest", "JsonBodyProviderTest\JsonBodyProviderTest.csproj", "{9FB8CDDF-2192-40AC-AD64-642800FC1CEC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {0BE9A54C-2854-98FE-DF07-B01FC5B4BA4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0BE9A54C-2854-98FE-DF07-B01FC5B4BA4D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0BE9A54C-2854-98FE-DF07-B01FC5B4BA4D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0BE9A54C-2854-98FE-DF07-B01FC5B4BA4D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9FB8CDDF-2192-40AC-AD64-642800FC1CEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9FB8CDDF-2192-40AC-AD64-642800FC1CEC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9FB8CDDF-2192-40AC-AD64-642800FC1CEC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9FB8CDDF-2192-40AC-AD64-642800FC1CEC}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {94BED1F4-72EB-4E1F-BCAF-EB91DE82052D}
+ EndGlobalSection
+EndGlobal
diff --git a/JsonBodyProvider/JsonBodyCorrectLists.cs b/JsonBodyProvider/JsonBodyCorrectLists.cs
new file mode 100644
index 0000000..c5eea66
--- /dev/null
+++ b/JsonBodyProvider/JsonBodyCorrectLists.cs
@@ -0,0 +1,170 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.Globalization;
+
+namespace JsonBodyProvider;
+
+public class EmptyStringPreservingBinderProvider : IModelBinderProvider
+{
+ public IModelBinder? GetBinder(ModelBinderProviderContext context)
+ {
+ if (context.Metadata.ModelType == typeof(string))
+ return new EmptyStringPreservingStringBinder();
+
+ return null;
+ }
+}
+
+public class EmptyStringPreservingStringBinder : IModelBinder
+{
+ public Task BindModelAsync(ModelBindingContext context)
+ {
+ var value = context.ValueProvider.GetValue(context.ModelName);
+
+ if (value == ValueProviderResult.None)
+ return Task.CompletedTask;
+
+ context.Result = ModelBindingResult.Success(value.FirstValue);
+ return Task.CompletedTask;
+ }
+}
+
+public class NoFallbackListBinder<T> : IModelBinder
+{
+ public Task BindModelAsync(ModelBindingContext context)
+ {
+ var result = new List<T?>();
+ var value = context.ValueProvider.GetValue(context.ModelName);
+
+ if (value != ValueProviderResult.None)
+ {
+ foreach (var v in value)
+ {
+ // null → null
+ if (v == null)
+ {
+ result.Add(default);
+ continue;
+ }
+
+ var underlying = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
+
+ // string
+ if (underlying == typeof(string))
+ {
+ result.Add((T?)(object)v);
+ continue;
+ }
+
+ // empty → default (regel a), behalve nullable → default = null? nee = 0/false (a/b)
+ if (v == "")
+ {
+ result.Add((T?)DefaultValueFor(underlying));
+ continue;
+ }
+
+ // parse naar type
+ result.Add((T?)ParseValue(v, underlying));
+ }
+
+ context.Result = ModelBindingResult.Success(result);
+ return Task.CompletedTask;
+ }
+
+ context.Result = ModelBindingResult.Success(result);
+ return Task.CompletedTask;
+ }
+
+ private static object? ParseValue(string v, Type type)
+ {
+ // === GUID ===
+ if (type == typeof(Guid))
+ {
+ if (v == "") return Guid.Empty;
+ if (Guid.TryParse(v, out var g)) return g;
+ return Guid.Empty; // unknown → default
+ }
+
+ // === DATETIME === (ISO / invariant)
+ if (type == typeof(DateTime))
+ {
+ if (v == "") return default(DateTime);
+ if (DateTime.TryParse(v, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var dt))
+ return dt;
+ return default(DateTime); // unknown
+ }
+
+ // === DATEONLY ===
+ if (type == typeof(DateOnly))
+ {
+ if (v == "") return default(DateOnly);
+ if (DateOnly.TryParse(v, CultureInfo.InvariantCulture, DateTimeStyles.None, out var d))
+ return d;
+ return default(DateOnly);
+ }
+
+ // === TIMEONLY ===
+ if (type == typeof(TimeOnly))
+ {
+ if (v == "") return default(TimeOnly);
+ if (TimeOnly.TryParse(v, CultureInfo.InvariantCulture, DateTimeStyles.None, out var t))
+ return t;
+ return default(TimeOnly);
+ }
+
+ // verder zoals je had ↓↓↓
+
+ if (type == typeof(double)) return double.Parse(v, CultureInfo.InvariantCulture);
+ if (type == typeof(float)) return float.Parse(v, CultureInfo.InvariantCulture);
+ if (type == typeof(decimal)) return decimal.Parse(v, CultureInfo.InvariantCulture);
+ if (type == typeof(int)) return int.Parse(v, CultureInfo.InvariantCulture);
+ if (type == typeof(long)) return long.Parse(v, CultureInfo.InvariantCulture);
+ if (type == typeof(short)) return short.Parse(v, CultureInfo.InvariantCulture);
+
+ if (type == typeof(bool))
+ {
+ if (v.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
+ if (v.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
+ return false;
+ }
+
+ if (type.IsEnum)
+ {
+ if (Enum.TryParse(type, v, true, out var parsed))
+ return parsed;
+ return Activator.CreateInstance(type)!;
+ }
+
+ // fallback
+ return Convert.ChangeType(v, type, CultureInfo.InvariantCulture);
+ }
+
+
+ private static object? DefaultValueFor(Type type)
+ {
+ if (type == typeof(string)) return "";
+ if (type == typeof(bool)) return false;
+ if (type.IsEnum) return Activator.CreateInstance(type);
+ return Activator.CreateInstance(type); // numerics → 0
+ }
+}
+
+public class NoFallbackListBinderProvider : IModelBinderProvider
+{
+ public IModelBinder? GetBinder(ModelBinderProviderContext context)
+ {
+ if (context.Metadata.BindingSource == BindingSource.Body)
+ {
+ return null;
+ }
+
+ if (context.Metadata.ModelType.IsGenericType &&
+ context.Metadata.ModelType.GetGenericTypeDefinition() == typeof(List<>))
+ {
+ var elementType = context.Metadata.ModelType.GetGenericArguments()[0];
+ var binderType = typeof(NoFallbackListBinder<>).MakeGenericType(elementType);
+ return (IModelBinder)Activator.CreateInstance(binderType)!;
+ }
+
+ return null;
+ }
+}
diff --git a/JsonBodyProvider/JsonBodyExtensions.cs b/JsonBodyProvider/JsonBodyExtensions.cs
new file mode 100644
index 0000000..edb2115
--- /dev/null
+++ b/JsonBodyProvider/JsonBodyExtensions.cs
@@ -0,0 +1,27 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JsonBodyProvider;
+
+ public static class JsonBodyExtensions
+ {
+ public static IServiceCollection AddJsonProviders(this IServiceCollection services, bool CorrectLists = false)
+ {
+ return services
+ .AddControllers(options =>
+ {
+ options.ValueProviderFactories.Add(new JsonBodyValueProviderFactory());
+ if(CorrectLists)
+ {
+ options.ModelBinderProviders.Insert(0, new NoFallbackListBinderProvider());
+ options.ModelBinderProviders.Insert(0, new EmptyStringPreservingBinderProvider());
+ }
+ })
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.PropertyNameCaseInsensitive = false;
+ options.JsonSerializerOptions.PropertyNamingPolicy = null;
+ options.JsonSerializerOptions.DictionaryKeyPolicy = null;
+ })
+ .Services;
+ }
+}
diff --git a/JsonBodyProvider/JsonBodyProvider.csproj b/JsonBodyProvider/JsonBodyProvider.csproj
new file mode 100644
index 0000000..2d60420
--- /dev/null
+++ b/JsonBodyProvider/JsonBodyProvider.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <FrameworkReference Include="Microsoft.AspNetCore.App" />
+ </ItemGroup>
+
+</Project>
diff --git a/JsonBodyProvider/JsonBodyValueProvider.cs b/JsonBodyProvider/JsonBodyValueProvider.cs
new file mode 100644
index 0000000..13f8152
--- /dev/null
+++ b/JsonBodyProvider/JsonBodyValueProvider.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.Globalization;
+using System.Text.Json.Nodes;
+
+namespace JsonBodyProvider;
+
+internal class JsonBodyValueProvider(Dictionary<string, JsonNode?> values) : IValueProvider
+{
+ public bool ContainsPrefix(string prefix) => values.ContainsKey(prefix);
+
+ public ValueProviderResult GetValue(string key)
+ {
+ if (!values.TryGetValue(key, out JsonNode? node) || node is null)
+ {
+ return ValueProviderResult.None;
+ }
+
+ if (node is JsonArray array)
+ {
+ string?[] valuesAsStrings = [.. array.Select(item => item?.ToString())];
+
+ return new ValueProviderResult(valuesAsStrings, CultureInfo.InvariantCulture);
+ }
+
+ // Scalar gevallen
+ string? rawValue = node switch
+ {
+ JsonValue valueNode when valueNode.TryGetValue(out object? obj) => obj?.ToString(),
+ _ => node.ToJsonString() // object of fallback
+ };
+
+ return new ValueProviderResult(
+ rawValue ?? string.Empty,
+ CultureInfo.InvariantCulture);
+ }
+}
\ No newline at end of file
diff --git a/JsonBodyProvider/JsonBodyValueProviderFactory.cs b/JsonBodyProvider/JsonBodyValueProviderFactory.cs
new file mode 100644
index 0000000..27822d0
--- /dev/null
+++ b/JsonBodyProvider/JsonBodyValueProviderFactory.cs
@@ -0,0 +1,47 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.Text.Json.Nodes;
+
+namespace JsonBodyProvider;
+
+internal class JsonBodyValueProviderFactory : IValueProviderFactory
+{
+ public async Task CreateValueProviderAsync(ValueProviderFactoryContext context)
+ {
+ HttpRequest request = context.ActionContext.HttpContext.Request;
+
+ Dictionary<string, JsonNode?> values = [];
+
+ if (IsJsonContentType(request) && request.ContentLength.GetValueOrDefault() >= 2)
+ {
+ request.EnableBuffering();
+
+ using var reader = new StreamReader(request.Body, leaveOpen: true);
+
+ var json = await reader.ReadToEndAsync(context.ActionContext.HttpContext.RequestAborted);
+
+ if (JsonNode.Parse(json) is JsonObject obj)
+ {
+ foreach (var kv in obj)
+ values[kv.Key] = kv.Value;
+ }
+
+ request.Body.Position = 0;
+ }
+
+ foreach (var header in request.Headers)
+ {
+ if (header.Value.FirstOrDefault() is { } value)
+ values.TryAdd(header.Key, JsonValue.Create(value));
+ }
+
+ foreach (var cookie in request.Cookies)
+ values.TryAdd(cookie.Key, JsonValue.Create(cookie.Value));
+
+ context.ValueProviders.Add(new JsonBodyValueProvider(values));
+
+ }
+
+ private static bool IsJsonContentType(HttpRequest request) =>
+ request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) == true;
+}
diff --git a/JsonBodyProviderTest/Controllers/FirstController.cs b/JsonBodyProviderTest/Controllers/FirstController.cs
new file mode 100644
index 0000000..834fe37
--- /dev/null
+++ b/JsonBodyProviderTest/Controllers/FirstController.cs
@@ -0,0 +1,17 @@
+using Microsoft.AspNetCore.Mvc;
+
+namespace JsonBodyProviderTest.Controllers;
+
+[Route("[controller]")]
+public class FirstController : ControllerBase
+{
+ [HttpPost("Test")]
+ public IActionResult Test(string name, int age)
+ {
+ return Ok(new
+ {
+ Name = $"Your name {name}",
+ Age = $"Your age {age}"
+ });
+ }
+}
diff --git a/JsonBodyProviderTest/JsonBodyProviderTest.csproj b/JsonBodyProviderTest/JsonBodyProviderTest.csproj
new file mode 100644
index 0000000..5658146
--- /dev/null
+++ b/JsonBodyProviderTest/JsonBodyProviderTest.csproj
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>net10.0</TargetFramework>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\JsonBodyProvider\JsonBodyProvider.csproj" />
+ </ItemGroup>
+
+</Project>
diff --git a/JsonBodyProviderTest/JsonBodyProviderTest.http b/JsonBodyProviderTest/JsonBodyProviderTest.http
new file mode 100644
index 0000000..48630b1
--- /dev/null
+++ b/JsonBodyProviderTest/JsonBodyProviderTest.http
@@ -0,0 +1,9 @@
+@JsonBodyProviderTest_HostAddress = http://localhost:5129
+
+POST {{JsonBodyProviderTest_HostAddress}}/First/Test
+Content-Type: application/json
+
+{
+ "name": "Alphons",
+ "age": 21
+}
diff --git a/JsonBodyProviderTest/Program.cs b/JsonBodyProviderTest/Program.cs
new file mode 100644
index 0000000..e2bc3c1
--- /dev/null
+++ b/JsonBodyProviderTest/Program.cs
@@ -0,0 +1,11 @@
+using JsonBodyProvider;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddControllers();
+builder.Services.AddJsonProviders(CorrectLists: false);
+
+var app = builder.Build();
+
+app.MapControllers();
+app.Run();
diff --git a/JsonBodyProviderTest/Properties/launchSettings.json b/JsonBodyProviderTest/Properties/launchSettings.json
new file mode 100644
index 0000000..1946cb0
--- /dev/null
+++ b/JsonBodyProviderTest/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:5129",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/JsonBodyProviderTest/appsettings.Development.json b/JsonBodyProviderTest/appsettings.Development.json
new file mode 100644
index 0000000..0c208ae
--- /dev/null
+++ b/JsonBodyProviderTest/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/JsonBodyProviderTest/appsettings.json b/JsonBodyProviderTest/appsettings.json
new file mode 100644
index 0000000..10f68b8
--- /dev/null
+++ b/JsonBodyProviderTest/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}