From 1dc8b6b637888582177a41296290558f4f5ff7ed Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 15:24:15 -0500 Subject: [PATCH 01/33] added header constants --- InertiaCore/Extensions/Configure.cs | 4 ++-- InertiaCore/Extensions/InertiaExtensions.cs | 7 ++++--- InertiaCore/Response.cs | 2 +- InertiaCore/Utils/Header.cs | 18 ++++++++++++++++++ InertiaCore/Utils/LocationResult.cs | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 InertiaCore/Utils/Header.cs diff --git a/InertiaCore/Extensions/Configure.cs b/InertiaCore/Extensions/Configure.cs index 6be799c..d0d12b5 100644 --- a/InertiaCore/Extensions/Configure.cs +++ b/InertiaCore/Extensions/Configure.cs @@ -25,7 +25,7 @@ public static IApplicationBuilder UseInertia(this IApplicationBuilder app) { if (context.IsInertiaRequest() && context.Request.Method == "GET" - && context.Request.Headers["X-Inertia-Version"] != Inertia.GetVersion()) + && context.Request.Headers[Header.Version] != Inertia.GetVersion()) { await OnVersionChange(context, app); return; @@ -69,7 +69,7 @@ private static async Task OnVersionChange(HttpContext context, IApplicationBuild if (tempData.Any()) tempData.Keep(); - context.Response.Headers.Add("X-Inertia-Location", context.RequestedUri()); + context.Response.Headers.Add(Header.Location, context.RequestedUri()); context.Response.StatusCode = (int)HttpStatusCode.Conflict; await context.Response.CompleteAsync(); diff --git a/InertiaCore/Extensions/InertiaExtensions.cs b/InertiaCore/Extensions/InertiaExtensions.cs index dbcfae3..10844ea 100644 --- a/InertiaCore/Extensions/InertiaExtensions.cs +++ b/InertiaCore/Extensions/InertiaExtensions.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using InertiaCore.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; @@ -12,13 +13,13 @@ internal static IEnumerable Only(this object obj, IEnumerable on .Intersect(only, StringComparer.OrdinalIgnoreCase).ToList(); internal static List GetPartialData(this ActionContext context) => - context.HttpContext.Request.Headers["X-Inertia-Partial-Data"] + context.HttpContext.Request.Headers[Header.PartialOnly] .FirstOrDefault()?.Split(",") .Where(s => !string.IsNullOrEmpty(s)) .ToList() ?? new List(); internal static bool IsInertiaPartialComponent(this ActionContext context, string component) => - context.HttpContext.Request.Headers["X-Inertia-Partial-Component"] == component; + context.HttpContext.Request.Headers[Header.PartialComponent] == component; internal static string RequestedUri(this HttpContext context) => Uri.UnescapeDataString(context.Request.GetEncodedPathAndQuery()); @@ -26,7 +27,7 @@ internal static string RequestedUri(this HttpContext context) => internal static string RequestedUri(this ActionContext context) => context.HttpContext.RequestedUri(); internal static bool IsInertiaRequest(this HttpContext context) => - bool.TryParse(context.Request.Headers["X-Inertia"], out _); + bool.TryParse(context.Request.Headers[Header.Inertia], out _); internal static bool IsInertiaRequest(this ActionContext context) => context.HttpContext.IsInertiaRequest(); diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 865ff45..f9bd802 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -81,7 +81,7 @@ protected internal void ProcessResponse() protected internal JsonResult GetJson() { - _context!.HttpContext.Response.Headers.Add("X-Inertia", "true"); + _context!.HttpContext.Response.Headers.Add(Header.Inertia, "true"); _context!.HttpContext.Response.Headers.Add("Vary", "Accept"); _context!.HttpContext.Response.StatusCode = 200; diff --git a/InertiaCore/Utils/Header.cs b/InertiaCore/Utils/Header.cs new file mode 100644 index 0000000..b729dbf --- /dev/null +++ b/InertiaCore/Utils/Header.cs @@ -0,0 +1,18 @@ +namespace InertiaCore.Utils; + +public static class Header +{ + public const string Inertia = "X-Inertia"; + + public const string ErrorBag = "X-Inertia-Error-Bag"; + + public const string Location = "X-Inertia-Location"; + + public const string Version = "X-Inertia-Version"; + + public const string PartialComponent = "X-Inertia-Partial-Component"; + + public const string PartialOnly = "X-Inertia-Partial-Data"; + + public const string PartialExcept = "X-Inertia-Partial-Except"; +} diff --git a/InertiaCore/Utils/LocationResult.cs b/InertiaCore/Utils/LocationResult.cs index 6e358ed..3ddcedd 100644 --- a/InertiaCore/Utils/LocationResult.cs +++ b/InertiaCore/Utils/LocationResult.cs @@ -14,7 +14,7 @@ public async Task ExecuteResultAsync(ActionContext context) { if (context.IsInertiaRequest()) { - context.HttpContext.Response.Headers.Add("X-Inertia-Location", _url); + context.HttpContext.Response.Headers.Add(Header.Location, _url); await new StatusCodeResult((int)HttpStatusCode.Conflict).ExecuteResultAsync(context); return; } From 3a334498c0dc4ffc96860bc2df7ce3aa0c435cb2 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 17:07:47 -0500 Subject: [PATCH 02/33] refactor resolve props --- InertiaCore/Extensions/InertiaExtensions.cs | 18 +++++-- InertiaCore/Response.cs | 59 +++++++++++++-------- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/InertiaCore/Extensions/InertiaExtensions.cs b/InertiaCore/Extensions/InertiaExtensions.cs index 10844ea..ba6cc0c 100644 --- a/InertiaCore/Extensions/InertiaExtensions.cs +++ b/InertiaCore/Extensions/InertiaExtensions.cs @@ -8,9 +8,21 @@ namespace InertiaCore.Extensions; internal static class InertiaExtensions { - internal static IEnumerable Only(this object obj, IEnumerable only) => - obj.GetType().GetProperties().Select(c => c.Name) - .Intersect(only, StringComparer.OrdinalIgnoreCase).ToList(); + internal static Dictionary OnlyProps(this ActionContext context, Dictionary props) + { + var onlyKeys = context.HttpContext.Request.Headers[Header.PartialOnly].ToString().Split(',').Select(k => k.Trim()).ToList(); + + return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase)) + .ToDictionary(kv => kv.Key, kv => kv.Value); + } + + internal static Dictionary ExceptProps(this ActionContext context, Dictionary props) + { + var exceptKeys = context.HttpContext.Request.Headers[Header.PartialExcept].ToString().Split(',').Select(k => k.Trim()).ToList(); + + return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false) + .ToDictionary(kv => kv.Key, kv => kv.Value); + } internal static List GetPartialData(this ActionContext context) => context.HttpContext.Request.Headers[Header.PartialOnly] diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index f9bd802..13348c1 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -37,29 +37,10 @@ protected internal void ProcessResponse() { Component = _component, Version = _version, - Url = _context!.RequestedUri() + Url = _context!.RequestedUri(), + Props = ResolveProperties(_props.GetType().GetProperties().ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props))) }; - var partial = _context!.GetPartialData(); - if (partial.Any() && _context!.IsInertiaPartialComponent(_component)) - { - var only = _props.Only(partial); - var partialProps = only.ToDictionary(o => o.ToCamelCase(), o => - _props.GetType().GetProperty(o)?.GetValue(_props)); - - page.Props = partialProps; - } - else - { - var props = _props.GetType().GetProperties() - .Where(o => o.PropertyType != typeof(LazyProp)) - .ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)); - - page.Props = props; - } - - page.Props = PrepareProps(page.Props); - var shared = _context!.HttpContext.Features.Get(); if (shared != null) page.Props = shared.GetMerged(page.Props); @@ -127,4 +108,40 @@ public Response WithViewData(IDictionary viewData) _viewData = viewData; return this; } + + private Dictionary ResolveProperties(Dictionary props) + { + bool isPartial = _context!.IsInertiaPartialComponent(_component); + + if (!isPartial) + { + props = props + .Where(kv => kv.Value is not LazyProp) + .ToDictionary(kv => kv.Key, kv => kv.Value); + } + + if (isPartial && _context!.HttpContext.Request.Headers.ContainsKey(Header.PartialOnly)) + { + props = ResolveOnly(props); + } + + if (isPartial && _context!.HttpContext.Request.Headers.ContainsKey(Header.PartialExcept)) + { + props = ResolveExcept(props); + } + + props = PrepareProps(props); + + return props; + } + + private Dictionary ResolveOnly(Dictionary props) + { + return _context!.OnlyProps(props); + } + + private Dictionary ResolveExcept(Dictionary props) + { + return _context!.ExceptProps(props); + } } From c456148b3e0f3b7603ac12dc480f8e0be33d956e Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 17:08:57 -0500 Subject: [PATCH 03/33] added always prop --- InertiaCore/Inertia.cs | 3 +++ InertiaCore/Response.cs | 14 ++++++++++++++ InertiaCore/ResponseFactory.cs | 6 ++++++ InertiaCore/Utils/AlwaysProp.cs | 25 +++++++++++++++++++++++++ 4 files changed, 48 insertions(+) create mode 100644 InertiaCore/Utils/AlwaysProp.cs diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 729e52f..dec6eda 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -28,5 +28,8 @@ public static class Inertia public static void Share(IDictionary data) => _factory.Share(data); + public static AlwaysProp Always(object? value) => _factory.Always(value); + + public static AlwaysProp Always(Func callback) => _factory.Always(callback); public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 13348c1..acab917 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -56,6 +56,7 @@ protected internal void ProcessResponse() { Func f => f.Invoke(), LazyProp l => l.Invoke(), + AlwaysProp l => l.Invoke(), _ => pair.Value }); } @@ -130,6 +131,8 @@ public Response WithViewData(IDictionary viewData) props = ResolveExcept(props); } + props = ResolveAlways(props); + props = PrepareProps(props); return props; @@ -144,4 +147,15 @@ public Response WithViewData(IDictionary viewData) { return _context!.ExceptProps(props); } + + private Dictionary ResolveAlways(Dictionary props) + { + var alwaysProps = _props.GetType().GetProperties() + .Where(o => o.PropertyType == typeof(AlwaysProp)) + .ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)); ; + + return props + .Where(kv => kv.Value is not AlwaysProp) + .Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value); + } } diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 0692b40..288721e 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -21,6 +21,9 @@ internal interface IResponseFactory public LocationResult Location(string url); public void Share(string key, object? value); public void Share(IDictionary data); + public AlwaysProp Always(object? value); + public AlwaysProp Always(Func callback); + public AlwaysProp Always(Func> callback); public LazyProp Lazy(Func callback); public LazyProp Lazy(Func> callback); } @@ -121,4 +124,7 @@ public void Share(IDictionary data) public LazyProp Lazy(Func callback) => new LazyProp(callback); public LazyProp Lazy(Func> callback) => new LazyProp(callback); + public AlwaysProp Always(object? value) => new AlwaysProp(value); + public AlwaysProp Always(Func callback) => new AlwaysProp(callback); + public AlwaysProp Always(Func> callback) => new AlwaysProp(callback); } diff --git a/InertiaCore/Utils/AlwaysProp.cs b/InertiaCore/Utils/AlwaysProp.cs new file mode 100644 index 0000000..12182de --- /dev/null +++ b/InertiaCore/Utils/AlwaysProp.cs @@ -0,0 +1,25 @@ +namespace InertiaCore.Utils; + +public class AlwaysProp +{ + private readonly object? _value; + + public AlwaysProp(object? value) + { + _value = value; + } + + public object? Invoke() + { + // Check if the value is a callable delegate + return Task.Run(() => + { + if (_value is Delegate callable) + { + return callable.DynamicInvoke(); + } + + return _value; + }).GetAwaiter().GetResult(); + } +} From 7d0d3bfa8d8414435de76a67e7e1487507030ff9 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 17:15:46 -0500 Subject: [PATCH 04/33] fix some compile time warnings --- InertiaCore/Extensions/Configure.cs | 2 +- InertiaCore/Extensions/InertiaExtensions.cs | 12 ++++++++++++ InertiaCore/Response.cs | 4 ++-- InertiaCore/Utils/InertiaActionFilter.cs | 2 +- InertiaCore/Utils/LocationResult.cs | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/InertiaCore/Extensions/Configure.cs b/InertiaCore/Extensions/Configure.cs index 6be799c..2e9ce05 100644 --- a/InertiaCore/Extensions/Configure.cs +++ b/InertiaCore/Extensions/Configure.cs @@ -69,7 +69,7 @@ private static async Task OnVersionChange(HttpContext context, IApplicationBuild if (tempData.Any()) tempData.Keep(); - context.Response.Headers.Add("X-Inertia-Location", context.RequestedUri()); + context.Response.Headers.Override(Header.Location, context.RequestedUri()); context.Response.StatusCode = (int)HttpStatusCode.Conflict; await context.Response.CompleteAsync(); diff --git a/InertiaCore/Extensions/InertiaExtensions.cs b/InertiaCore/Extensions/InertiaExtensions.cs index dbcfae3..e92dbd9 100644 --- a/InertiaCore/Extensions/InertiaExtensions.cs +++ b/InertiaCore/Extensions/InertiaExtensions.cs @@ -31,4 +31,16 @@ internal static bool IsInertiaRequest(this HttpContext context) => internal static bool IsInertiaRequest(this ActionContext context) => context.HttpContext.IsInertiaRequest(); internal static string ToCamelCase(this string s) => JsonNamingPolicy.CamelCase.ConvertName(s); + + internal static bool Override(this IDictionary dictionary, TKey key, TValue value) + { + if (dictionary.ContainsKey(key)) + { + dictionary[key] = value; + return true; + } + + dictionary.Add(key, value); + return false; + } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 865ff45..966a95f 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -81,8 +81,8 @@ protected internal void ProcessResponse() protected internal JsonResult GetJson() { - _context!.HttpContext.Response.Headers.Add("X-Inertia", "true"); - _context!.HttpContext.Response.Headers.Add("Vary", "Accept"); + _context!.HttpContext.Response.Headers.Override(Header.Inertia, "true"); + _context!.HttpContext.Response.Headers.Override("Vary", "Accept"); _context!.HttpContext.Response.StatusCode = 200; return new JsonResult(_page, new JsonSerializerOptions diff --git a/InertiaCore/Utils/InertiaActionFilter.cs b/InertiaCore/Utils/InertiaActionFilter.cs index f3c5b75..c2ff160 100644 --- a/InertiaCore/Utils/InertiaActionFilter.cs +++ b/InertiaCore/Utils/InertiaActionFilter.cs @@ -32,7 +32,7 @@ public void OnActionExecuted(ActionExecutedContext context) }; if (destinationUrl == null) return; - context.HttpContext.Response.Headers.Add("Location", destinationUrl); + context.HttpContext.Response.Headers.Override("Location", destinationUrl); context.Result = new StatusCodeResult((int)HttpStatusCode.RedirectMethod); } diff --git a/InertiaCore/Utils/LocationResult.cs b/InertiaCore/Utils/LocationResult.cs index 6e358ed..436f372 100644 --- a/InertiaCore/Utils/LocationResult.cs +++ b/InertiaCore/Utils/LocationResult.cs @@ -14,7 +14,7 @@ public async Task ExecuteResultAsync(ActionContext context) { if (context.IsInertiaRequest()) { - context.HttpContext.Response.Headers.Add("X-Inertia-Location", _url); + context.HttpContext.Response.Headers.Override(Header.Location, _url); await new StatusCodeResult((int)HttpStatusCode.Conflict).ExecuteResultAsync(context); return; } From f9ad84b836c0fd459a2e88fbd1fa4656a79693fa Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 22:26:54 -0500 Subject: [PATCH 05/33] added always prop test --- InertiaCore/Utils/AlwaysProp.cs | 11 +- InertiaCoreTests/UnitTestAlwaysData.cs | 175 +++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 InertiaCoreTests/UnitTestAlwaysData.cs diff --git a/InertiaCore/Utils/AlwaysProp.cs b/InertiaCore/Utils/AlwaysProp.cs index 12182de..1ccc9b8 100644 --- a/InertiaCore/Utils/AlwaysProp.cs +++ b/InertiaCore/Utils/AlwaysProp.cs @@ -12,11 +12,16 @@ public AlwaysProp(object? value) public object? Invoke() { // Check if the value is a callable delegate - return Task.Run(() => + return Task.Run(async () => { - if (_value is Delegate callable) + if (_value is Func> asyncCallable) { - return callable.DynamicInvoke(); + return await asyncCallable.Invoke(); + } + + if (_value is Func callable) + { + return callable.Invoke(); } return _value; diff --git a/InertiaCoreTests/UnitTestAlwaysData.cs b/InertiaCoreTests/UnitTestAlwaysData.cs new file mode 100644 index 0000000..f3d7bcb --- /dev/null +++ b/InertiaCoreTests/UnitTestAlwaysData.cs @@ -0,0 +1,175 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the always data is fetched properly.")] + public void TestAlwaysData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestAlways = _factory.Always(() => + { + return "Always"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testAlways", "Always" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the always data is fetched properly with specified partial props.")] + public void TestAlwaysPartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestAlways = _factory.Always(() => "Always") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testAlways" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testAlways", "Always" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the always async data is fetched properly.")] + public void TestAlwaysAsyncData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Always Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestAlways = _factory.Always(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testAlways", "Always Async" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the always async data is fetched properly with specified partial props.")] + public void TestAlwaysAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Always Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestAlways = _factory.Always(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testAlways" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testAlways", "Always Async" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the always async data is fetched properly without specified partial props.")] + public void TestAlwaysAsyncPartialDataOmitted() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Always Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestAlways = _factory.Always(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testAlways", "Always Async" }, + { "errors", new Dictionary(0) } + })); + } +} From 77473d6572d676c97c7df24e9f560df3db4ae78e Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 22:27:15 -0500 Subject: [PATCH 06/33] added async task wrapper --- InertiaCore/Inertia.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index dec6eda..8519b53 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -31,5 +31,10 @@ public static class Inertia public static AlwaysProp Always(object? value) => _factory.Always(value); public static AlwaysProp Always(Func callback) => _factory.Always(callback); + + public static AlwaysProp Always(Func> callback) => _factory.Always(callback); + public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); + + public static LazyProp Lazy(Func> callback) => _factory.Lazy(callback); } From f64ca769b65a239fd574d13ebfbc2ee82d57144e Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 14 Dec 2024 22:30:28 -0500 Subject: [PATCH 07/33] .net 8 --- InertiaCore/InertiaCore.csproj | 2 +- InertiaCoreTests/InertiaCoreTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InertiaCore/InertiaCore.csproj b/InertiaCore/InertiaCore.csproj index b54f108..05d7e54 100644 --- a/InertiaCore/InertiaCore.csproj +++ b/InertiaCore/InertiaCore.csproj @@ -3,7 +3,7 @@ enable enable 0.0.9 - net6.0;net7.0 + net6.0;net7.0;net8.0 AspNetCore.InertiaCore kapi2289 Inertia.js ASP.NET Adapter. https://inertiajs.com/ diff --git a/InertiaCoreTests/InertiaCoreTests.csproj b/InertiaCoreTests/InertiaCoreTests.csproj index f85c6b7..81f3de9 100644 --- a/InertiaCoreTests/InertiaCoreTests.csproj +++ b/InertiaCoreTests/InertiaCoreTests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 enable enable From 51fe030f143c64a7043776101d8a463c7755f953 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 20 Dec 2024 23:56:40 -0500 Subject: [PATCH 08/33] restore header keys --- InertiaCore/Extensions/Configure.cs | 2 +- InertiaCore/Response.cs | 2 +- InertiaCore/Utils/LocationResult.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/InertiaCore/Extensions/Configure.cs b/InertiaCore/Extensions/Configure.cs index 2e9ce05..f0ff601 100644 --- a/InertiaCore/Extensions/Configure.cs +++ b/InertiaCore/Extensions/Configure.cs @@ -69,7 +69,7 @@ private static async Task OnVersionChange(HttpContext context, IApplicationBuild if (tempData.Any()) tempData.Keep(); - context.Response.Headers.Override(Header.Location, context.RequestedUri()); + context.Response.Headers.Override("X-Inertia-Location", context.RequestedUri()); context.Response.StatusCode = (int)HttpStatusCode.Conflict; await context.Response.CompleteAsync(); diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 966a95f..ee364e6 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -81,7 +81,7 @@ protected internal void ProcessResponse() protected internal JsonResult GetJson() { - _context!.HttpContext.Response.Headers.Override(Header.Inertia, "true"); + _context!.HttpContext.Response.Headers.Override("X-Inertia", "true"); _context!.HttpContext.Response.Headers.Override("Vary", "Accept"); _context!.HttpContext.Response.StatusCode = 200; diff --git a/InertiaCore/Utils/LocationResult.cs b/InertiaCore/Utils/LocationResult.cs index 436f372..03887d9 100644 --- a/InertiaCore/Utils/LocationResult.cs +++ b/InertiaCore/Utils/LocationResult.cs @@ -14,7 +14,7 @@ public async Task ExecuteResultAsync(ActionContext context) { if (context.IsInertiaRequest()) { - context.HttpContext.Response.Headers.Override(Header.Location, _url); + context.HttpContext.Response.Headers.Override("X-Inertia-Location", _url); await new StatusCodeResult((int)HttpStatusCode.Conflict).ExecuteResultAsync(context); return; } From 62a0864c2f08ea422b742abc94e0511e3d2fe7d8 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 20 Dec 2024 23:58:09 -0500 Subject: [PATCH 09/33] added .net 9 --- InertiaCore/InertiaCore.csproj | 2 +- InertiaCoreTests/InertiaCoreTests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/InertiaCore/InertiaCore.csproj b/InertiaCore/InertiaCore.csproj index 05d7e54..5f68508 100644 --- a/InertiaCore/InertiaCore.csproj +++ b/InertiaCore/InertiaCore.csproj @@ -3,7 +3,7 @@ enable enable 0.0.9 - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0 AspNetCore.InertiaCore kapi2289 Inertia.js ASP.NET Adapter. https://inertiajs.com/ diff --git a/InertiaCoreTests/InertiaCoreTests.csproj b/InertiaCoreTests/InertiaCoreTests.csproj index 81f3de9..328dafe 100644 --- a/InertiaCoreTests/InertiaCoreTests.csproj +++ b/InertiaCoreTests/InertiaCoreTests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0 enable enable From 2ab07651da7f48d66bcae34b187f60ec6aca4d6c Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 00:21:26 -0500 Subject: [PATCH 10/33] added ignore first load --- InertiaCore/Response.cs | 2 +- InertiaCore/Utils/IgnoreFirstLoad.cs | 6 ++++++ InertiaCore/Utils/LazyProp.cs | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 InertiaCore/Utils/IgnoreFirstLoad.cs diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 87b53da..263b5b5 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -117,7 +117,7 @@ public Response WithViewData(IDictionary viewData) if (!isPartial) { props = props - .Where(kv => kv.Value is not LazyProp) + .Where(kv => (kv.Value as IgnoreFirstLoad) == null) .ToDictionary(kv => kv.Key, kv => kv.Value); } diff --git a/InertiaCore/Utils/IgnoreFirstLoad.cs b/InertiaCore/Utils/IgnoreFirstLoad.cs new file mode 100644 index 0000000..f5ff0f7 --- /dev/null +++ b/InertiaCore/Utils/IgnoreFirstLoad.cs @@ -0,0 +1,6 @@ +namespace InertiaCore.Utils; + +public interface IgnoreFirstLoad +{ + +} diff --git a/InertiaCore/Utils/LazyProp.cs b/InertiaCore/Utils/LazyProp.cs index 1f74f9f..46004b5 100644 --- a/InertiaCore/Utils/LazyProp.cs +++ b/InertiaCore/Utils/LazyProp.cs @@ -1,6 +1,6 @@ namespace InertiaCore.Utils; -public class LazyProp +public class LazyProp : IgnoreFirstLoad { private readonly Func> _callback; From bf08e03df726867b3e8e06a6b90a62bd7f0e3683 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 00:22:03 -0500 Subject: [PATCH 11/33] add optional prop --- InertiaCore/Response.cs | 1 + InertiaCore/ResponseFactory.cs | 4 ++++ InertiaCore/Utils/OptionalProp.cs | 12 ++++++++++++ 3 files changed, 17 insertions(+) create mode 100644 InertiaCore/Utils/OptionalProp.cs diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 263b5b5..a7344d7 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -56,6 +56,7 @@ protected internal void ProcessResponse() { Func f => f.Invoke(), LazyProp l => l.Invoke(), + OptionalProp l => l.Invoke(), AlwaysProp l => l.Invoke(), _ => pair.Value }); diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 288721e..1ed9277 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -26,6 +26,8 @@ internal interface IResponseFactory public AlwaysProp Always(Func> callback); public LazyProp Lazy(Func callback); public LazyProp Lazy(Func> callback); + public OptionalProp Optional(Func callback); + public OptionalProp Optional(Func> callback); } internal class ResponseFactory : IResponseFactory @@ -127,4 +129,6 @@ public void Share(IDictionary data) public AlwaysProp Always(object? value) => new AlwaysProp(value); public AlwaysProp Always(Func callback) => new AlwaysProp(callback); public AlwaysProp Always(Func> callback) => new AlwaysProp(callback); + public OptionalProp Optional(Func callback) => new OptionalProp(callback); + public OptionalProp Optional(Func> callback) => new OptionalProp(callback); } diff --git a/InertiaCore/Utils/OptionalProp.cs b/InertiaCore/Utils/OptionalProp.cs new file mode 100644 index 0000000..20c6e5a --- /dev/null +++ b/InertiaCore/Utils/OptionalProp.cs @@ -0,0 +1,12 @@ +namespace InertiaCore.Utils; + +public class OptionalProp : IgnoreFirstLoad +{ + private readonly Func> _callback; + + public OptionalProp(Func callback) => _callback = async () => await Task.FromResult(callback()); + + public OptionalProp(Func> callback) => _callback = callback; + + public object? Invoke() => Task.Run(() => _callback.Invoke()).GetAwaiter().GetResult(); +} From 3e30450aebc9c3046dc54362a7d32d764ae962f7 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 00:45:24 -0500 Subject: [PATCH 12/33] added optional test --- InertiaCoreTests/UnitTestOptionalData.cs | 139 +++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 InertiaCoreTests/UnitTestOptionalData.cs diff --git a/InertiaCoreTests/UnitTestOptionalData.cs b/InertiaCoreTests/UnitTestOptionalData.cs new file mode 100644 index 0000000..8d8365f --- /dev/null +++ b/InertiaCoreTests/UnitTestOptionalData.cs @@ -0,0 +1,139 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the optional data is fetched properly.")] + public void TestOptionalData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(() => + { + Assert.Fail(); + return "Optional"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the optional data is fetched properly with specified partial props.")] + public void TestOptionalPartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(() => "Optional") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testOptional" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testOptional", "Optional" }, + { "errors", new Dictionary(0) } + })); + } + + + [Test] + [Description("Test if the optional async data is fetched properly.")] + public void TestOptionalAsyncData() + { + var testFunction = new Func>(async () => + { + Assert.Fail(); + await Task.Delay(100); + return "Optional Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if the optional async data is fetched properly with specified partial props.")] + public void TestOptionalAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Optional Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestOptional = _factory.Optional(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testOptional" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testOptional", "Optional Async" }, + { "errors", new Dictionary(0) } + })); + } +} From 1d74592da708cb2346e303c1a797a73b50e2a97d Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 00:44:50 -0500 Subject: [PATCH 13/33] added merge prop --- InertiaCore/Inertia.cs | 6 ++++++ InertiaCore/Models/Page.cs | 1 + InertiaCore/Response.cs | 30 ++++++++++++++++++++++++++++++ InertiaCore/ResponseFactory.cs | 6 ++++++ InertiaCore/Utils/Header.cs | 2 ++ InertiaCore/Utils/MergeProp.cs | 33 +++++++++++++++++++++++++++++++++ InertiaCore/Utils/Mergeable.cs | 15 +++++++++++++++ 7 files changed, 93 insertions(+) create mode 100644 InertiaCore/Utils/MergeProp.cs create mode 100644 InertiaCore/Utils/Mergeable.cs diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 8519b53..aa87a2f 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -34,6 +34,12 @@ public static class Inertia public static AlwaysProp Always(Func> callback) => _factory.Always(callback); + public static MergeProp Merge(object? value) => _factory.Merge(value); + + public static MergeProp Merge(Func callback) => _factory.Merge(callback); + + public static MergeProp Merge(Func> callback) => _factory.Merge(callback); + public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); public static LazyProp Lazy(Func> callback) => _factory.Lazy(callback); diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index 47abba7..230462a 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -6,4 +6,5 @@ internal class Page public string Component { get; set; } = default!; public string? Version { get; set; } public string Url { get; set; } = default!; + public List? MergeProps { get; set; } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 87b53da..1125530 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -41,6 +41,8 @@ protected internal void ProcessResponse() Props = ResolveProperties(_props.GetType().GetProperties().ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props))) }; + page.MergeProps = ResolveMergeProps(page.Props); + var shared = _context!.HttpContext.Features.Get(); if (shared != null) page.Props = shared.GetMerged(page.Props); @@ -57,6 +59,7 @@ protected internal void ProcessResponse() Func f => f.Invoke(), LazyProp l => l.Invoke(), AlwaysProp l => l.Invoke(), + MergeProp m => m.Invoke(), _ => pair.Value }); } @@ -158,4 +161,31 @@ public Response WithViewData(IDictionary viewData) .Where(kv => kv.Value is not AlwaysProp) .Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value); } + + private List? ResolveMergeProps(Dictionary props) + { + // Parse the "RESET" header into a collection of keys to reset + var resetProps = new HashSet( + _context!.HttpContext.Request.Headers[Header.Reset] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + var resolvedProps = props + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .ToList(); + + // Filter the props that are Mergeable and should be merged + var mergeProps = _props.GetType().GetProperties().ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)) + .Where(kv => kv.Value is Mergeable mergeable && mergeable.ShouldMerge()) // Check if value is Mergeable and should merge + .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .Where(resolvedProps.Contains) // Filter only the props that are in the resolved props + .ToList(); + + // Return the result + return mergeProps; + } } diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 288721e..e95f10d 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -26,6 +26,9 @@ internal interface IResponseFactory public AlwaysProp Always(Func> callback); public LazyProp Lazy(Func callback); public LazyProp Lazy(Func> callback); + public MergeProp Merge(object? value); + public MergeProp Merge(Func callback); + public MergeProp Merge(Func> callback); } internal class ResponseFactory : IResponseFactory @@ -127,4 +130,7 @@ public void Share(IDictionary data) public AlwaysProp Always(object? value) => new AlwaysProp(value); public AlwaysProp Always(Func callback) => new AlwaysProp(callback); public AlwaysProp Always(Func> callback) => new AlwaysProp(callback); + public MergeProp Merge(object? value) => new MergeProp(value); + public MergeProp Merge(Func callback) => new MergeProp(callback); + public MergeProp Merge(Func> callback) => new MergeProp(callback); } diff --git a/InertiaCore/Utils/Header.cs b/InertiaCore/Utils/Header.cs index b729dbf..b1f9abb 100644 --- a/InertiaCore/Utils/Header.cs +++ b/InertiaCore/Utils/Header.cs @@ -15,4 +15,6 @@ public static class Header public const string PartialOnly = "X-Inertia-Partial-Data"; public const string PartialExcept = "X-Inertia-Partial-Except"; + + public const string Reset = "X-Inertia-Reset"; } diff --git a/InertiaCore/Utils/MergeProp.cs b/InertiaCore/Utils/MergeProp.cs new file mode 100644 index 0000000..c26b7ad --- /dev/null +++ b/InertiaCore/Utils/MergeProp.cs @@ -0,0 +1,33 @@ +namespace InertiaCore.Utils; + +public class MergeProp : Mergeable +{ + public bool merge { get; set; } = true; + + private readonly object? _value; + + public MergeProp(object? value) + { + _value = value; + merge = true; + } + + public object? Invoke() + { + // Check if the value is a callable delegate + return Task.Run(async () => + { + if (_value is Func> asyncCallable) + { + return await asyncCallable.Invoke(); + } + + if (_value is Func callable) + { + return callable.Invoke(); + } + + return _value; + }).GetAwaiter().GetResult(); + } +} diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs new file mode 100644 index 0000000..b7e5b6e --- /dev/null +++ b/InertiaCore/Utils/Mergeable.cs @@ -0,0 +1,15 @@ +namespace InertiaCore.Utils; + +public interface Mergeable +{ + public bool merge { get; set; } + + public Mergeable Merge() + { + merge = true; + + return this; + } + + public bool ShouldMerge() => merge; +} From 05bf42d85f5485638f29b7f729666e5432a54265 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 14:40:50 -0500 Subject: [PATCH 14/33] added unit test for merge prop --- InertiaCoreTests/UnitTestMergeData.cs | 206 ++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 InertiaCoreTests/UnitTestMergeData.cs diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs new file mode 100644 index 0000000..a6e9312 --- /dev/null +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -0,0 +1,206 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the merge data is fetched properly.")] + public void TestMergeData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(() => + { + return "Merge"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testMerge", "Merge" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge data is fetched properly with specified partial props.")] + public void TestMergePartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(() => "Merge") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testMerge" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testMerge", "Merge" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly.")] + public void TestMergeAsyncData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testMerge", "Merge Async" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly with specified partial props.")] + public void TestMergeAsyncPartialData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testMerge" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testMerge", "Merge Async" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + } + + [Test] + [Description("Test if the merge async data is fetched properly without specified partial props.")] + public void TestMergeAsyncPartialDataOmitted() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestMerge = _factory.Merge(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { })); + } + + public void TestNoMergeProps() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + }); + + var context = PrepareContext(); + + response.SetContext(context); + response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.MergeProps, Is.EqualTo(new List { })); + } +} From 3cacdaa9bd86580b382e74282034306697dfb6e8 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 14:52:03 -0500 Subject: [PATCH 15/33] update version in actions --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 25f43e7..235f1ef 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build From 52ad4b83296b667a599eaee30c7f1c8e32a03d99 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 14:52:03 -0500 Subject: [PATCH 16/33] update version in actions --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 25f43e7..235f1ef 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -19,7 +19,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 9.0.x - name: Restore dependencies run: dotnet restore - name: Build From 1e1869695e88d335a0a3451f4eeb40df362e334c Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 15:19:04 -0500 Subject: [PATCH 17/33] dont resolve Lazy and Optional Props until they are invoked --- InertiaCore/Utils/LazyProp.cs | 25 +++++++++++++++++++++---- InertiaCore/Utils/OptionalProp.cs | 24 ++++++++++++++++++++---- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/InertiaCore/Utils/LazyProp.cs b/InertiaCore/Utils/LazyProp.cs index 46004b5..7676e6c 100644 --- a/InertiaCore/Utils/LazyProp.cs +++ b/InertiaCore/Utils/LazyProp.cs @@ -2,10 +2,27 @@ namespace InertiaCore.Utils; public class LazyProp : IgnoreFirstLoad { - private readonly Func> _callback; + private readonly object? _value; - public LazyProp(Func callback) => _callback = async () => await Task.FromResult(callback()); - public LazyProp(Func> callback) => _callback = callback; + public LazyProp(Func callback) => _value = callback; + public LazyProp(Func> callback) => _value = callback; - public object? Invoke() => Task.Run(() => _callback.Invoke()).GetAwaiter().GetResult(); + public object? Invoke() + { + // Check if the value is a callable delegate + return Task.Run(async () => + { + if (_value is Func> asyncCallable) + { + return await asyncCallable.Invoke(); + } + + if (_value is Func callable) + { + return callable.Invoke(); + } + + return _value; + }).GetAwaiter().GetResult(); + } } diff --git a/InertiaCore/Utils/OptionalProp.cs b/InertiaCore/Utils/OptionalProp.cs index 20c6e5a..7818b17 100644 --- a/InertiaCore/Utils/OptionalProp.cs +++ b/InertiaCore/Utils/OptionalProp.cs @@ -2,11 +2,27 @@ namespace InertiaCore.Utils; public class OptionalProp : IgnoreFirstLoad { - private readonly Func> _callback; + private readonly object? _value; - public OptionalProp(Func callback) => _callback = async () => await Task.FromResult(callback()); + public OptionalProp(Func callback) => _value = callback; + public OptionalProp(Func> callback) => _value = callback; - public OptionalProp(Func> callback) => _callback = callback; + public object? Invoke() + { + // Check if the value is a callable delegate + return Task.Run(async () => + { + if (_value is Func> asyncCallable) + { + return await asyncCallable.Invoke(); + } - public object? Invoke() => Task.Run(() => _callback.Invoke()).GetAwaiter().GetResult(); + if (_value is Func callable) + { + return callable.Invoke(); + } + + return _value; + }).GetAwaiter().GetResult(); + } } From 7860eea4159556897454c13c5f60a03e3c09f3d2 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 16:11:43 -0500 Subject: [PATCH 18/33] dont include Merge props in the json --- InertiaCore/Models/Page.cs | 4 ++ InertiaCore/Response.cs | 5 +++ InertiaCoreTests/UnitTestMergeData.cs | 5 ++- InertiaCoreTests/UnitTestResult.cs | 57 +++++++++++++++++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index 230462a..93ab0a2 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace InertiaCore.Models; internal class Page @@ -6,5 +8,7 @@ internal class Page public string Component { get; set; } = default!; public string? Version { get; set; } public string Url { get; set; } = default!; + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? MergeProps { get; set; } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 1125530..5c1e0e4 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -185,6 +185,11 @@ public Response WithViewData(IDictionary viewData) .Where(resolvedProps.Contains) // Filter only the props that are in the resolved props .ToList(); + if (mergeProps.Count == 0) + { + return null; + } + // Return the result return mergeProps; } diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs index a6e9312..6f35a0c 100644 --- a/InertiaCoreTests/UnitTestMergeData.cs +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -177,7 +177,7 @@ public void TestMergeAsyncPartialDataOmitted() { "errors", new Dictionary(0) } })); - Assert.That(page?.MergeProps, Is.EqualTo(new List { })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); } public void TestNoMergeProps() @@ -201,6 +201,7 @@ public void TestNoMergeProps() { "testFunc", "Func" }, { "errors", new Dictionary(0) } })); - Assert.That(page?.MergeProps, Is.EqualTo(new List { })); + Assert.That(page?.MergeProps, Is.EqualTo(null)); } + } diff --git a/InertiaCoreTests/UnitTestResult.cs b/InertiaCoreTests/UnitTestResult.cs index f3ff8c0..38cfa34 100644 --- a/InertiaCoreTests/UnitTestResult.cs +++ b/InertiaCoreTests/UnitTestResult.cs @@ -1,6 +1,7 @@ using InertiaCore.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using System.Text.Json; namespace InertiaCoreTests; @@ -40,6 +41,62 @@ public void TestJsonResult() { "test", "Test" }, { "errors", new Dictionary(0) } })); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.False); + }); + } + [ + Test] + [Description("Test if the JSON result with merged data is created correctly.")] + public void TestJsonMergedResult() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerged = _factory.Merge(() => "Merged") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia", "true" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + response.ProcessResponse(); + + var result = response.GetResult(); + + Assert.Multiple(() => + { + Assert.That(result, Is.InstanceOf(typeof(JsonResult))); + + var json = (result as JsonResult)?.Value; + Assert.That(json, Is.InstanceOf(typeof(Page))); + + Assert.That((json as Page)?.Component, Is.EqualTo("Test/Page")); + Assert.That((json as Page)?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerged", "Merged" }, + { "errors", new Dictionary(0) } + })); + Assert.That((json as Page)?.MergeProps, Is.EqualTo(new List { + "testMerged" + })); + + // Check the serialized JSON + var jsonString = JsonSerializer.Serialize(json); + var dictionary = JsonSerializer.Deserialize>(jsonString); + + Assert.That(dictionary, Is.Not.Null); + Assert.That(dictionary!.ContainsKey("MergeProps"), Is.True); }); } From 21a8f65ec643955981362902bb448757d817d53f Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 21 Dec 2024 17:04:05 -0500 Subject: [PATCH 19/33] fix formatting --- InertiaCoreTests/UnitTestResult.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InertiaCoreTests/UnitTestResult.cs b/InertiaCoreTests/UnitTestResult.cs index 38cfa34..38988bb 100644 --- a/InertiaCoreTests/UnitTestResult.cs +++ b/InertiaCoreTests/UnitTestResult.cs @@ -50,8 +50,8 @@ public void TestJsonResult() Assert.That(dictionary!.ContainsKey("MergeProps"), Is.False); }); } - [ - Test] + + [Test] [Description("Test if the JSON result with merged data is created correctly.")] public void TestJsonMergedResult() { From cf2c45e05050c3c597c2d3e0dc4acb573833af47 Mon Sep 17 00:00:00 2001 From: kapi2289 Date: Mon, 6 Jan 2025 18:00:31 +0100 Subject: [PATCH 20/33] Minor fixes and changes --- InertiaCore/Props/LazyProp.cs | 23 +++++++++-------------- InertiaCore/Props/OptionalProp.cs | 23 +++++++++-------------- InertiaCore/Response.cs | 2 +- InertiaCore/Utils/IIgnoresFirstLoad.cs | 5 +++++ InertiaCore/Utils/IgnoreFirstLoad.cs | 6 ------ 5 files changed, 24 insertions(+), 35 deletions(-) create mode 100644 InertiaCore/Utils/IIgnoresFirstLoad.cs delete mode 100644 InertiaCore/Utils/IgnoreFirstLoad.cs diff --git a/InertiaCore/Props/LazyProp.cs b/InertiaCore/Props/LazyProp.cs index ade983e..8254830 100644 --- a/InertiaCore/Props/LazyProp.cs +++ b/InertiaCore/Props/LazyProp.cs @@ -2,7 +2,7 @@ namespace InertiaCore.Props; -public class LazyProp : IgnoreFirstLoad +public class LazyProp : IIgnoresFirstLoad { private readonly object? _value; @@ -13,18 +13,13 @@ public class LazyProp : IgnoreFirstLoad { // Check if the value is a callable delegate return Task.Run(async () => - { - if (_value is Func> asyncCallable) - { - return await asyncCallable.Invoke(); - } - - if (_value is Func callable) - { - return callable.Invoke(); - } - - return _value; - }).GetAwaiter().GetResult(); + { + return _value switch + { + Func> asyncCallable => await asyncCallable.Invoke(), + Func callable => callable.Invoke(), + _ => _value + }; + }).GetAwaiter().GetResult(); } } diff --git a/InertiaCore/Props/OptionalProp.cs b/InertiaCore/Props/OptionalProp.cs index d084786..9709b4f 100644 --- a/InertiaCore/Props/OptionalProp.cs +++ b/InertiaCore/Props/OptionalProp.cs @@ -2,7 +2,7 @@ namespace InertiaCore.Props; -public class OptionalProp : IgnoreFirstLoad +public class OptionalProp : IIgnoresFirstLoad { private readonly object? _value; @@ -13,18 +13,13 @@ public class OptionalProp : IgnoreFirstLoad { // Check if the value is a callable delegate return Task.Run(async () => - { - if (_value is Func> asyncCallable) - { - return await asyncCallable.Invoke(); - } - - if (_value is Func callable) - { - return callable.Invoke(); - } - - return _value; - }).GetAwaiter().GetResult(); + { + return _value switch + { + Func> asyncCallable => await asyncCallable.Invoke(), + Func callable => callable.Invoke(), + _ => _value + }; + }).GetAwaiter().GetResult(); } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index e071667..890bc5c 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -119,7 +119,7 @@ public Response WithViewData(IDictionary viewData) if (!isPartial) { props = props - .Where(kv => (kv.Value as IgnoreFirstLoad) == null) + .Where(kv => kv.Value is not IIgnoresFirstLoad) .ToDictionary(kv => kv.Key, kv => kv.Value); } else diff --git a/InertiaCore/Utils/IIgnoresFirstLoad.cs b/InertiaCore/Utils/IIgnoresFirstLoad.cs new file mode 100644 index 0000000..10fc9ba --- /dev/null +++ b/InertiaCore/Utils/IIgnoresFirstLoad.cs @@ -0,0 +1,5 @@ +namespace InertiaCore.Utils; + +public interface IIgnoresFirstLoad +{ +} diff --git a/InertiaCore/Utils/IgnoreFirstLoad.cs b/InertiaCore/Utils/IgnoreFirstLoad.cs deleted file mode 100644 index f5ff0f7..0000000 --- a/InertiaCore/Utils/IgnoreFirstLoad.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace InertiaCore.Utils; - -public interface IgnoreFirstLoad -{ - -} From 42396a772c0efdf572f262ae7775c499427e58fb Mon Sep 17 00:00:00 2001 From: kapi2289 Date: Mon, 6 Jan 2025 18:04:20 +0100 Subject: [PATCH 21/33] Add missing Inertia static methods --- InertiaCore/Inertia.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 9220dc6..6491693 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -29,13 +29,17 @@ public static class Inertia public static void Share(IDictionary data) => _factory.Share(data); + public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); + + public static LazyProp Lazy(Func> callback) => _factory.Lazy(callback); + public static AlwaysProp Always(object? value) => _factory.Always(value); public static AlwaysProp Always(Func callback) => _factory.Always(callback); public static AlwaysProp Always(Func> callback) => _factory.Always(callback); - public static LazyProp Lazy(Func callback) => _factory.Lazy(callback); + public static OptionalProp Optional(Func callback) => _factory.Optional(callback); - public static LazyProp Lazy(Func> callback) => _factory.Lazy(callback); + public static OptionalProp Optional(Func> callback) => _factory.Optional(callback); } From ea4592ee5ae4abefd9af255d8681be37133bd1e7 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 10 Jan 2025 21:32:27 -0500 Subject: [PATCH 22/33] make optional prop invokable --- InertiaCore/Props/OptionalProp.cs | 21 +++++---------------- InertiaCoreTests/UnitTestOptionalData.cs | 16 ++++++++-------- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/InertiaCore/Props/OptionalProp.cs b/InertiaCore/Props/OptionalProp.cs index 9709b4f..cf9c971 100644 --- a/InertiaCore/Props/OptionalProp.cs +++ b/InertiaCore/Props/OptionalProp.cs @@ -2,24 +2,13 @@ namespace InertiaCore.Props; -public class OptionalProp : IIgnoresFirstLoad +public class OptionalProp : InvokableProp, IIgnoresFirstLoad { - private readonly object? _value; - - public OptionalProp(Func callback) => _value = callback; - public OptionalProp(Func> callback) => _value = callback; + internal OptionalProp(Func value) : base(value) + { + } - public object? Invoke() + internal OptionalProp(Func> value) : base(value) { - // Check if the value is a callable delegate - return Task.Run(async () => - { - return _value switch - { - Func> asyncCallable => await asyncCallable.Invoke(), - Func callable => callable.Invoke(), - _ => _value - }; - }).GetAwaiter().GetResult(); } } diff --git a/InertiaCoreTests/UnitTestOptionalData.cs b/InertiaCoreTests/UnitTestOptionalData.cs index 8d8365f..3d925ec 100644 --- a/InertiaCoreTests/UnitTestOptionalData.cs +++ b/InertiaCoreTests/UnitTestOptionalData.cs @@ -7,7 +7,7 @@ public partial class Tests { [Test] [Description("Test if the optional data is fetched properly.")] - public void TestOptionalData() + public async Task TestOptionalData() { var response = _factory.Render("Test/Page", new { @@ -23,7 +23,7 @@ public void TestOptionalData() var context = PrepareContext(); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -37,7 +37,7 @@ public void TestOptionalData() [Test] [Description("Test if the optional data is fetched properly with specified partial props.")] - public void TestOptionalPartialData() + public async Task TestOptionalPartialData() { var response = _factory.Render("Test/Page", new { @@ -54,7 +54,7 @@ public void TestOptionalPartialData() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -69,7 +69,7 @@ public void TestOptionalPartialData() [Test] [Description("Test if the optional async data is fetched properly.")] - public void TestOptionalAsyncData() + public async Task TestOptionalAsyncData() { var testFunction = new Func>(async () => { @@ -88,7 +88,7 @@ public void TestOptionalAsyncData() var context = PrepareContext(); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -102,7 +102,7 @@ public void TestOptionalAsyncData() [Test] [Description("Test if the optional async data is fetched properly with specified partial props.")] - public void TestOptionalAsyncPartialData() + public async Task TestOptionalAsyncPartialData() { var testFunction = new Func>(async () => { @@ -125,7 +125,7 @@ public void TestOptionalAsyncPartialData() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; From 1e949b0db5db84d2429c4593ea01aac783bab8c9 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 10 Jan 2025 21:42:03 -0500 Subject: [PATCH 23/33] fix merge prop tests --- InertiaCore/Props/MergeProp.cs | 25 ++++++++++++++++++++ InertiaCore/Utils/MergeProp.cs | 33 --------------------------- InertiaCoreTests/UnitTestMergeData.cs | 24 +++++++++---------- InertiaCoreTests/UnitTestResult.cs | 4 ++-- 4 files changed, 39 insertions(+), 47 deletions(-) create mode 100644 InertiaCore/Props/MergeProp.cs delete mode 100644 InertiaCore/Utils/MergeProp.cs diff --git a/InertiaCore/Props/MergeProp.cs b/InertiaCore/Props/MergeProp.cs new file mode 100644 index 0000000..6d41883 --- /dev/null +++ b/InertiaCore/Props/MergeProp.cs @@ -0,0 +1,25 @@ +using InertiaCore.Props; + +namespace InertiaCore.Utils; + +public class MergeProp : InvokableProp, Mergeable +{ + public bool merge { get; set; } = true; + + public MergeProp(object? value) : base(value) + { + merge = true; + } + + internal MergeProp(Func value) : base(value) + { + merge = true; + } + + internal MergeProp(Func> value) : base(value) + { + merge = true; + } +} + + diff --git a/InertiaCore/Utils/MergeProp.cs b/InertiaCore/Utils/MergeProp.cs deleted file mode 100644 index c26b7ad..0000000 --- a/InertiaCore/Utils/MergeProp.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace InertiaCore.Utils; - -public class MergeProp : Mergeable -{ - public bool merge { get; set; } = true; - - private readonly object? _value; - - public MergeProp(object? value) - { - _value = value; - merge = true; - } - - public object? Invoke() - { - // Check if the value is a callable delegate - return Task.Run(async () => - { - if (_value is Func> asyncCallable) - { - return await asyncCallable.Invoke(); - } - - if (_value is Func callable) - { - return callable.Invoke(); - } - - return _value; - }).GetAwaiter().GetResult(); - } -} diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs index 6f35a0c..844aaeb 100644 --- a/InertiaCoreTests/UnitTestMergeData.cs +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -7,7 +7,7 @@ public partial class Tests { [Test] [Description("Test if the merge data is fetched properly.")] - public void TestMergeData() + public async Task TestMergeData() { var response = _factory.Render("Test/Page", new { @@ -22,7 +22,7 @@ public void TestMergeData() var context = PrepareContext(); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -38,7 +38,7 @@ public void TestMergeData() [Test] [Description("Test if the merge data is fetched properly with specified partial props.")] - public void TestMergePartialData() + public async Task TestMergePartialData() { var response = _factory.Render("Test/Page", new { @@ -55,7 +55,7 @@ public void TestMergePartialData() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -71,7 +71,7 @@ public void TestMergePartialData() [Test] [Description("Test if the merge async data is fetched properly.")] - public void TestMergeAsyncData() + public async Task TestMergeAsyncData() { var testFunction = new Func>(async () => { @@ -89,7 +89,7 @@ public void TestMergeAsyncData() var context = PrepareContext(); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -105,7 +105,7 @@ public void TestMergeAsyncData() [Test] [Description("Test if the merge async data is fetched properly with specified partial props.")] - public void TestMergeAsyncPartialData() + public async Task TestMergeAsyncPartialData() { var testFunction = new Func>(async () => { @@ -128,7 +128,7 @@ public void TestMergeAsyncPartialData() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -144,7 +144,7 @@ public void TestMergeAsyncPartialData() [Test] [Description("Test if the merge async data is fetched properly without specified partial props.")] - public void TestMergeAsyncPartialDataOmitted() + public async Task TestMergeAsyncPartialDataOmitted() { var testFunction = new Func>(async () => { @@ -167,7 +167,7 @@ public void TestMergeAsyncPartialDataOmitted() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; @@ -180,7 +180,7 @@ public void TestMergeAsyncPartialDataOmitted() Assert.That(page?.MergeProps, Is.EqualTo(null)); } - public void TestNoMergeProps() + public async Task TestNoMergeProps() { var response = _factory.Render("Test/Page", new { @@ -191,7 +191,7 @@ public void TestNoMergeProps() var context = PrepareContext(); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var page = response.GetJson().Value as Page; diff --git a/InertiaCoreTests/UnitTestResult.cs b/InertiaCoreTests/UnitTestResult.cs index 8fb08c6..36e4cc9 100644 --- a/InertiaCoreTests/UnitTestResult.cs +++ b/InertiaCoreTests/UnitTestResult.cs @@ -53,7 +53,7 @@ public async Task TestJsonResult() [Test] [Description("Test if the JSON result with merged data is created correctly.")] - public void TestJsonMergedResult() + public async Task TestJsonMergedResult() { var response = _factory.Render("Test/Page", new { @@ -69,7 +69,7 @@ public void TestJsonMergedResult() var context = PrepareContext(headers); response.SetContext(context); - response.ProcessResponse(); + await response.ProcessResponse(); var result = response.GetResult(); From ad8c4762589386ad9d85f63a29b10effc0859152 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 10 Jan 2025 21:49:12 -0500 Subject: [PATCH 24/33] fix test sdks? --- .github/workflows/dotnet.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b78f820..599ad5b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,7 +23,11 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + 9.0.x - name: Restore dependencies run: dotnet restore - name: Build From 6333daa445558a6967f280585658af5230154353 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Fri, 10 Jan 2025 21:49:12 -0500 Subject: [PATCH 25/33] fix test sdks? --- .github/workflows/dotnet.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b78f820..599ad5b 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,7 +23,11 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 9.0.x + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + 9.0.x - name: Restore dependencies run: dotnet restore - name: Build From 2ae0bffe995101e9fd114729a76bd50fe823bcbc Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 8 Feb 2025 11:31:14 -0500 Subject: [PATCH 26/33] revert formatting --- .github/workflows/dotnet.yml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 6788a6d..dfea3e2 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -15,21 +15,22 @@ on: jobs: build: + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - 9.0.x - - name: Restore dependencies - run: dotnet restore - - name: Build - run: dotnet build --no-restore - - name: Test - run: dotnet test --no-build --verbosity normal + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + 9.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From 0e9353c18d22d15247f64eed354f15d2d0f7c8e1 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 8 Feb 2025 11:31:39 -0500 Subject: [PATCH 27/33] one more formatting fix --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index dfea3e2..899c0f2 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -15,7 +15,7 @@ on: jobs: build: - + runs-on: ubuntu-latest steps: From d842ac824e405cccc62b8224749463808411e8df Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 13:02:26 -0400 Subject: [PATCH 28/33] [2.x] Keep only partial data in mergeProps https://github.com/inertiajs/inertia-laravel/pull/745 --- InertiaCore/Response.cs | 19 +++++ InertiaCoreTests/UnitTestMergeData.cs | 112 ++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index b6779a4..9fd7cc3 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -159,6 +159,23 @@ protected internal async Task ProcessResponse() StringComparer.OrdinalIgnoreCase ); + // Parse the "PARTIAL_ONLY" header into a collection of keys to include + var onlyProps = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Parse the "PARTIAL_EXCEPT" header into a collection of keys to exclude + var exceptProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + var resolvedProps = props .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase .ToList(); @@ -166,6 +183,8 @@ protected internal async Task ProcessResponse() // Filter the props that are Mergeable and should be merged var mergeProps = _props.Where(o => o.Value is Mergeable mergeable && mergeable.ShouldMerge()) // Check if value is Mergeable and should merge .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys + .Where(kv => onlyProps.Count == 0 || onlyProps.Contains(kv.Key)) // Include only specified keys if any + .Where(kv => !exceptProps.Contains(kv.Key)) // Exclude specified keys .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase .Where(resolvedProps.Contains) // Filter only the props that are in the resolved props .ToList(); diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs index 7065f8d..2a09a07 100644 --- a/InertiaCoreTests/UnitTestMergeData.cs +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -206,4 +206,116 @@ public async Task TestNoMergeProps() Assert.That(page?.MergeProps, Is.EqualTo(null)); } + [Test] + [Description("Test if merge props are excluded when using PARTIAL_EXCEPT header.")] + public async Task TestMergePropsWithPartialExcept() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge1 = _factory.Merge(() => "Merge1"), + TestMerge2 = _factory.Merge(() => "Merge2"), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Except", "testMerge1" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerge2", "Merge2" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + // testMerge1 should be excluded from merge props due to PARTIAL_EXCEPT header + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge2" })); + } + + [Test] + [Description("Test if only specified merge props are included when using PARTIAL_ONLY header.")] + public async Task TestMergePropsWithPartialOnly() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge1 = _factory.Merge(() => "Merge1"), + TestMerge2 = _factory.Merge(() => "Merge2"), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testMerge1,testNormal" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testMerge1", "Merge1" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + // Only testMerge1 should be in merge props since testMerge2 was not included in PARTIAL_ONLY + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1" })); + } + + [Test] + [Description("Test if merge props respect both PARTIAL_ONLY and PARTIAL_EXCEPT headers.")] + public async Task TestMergePropsWithPartialOnlyAndExcept() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge1 = _factory.Merge(() => "Merge1"), + TestMerge2 = _factory.Merge(() => "Merge2"), + TestMerge3 = _factory.Merge(() => "Merge3"), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testMerge1,testMerge2,testMerge3,testNormal" }, + { "X-Inertia-Partial-Except", "testMerge2" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testMerge1", "Merge1" }, + { "testMerge3", "Merge3" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + // testMerge1 and testMerge3 should be in merge props (testMerge2 excluded by PARTIAL_EXCEPT) + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge3" })); + } + } From 58913c939cca09f38f24732483927c2e9839a3b9 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 16:46:14 -0400 Subject: [PATCH 29/33] [2.x] Allow deepMerge on custom properties https://github.com/inertiajs/inertia-laravel/pull/732 https://github.com/inertiajs/inertia/pull/2344 --- InertiaCore/Inertia.cs | 12 +++ InertiaCore/Models/Page.cs | 3 + InertiaCore/Props/MergeProp.cs | 37 +++++++ InertiaCore/Response.cs | 56 ++++++++++ InertiaCore/ResponseFactory.cs | 12 +++ InertiaCore/Utils/Mergeable.cs | 2 + InertiaCoreTests/UnitTestMergeData.cs | 145 ++++++++++++++++++++++++++ 7 files changed, 267 insertions(+) diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 81f88c6..6bd0ba4 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -50,4 +50,16 @@ public static class Inertia public static MergeProp Merge(Func callback) => _factory.Merge(callback); public static MergeProp Merge(Func> callback) => _factory.Merge(callback); + + public static MergeProp Merge(object? value, string strategy) => _factory.Merge(value, strategy); + + public static MergeProp Merge(object? value, string[]? strategies) => _factory.Merge(value, strategies); + + public static MergeProp Merge(Func callback, string strategy) => _factory.Merge(callback, strategy); + + public static MergeProp Merge(Func callback, string[]? strategies) => _factory.Merge(callback, strategies); + + public static MergeProp Merge(Func> callback, string strategy) => _factory.Merge(callback, strategy); + + public static MergeProp Merge(Func> callback, string[]? strategies) => _factory.Merge(callback, strategies); } diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index d64b751..cf5e4f9 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -13,4 +13,7 @@ internal class Page [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? MergeProps { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public Dictionary? MergeStrategies { get; set; } } diff --git a/InertiaCore/Props/MergeProp.cs b/InertiaCore/Props/MergeProp.cs index 6d41883..69d943f 100644 --- a/InertiaCore/Props/MergeProp.cs +++ b/InertiaCore/Props/MergeProp.cs @@ -5,21 +5,58 @@ namespace InertiaCore.Utils; public class MergeProp : InvokableProp, Mergeable { public bool merge { get; set; } = true; + public string[]? mergeStrategies { get; set; } public MergeProp(object? value) : base(value) { merge = true; } + public MergeProp(object? value, string[]? strategies) : base(value) + { + merge = true; + mergeStrategies = strategies; + } + + public MergeProp(object? value, string strategy) : base(value) + { + merge = true; + mergeStrategies = new[] { strategy }; + } + internal MergeProp(Func value) : base(value) { merge = true; } + internal MergeProp(Func value, string[]? strategies) : base(value) + { + merge = true; + mergeStrategies = strategies; + } + + internal MergeProp(Func value, string strategy) : base(value) + { + merge = true; + mergeStrategies = new[] { strategy }; + } + internal MergeProp(Func> value) : base(value) { merge = true; } + + internal MergeProp(Func> value, string[]? strategies) : base(value) + { + merge = true; + mergeStrategies = strategies; + } + + internal MergeProp(Func> value, string strategy) : base(value) + { + merge = true; + mergeStrategies = new[] { strategy }; + } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 9fd7cc3..398fae3 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -48,6 +48,7 @@ protected internal async Task ProcessResponse() }; page.MergeProps = ResolveMergeProps(props); + page.MergeStrategies = ResolveMergeStrategies(props); page.Props["errors"] = GetErrors(); SetPage(page); @@ -198,6 +199,61 @@ protected internal async Task ProcessResponse() return mergeProps; } + /// + /// Resolve merge strategies for properties that should be merged with custom strategies. + /// + private Dictionary? ResolveMergeStrategies(Dictionary props) + { + // Parse the "RESET" header into a collection of keys to reset + var resetProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.Reset] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + // Parse the "PARTIAL_ONLY" header into a collection of keys to include + var onlyProps = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Parse the "PARTIAL_EXCEPT" header into a collection of keys to exclude + var exceptProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + var resolvedProps = props + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .ToList(); + + // Filter the props that have merge strategies + var mergeStrategies = _props.Where(o => o.Value is Mergeable mergeable && mergeable.ShouldMerge() && mergeable.GetMergeStrategies() != null) + .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys + .Where(kv => onlyProps.Count == 0 || onlyProps.Contains(kv.Key)) // Include only specified keys if any + .Where(kv => !exceptProps.Contains(kv.Key)) // Exclude specified keys + .Where(kv => resolvedProps.Contains(kv.Key.ToCamelCase())) // Filter only the props that are in the resolved props + .ToDictionary( + kv => kv.Key.ToCamelCase(), // Convert property name to camelCase + kv => ((Mergeable)kv.Value!).GetMergeStrategies()! + ); + + if (mergeStrategies.Count == 0) + { + return null; + } + + // Return the result + return mergeStrategies; + } + /// /// Resolve all necessary class instances in the given props. /// diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 62b71a0..405080d 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -32,6 +32,12 @@ internal interface IResponseFactory public MergeProp Merge(object? value); public MergeProp Merge(Func callback); public MergeProp Merge(Func> callback); + public MergeProp Merge(object? value, string strategy); + public MergeProp Merge(object? value, string[]? strategies); + public MergeProp Merge(Func callback, string strategy); + public MergeProp Merge(Func callback, string[]? strategies); + public MergeProp Merge(Func> callback, string strategy); + public MergeProp Merge(Func> callback, string[]? strategies); public OptionalProp Optional(Func callback); public OptionalProp Optional(Func> callback); } @@ -152,6 +158,12 @@ public void Share(IDictionary data) public MergeProp Merge(object? value) => new(value); public MergeProp Merge(Func callback) => new(callback); public MergeProp Merge(Func> callback) => new(callback); + public MergeProp Merge(object? value, string strategy) => new(value, strategy); + public MergeProp Merge(object? value, string[]? strategies) => new(value, strategies); + public MergeProp Merge(Func callback, string strategy) => new(callback, strategy); + public MergeProp Merge(Func callback, string[]? strategies) => new(callback, strategies); + public MergeProp Merge(Func> callback, string strategy) => new(callback, strategy); + public MergeProp Merge(Func> callback, string[]? strategies) => new(callback, strategies); public OptionalProp Optional(Func callback) => new(callback); public OptionalProp Optional(Func> callback) => new(callback); } diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs index b7e5b6e..bcd3616 100644 --- a/InertiaCore/Utils/Mergeable.cs +++ b/InertiaCore/Utils/Mergeable.cs @@ -3,6 +3,7 @@ namespace InertiaCore.Utils; public interface Mergeable { public bool merge { get; set; } + public string[]? mergeStrategies { get; set; } public Mergeable Merge() { @@ -12,4 +13,5 @@ public Mergeable Merge() } public bool ShouldMerge() => merge; + public string[]? GetMergeStrategies() => mergeStrategies; } diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs index 2a09a07..606fa2e 100644 --- a/InertiaCoreTests/UnitTestMergeData.cs +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -318,4 +318,149 @@ public async Task TestMergePropsWithPartialOnlyAndExcept() Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge3" })); } + [Test] + [Description("Test if merge strategies are resolved properly for merge props.")] + public async Task TestMergeStrategies() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge1 = _factory.Merge("Merge1", "deep"), + TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), + TestNormal = "Normal" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerge1", "Merge1" }, + { "testMerge2", "Merge2" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge2" })); + Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + { + { "testMerge1", new[] { "deep" } }, + { "testMerge2", new[] { "shallow", "replace" } } + })); + } + + [Test] + [Description("Test if merge strategies are handled properly with partial props.")] + public async Task TestMergeStrategiesWithPartialProps() + { + var response = _factory.Render("Test/Page", new + { + TestMerge1 = _factory.Merge("Merge1", "deep"), + TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), + TestMerge3 = _factory.Merge("Merge3", "custom") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testMerge1,testMerge3" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testMerge1", "Merge1" }, + { "testMerge3", "Merge3" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge3" })); + Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + { + { "testMerge1", new[] { "deep" } }, + { "testMerge3", new[] { "custom" } } + })); + } + + [Test] + [Description("Test if merge strategies are excluded when using PARTIAL_EXCEPT header.")] + public async Task TestMergeStrategiesWithPartialExcept() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge1 = _factory.Merge("Merge1", "deep"), + TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Except", "testMerge1" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerge2", "Merge2" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge2" })); + Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + { + { "testMerge2", new[] { "shallow", "replace" } } + })); + } + + [Test] + [Description("Test if merge strategies are null when no merge props have strategies.")] + public async Task TestNoMergeStrategies() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge = _factory.Merge(() => "Merge"), // No strategies + TestNormal = "Normal" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerge", "Merge" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); + Assert.That(page?.MergeStrategies, Is.EqualTo(null)); + } + } From 0d393c0cafdfbbe1a2e6fd91644d27e24589f137 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 22 Sep 2025 02:24:53 -0400 Subject: [PATCH 30/33] Add Inertia::deepMerge Method for Handling Complex Data Merges in Responses # https://github.com/inertiajs/inertia-laravel/pull/679 --- InertiaCore/Inertia.cs | 18 ++ InertiaCore/Models/Page.cs | 3 + InertiaCore/Props/DeepMergeProp.cs | 72 +++++ InertiaCore/Response.cs | 54 ++++ InertiaCore/ResponseFactory.cs | 18 ++ InertiaCoreTests/UnitTestDeepMergeData.cs | 311 ++++++++++++++++++++++ 6 files changed, 476 insertions(+) create mode 100644 InertiaCore/Props/DeepMergeProp.cs create mode 100644 InertiaCoreTests/UnitTestDeepMergeData.cs diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 6bd0ba4..41c1a74 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -62,4 +62,22 @@ public static class Inertia public static MergeProp Merge(Func> callback, string strategy) => _factory.Merge(callback, strategy); public static MergeProp Merge(Func> callback, string[]? strategies) => _factory.Merge(callback, strategies); + + public static DeepMergeProp DeepMerge(object? value) => _factory.DeepMerge(value); + + public static DeepMergeProp DeepMerge(Func callback) => _factory.DeepMerge(callback); + + public static DeepMergeProp DeepMerge(Func> callback) => _factory.DeepMerge(callback); + + public static DeepMergeProp DeepMerge(object? value, string strategy) => _factory.DeepMerge(value, strategy); + + public static DeepMergeProp DeepMerge(object? value, string[]? strategies) => _factory.DeepMerge(value, strategies); + + public static DeepMergeProp DeepMerge(Func callback, string strategy) => _factory.DeepMerge(callback, strategy); + + public static DeepMergeProp DeepMerge(Func callback, string[]? strategies) => _factory.DeepMerge(callback, strategies); + + public static DeepMergeProp DeepMerge(Func> callback, string strategy) => _factory.DeepMerge(callback, strategy); + + public static DeepMergeProp DeepMerge(Func> callback, string[]? strategies) => _factory.DeepMerge(callback, strategies); } diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index cf5e4f9..0073e7f 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -16,4 +16,7 @@ internal class Page [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public Dictionary? MergeStrategies { get; set; } + + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public List? DeepMergeProps { get; set; } } diff --git a/InertiaCore/Props/DeepMergeProp.cs b/InertiaCore/Props/DeepMergeProp.cs new file mode 100644 index 0000000..133db11 --- /dev/null +++ b/InertiaCore/Props/DeepMergeProp.cs @@ -0,0 +1,72 @@ +using InertiaCore.Utils; + +namespace InertiaCore.Props; + +public class DeepMergeProp : InvokableProp, Mergeable +{ + public bool merge { get; set; } = true; + public string[]? mergeStrategies { get; set; } + public bool deepMerge { get; set; } = true; + + public DeepMergeProp(object? value) : base(value) + { + merge = true; + deepMerge = true; + } + + public DeepMergeProp(object? value, string[]? strategies) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = strategies; + } + + public DeepMergeProp(object? value, string strategy) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = new[] { strategy }; + } + + internal DeepMergeProp(Func value) : base(value) + { + merge = true; + deepMerge = true; + } + + internal DeepMergeProp(Func value, string[]? strategies) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = strategies; + } + + internal DeepMergeProp(Func value, string strategy) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = new[] { strategy }; + } + + internal DeepMergeProp(Func> value) : base(value) + { + merge = true; + deepMerge = true; + } + + internal DeepMergeProp(Func> value, string[]? strategies) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = strategies; + } + + internal DeepMergeProp(Func> value, string strategy) : base(value) + { + merge = true; + deepMerge = true; + mergeStrategies = new[] { strategy }; + } + + public bool ShouldDeepMerge() => deepMerge; +} \ No newline at end of file diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 398fae3..128a300 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -49,6 +49,7 @@ protected internal async Task ProcessResponse() page.MergeProps = ResolveMergeProps(props); page.MergeStrategies = ResolveMergeStrategies(props); + page.DeepMergeProps = ResolveDeepMergeProps(props); page.Props["errors"] = GetErrors(); SetPage(page); @@ -254,6 +255,59 @@ protected internal async Task ProcessResponse() return mergeStrategies; } + /// + /// Resolve deep merge properties that should be deeply merged with existing values by the front-end. + /// + private List? ResolveDeepMergeProps(Dictionary props) + { + // Parse the "RESET" header into a collection of keys to reset + var resetProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.Reset] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + // Parse the "PARTIAL_ONLY" header into a collection of keys to include + var onlyProps = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()) + .Where(s => !string.IsNullOrEmpty(s)) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + // Parse the "PARTIAL_EXCEPT" header into a collection of keys to exclude + var exceptProps = new HashSet( + _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept] + .ToString() + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => s.Trim()), + StringComparer.OrdinalIgnoreCase + ); + + var resolvedProps = props + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .ToList(); + + // Filter the props that are DeepMergeable and should be deeply merged + var deepMergeProps = _props.Where(o => o.Value is DeepMergeProp deepMergeable && deepMergeable.ShouldDeepMerge()) // Check if value is DeepMergeProp and should deep merge + .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys + .Where(kv => onlyProps.Count == 0 || onlyProps.Contains(kv.Key)) // Include only specified keys if any + .Where(kv => !exceptProps.Contains(kv.Key)) // Exclude specified keys + .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase + .Where(resolvedProps.Contains) // Filter only the props that are in the resolved props + .ToList(); + + if (deepMergeProps.Count == 0) + { + return null; + } + + // Return the result + return deepMergeProps; + } + /// /// Resolve all necessary class instances in the given props. /// diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 405080d..7689a11 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -38,6 +38,15 @@ internal interface IResponseFactory public MergeProp Merge(Func callback, string[]? strategies); public MergeProp Merge(Func> callback, string strategy); public MergeProp Merge(Func> callback, string[]? strategies); + public DeepMergeProp DeepMerge(object? value); + public DeepMergeProp DeepMerge(Func callback); + public DeepMergeProp DeepMerge(Func> callback); + public DeepMergeProp DeepMerge(object? value, string strategy); + public DeepMergeProp DeepMerge(object? value, string[]? strategies); + public DeepMergeProp DeepMerge(Func callback, string strategy); + public DeepMergeProp DeepMerge(Func callback, string[]? strategies); + public DeepMergeProp DeepMerge(Func> callback, string strategy); + public DeepMergeProp DeepMerge(Func> callback, string[]? strategies); public OptionalProp Optional(Func callback); public OptionalProp Optional(Func> callback); } @@ -164,6 +173,15 @@ public void Share(IDictionary data) public MergeProp Merge(Func callback, string[]? strategies) => new(callback, strategies); public MergeProp Merge(Func> callback, string strategy) => new(callback, strategy); public MergeProp Merge(Func> callback, string[]? strategies) => new(callback, strategies); + public DeepMergeProp DeepMerge(object? value) => new(value); + public DeepMergeProp DeepMerge(Func callback) => new(callback); + public DeepMergeProp DeepMerge(Func> callback) => new(callback); + public DeepMergeProp DeepMerge(object? value, string strategy) => new(value, strategy); + public DeepMergeProp DeepMerge(object? value, string[]? strategies) => new(value, strategies); + public DeepMergeProp DeepMerge(Func callback, string strategy) => new(callback, strategy); + public DeepMergeProp DeepMerge(Func callback, string[]? strategies) => new(callback, strategies); + public DeepMergeProp DeepMerge(Func> callback, string strategy) => new(callback, strategy); + public DeepMergeProp DeepMerge(Func> callback, string[]? strategies) => new(callback, strategies); public OptionalProp Optional(Func callback) => new(callback); public OptionalProp Optional(Func> callback) => new(callback); } diff --git a/InertiaCoreTests/UnitTestDeepMergeData.cs b/InertiaCoreTests/UnitTestDeepMergeData.cs new file mode 100644 index 0000000..53faf5f --- /dev/null +++ b/InertiaCoreTests/UnitTestDeepMergeData.cs @@ -0,0 +1,311 @@ +using InertiaCore.Models; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if the deep merge data is fetched properly.")] + public async Task TestDeepMergeData() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestDeepMerge = _factory.DeepMerge(() => + { + return "Deep Merge"; + }) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testDeepMerge", "Deep Merge" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge" })); + } + + [Test] + [Description("Test if the deep merge data is fetched properly with specified partial props.")] + public async Task TestDeepMergePartialData() + { + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDeepMerge = _factory.DeepMerge(() => "Deep Merge") + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc,testDeepMerge" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "testDeepMerge", "Deep Merge" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge" })); + } + + [Test] + [Description("Test if the deep merge async data is fetched properly.")] + public async Task TestDeepMergeAsyncData() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Deep Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + TestDeepMerge = _factory.DeepMerge(testFunction) + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "testDeepMerge", "Deep Merge Async" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge" })); + } + + [Test] + [Description("Test if the deep merge data is fetched properly without specified partial props.")] + public async Task TestDeepMergePartialDataOmitted() + { + var testFunction = new Func>(async () => + { + await Task.Delay(100); + return "Deep Merge Async"; + }); + + var response = _factory.Render("Test/Page", new + { + TestFunc = new Func(() => "Func"), + TestDeepMerge = _factory.DeepMerge(async () => await testFunction()) + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testFunc" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeepMergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if there are no deep merge props when none are specified.")] + public async Task TestNoDeepMergeProps() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestFunc = new Func(() => "Func"), + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testFunc", "Func" }, + { "errors", new Dictionary(0) } + })); + Assert.That(page?.DeepMergeProps, Is.EqualTo(null)); + } + + [Test] + [Description("Test if deep merge props are excluded when using PARTIAL_EXCEPT header.")] + public async Task TestDeepMergePropsWithPartialExcept() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestDeepMerge1 = _factory.DeepMerge(() => "Deep Merge1"), + TestDeepMerge2 = _factory.DeepMerge(() => "Deep Merge2"), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Except", "testDeepMerge1" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testDeepMerge2", "Deep Merge2" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + // testDeepMerge1 should be excluded from deep merge props due to PARTIAL_EXCEPT header + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge2" })); + } + + [Test] + [Description("Test if only specified deep merge props are included when using PARTIAL_ONLY header.")] + public async Task TestDeepMergePropsWithPartialOnly() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestDeepMerge1 = _factory.DeepMerge(() => "Deep Merge1"), + TestDeepMerge2 = _factory.DeepMerge(() => "Deep Merge2"), + TestNormal = "Normal" + }); + + var headers = new HeaderDictionary + { + { "X-Inertia-Partial-Data", "testDeepMerge1,testNormal" }, + { "X-Inertia-Partial-Component", "Test/Page" } + }; + + var context = PrepareContext(headers); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "testDeepMerge1", "Deep Merge1" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + // Only testDeepMerge1 should be in deep merge props since testDeepMerge2 was not included in PARTIAL_ONLY + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge1" })); + } + + [Test] + [Description("Test if deep merge props work with strategies.")] + public async Task TestDeepMergeWithStrategies() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestDeepMerge1 = _factory.DeepMerge("Deep Merge1", "deep"), + TestDeepMerge2 = _factory.DeepMerge(() => "Deep Merge2", new[] { "shallow", "replace" }), + TestNormal = "Normal" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testDeepMerge1", "Deep Merge1" }, + { "testDeepMerge2", "Deep Merge2" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge1", "testDeepMerge2" })); + // Deep merge props should also appear in merge strategies since they inherit from Mergeable + Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + { + { "testDeepMerge1", new[] { "deep" } }, + { "testDeepMerge2", new[] { "shallow", "replace" } } + })); + } + + [Test] + [Description("Test if regular merge and deep merge props coexist properly.")] + public async Task TestMergeAndDeepMergeCoexistence() + { + var response = _factory.Render("Test/Page", new + { + Test = "Test", + TestMerge = _factory.Merge(() => "Regular Merge"), + TestDeepMerge = _factory.DeepMerge(() => "Deep Merge"), + TestNormal = "Normal" + }); + + var context = PrepareContext(); + + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "test", "Test" }, + { "testMerge", "Regular Merge" }, + { "testDeepMerge", "Deep Merge" }, + { "testNormal", "Normal" }, + { "errors", new Dictionary(0) } + })); + + Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge", "testDeepMerge" })); + Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge" })); + } +} \ No newline at end of file From a82297ad93070af4930be12a53a9c704205bdf9b Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 22 Sep 2025 02:31:25 -0400 Subject: [PATCH 31/33] [2.x] Refactor mergeStrategies argument to matchOn() method https://github.com/inertiajs/inertia-laravel/pull/747 --- InertiaCore/Inertia.cs | 24 ------------- InertiaCore/Models/Page.cs | 2 +- InertiaCore/Props/DeepMergeProp.cs | 44 +---------------------- InertiaCore/Props/MergeProp.cs | 38 +------------------- InertiaCore/Response.cs | 16 ++++----- InertiaCore/ResponseFactory.cs | 24 ------------- InertiaCore/Utils/Mergeable.cs | 10 ++++-- InertiaCoreTests/UnitTestDeepMergeData.cs | 13 +++---- InertiaCoreTests/UnitTestMergeData.cs | 39 ++++++++++---------- 9 files changed, 46 insertions(+), 164 deletions(-) diff --git a/InertiaCore/Inertia.cs b/InertiaCore/Inertia.cs index 41c1a74..8f6a4c9 100644 --- a/InertiaCore/Inertia.cs +++ b/InertiaCore/Inertia.cs @@ -51,33 +51,9 @@ public static class Inertia public static MergeProp Merge(Func> callback) => _factory.Merge(callback); - public static MergeProp Merge(object? value, string strategy) => _factory.Merge(value, strategy); - - public static MergeProp Merge(object? value, string[]? strategies) => _factory.Merge(value, strategies); - - public static MergeProp Merge(Func callback, string strategy) => _factory.Merge(callback, strategy); - - public static MergeProp Merge(Func callback, string[]? strategies) => _factory.Merge(callback, strategies); - - public static MergeProp Merge(Func> callback, string strategy) => _factory.Merge(callback, strategy); - - public static MergeProp Merge(Func> callback, string[]? strategies) => _factory.Merge(callback, strategies); - public static DeepMergeProp DeepMerge(object? value) => _factory.DeepMerge(value); public static DeepMergeProp DeepMerge(Func callback) => _factory.DeepMerge(callback); public static DeepMergeProp DeepMerge(Func> callback) => _factory.DeepMerge(callback); - - public static DeepMergeProp DeepMerge(object? value, string strategy) => _factory.DeepMerge(value, strategy); - - public static DeepMergeProp DeepMerge(object? value, string[]? strategies) => _factory.DeepMerge(value, strategies); - - public static DeepMergeProp DeepMerge(Func callback, string strategy) => _factory.DeepMerge(callback, strategy); - - public static DeepMergeProp DeepMerge(Func callback, string[]? strategies) => _factory.DeepMerge(callback, strategies); - - public static DeepMergeProp DeepMerge(Func> callback, string strategy) => _factory.DeepMerge(callback, strategy); - - public static DeepMergeProp DeepMerge(Func> callback, string[]? strategies) => _factory.DeepMerge(callback, strategies); } diff --git a/InertiaCore/Models/Page.cs b/InertiaCore/Models/Page.cs index 0073e7f..df7fbc4 100644 --- a/InertiaCore/Models/Page.cs +++ b/InertiaCore/Models/Page.cs @@ -15,7 +15,7 @@ internal class Page public List? MergeProps { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public Dictionary? MergeStrategies { get; set; } + public Dictionary? MatchPropsOn { get; set; } [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public List? DeepMergeProps { get; set; } diff --git a/InertiaCore/Props/DeepMergeProp.cs b/InertiaCore/Props/DeepMergeProp.cs index 133db11..f2a03b2 100644 --- a/InertiaCore/Props/DeepMergeProp.cs +++ b/InertiaCore/Props/DeepMergeProp.cs @@ -5,7 +5,7 @@ namespace InertiaCore.Props; public class DeepMergeProp : InvokableProp, Mergeable { public bool merge { get; set; } = true; - public string[]? mergeStrategies { get; set; } + public string[]? matchOn { get; set; } public bool deepMerge { get; set; } = true; public DeepMergeProp(object? value) : base(value) @@ -14,59 +14,17 @@ public DeepMergeProp(object? value) : base(value) deepMerge = true; } - public DeepMergeProp(object? value, string[]? strategies) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = strategies; - } - - public DeepMergeProp(object? value, string strategy) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = new[] { strategy }; - } - internal DeepMergeProp(Func value) : base(value) { merge = true; deepMerge = true; } - internal DeepMergeProp(Func value, string[]? strategies) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = strategies; - } - - internal DeepMergeProp(Func value, string strategy) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = new[] { strategy }; - } - internal DeepMergeProp(Func> value) : base(value) { merge = true; deepMerge = true; } - internal DeepMergeProp(Func> value, string[]? strategies) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = strategies; - } - - internal DeepMergeProp(Func> value, string strategy) : base(value) - { - merge = true; - deepMerge = true; - mergeStrategies = new[] { strategy }; - } - public bool ShouldDeepMerge() => deepMerge; } \ No newline at end of file diff --git a/InertiaCore/Props/MergeProp.cs b/InertiaCore/Props/MergeProp.cs index 69d943f..f0d55f8 100644 --- a/InertiaCore/Props/MergeProp.cs +++ b/InertiaCore/Props/MergeProp.cs @@ -5,58 +5,22 @@ namespace InertiaCore.Utils; public class MergeProp : InvokableProp, Mergeable { public bool merge { get; set; } = true; - public string[]? mergeStrategies { get; set; } + public string[]? matchOn { get; set; } public MergeProp(object? value) : base(value) { merge = true; } - public MergeProp(object? value, string[]? strategies) : base(value) - { - merge = true; - mergeStrategies = strategies; - } - - public MergeProp(object? value, string strategy) : base(value) - { - merge = true; - mergeStrategies = new[] { strategy }; - } - internal MergeProp(Func value) : base(value) { merge = true; } - internal MergeProp(Func value, string[]? strategies) : base(value) - { - merge = true; - mergeStrategies = strategies; - } - - internal MergeProp(Func value, string strategy) : base(value) - { - merge = true; - mergeStrategies = new[] { strategy }; - } - internal MergeProp(Func> value) : base(value) { merge = true; } - - internal MergeProp(Func> value, string[]? strategies) : base(value) - { - merge = true; - mergeStrategies = strategies; - } - - internal MergeProp(Func> value, string strategy) : base(value) - { - merge = true; - mergeStrategies = new[] { strategy }; - } } diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 128a300..1a08dd2 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -48,7 +48,7 @@ protected internal async Task ProcessResponse() }; page.MergeProps = ResolveMergeProps(props); - page.MergeStrategies = ResolveMergeStrategies(props); + page.MatchPropsOn = ResolveMatchPropsOn(props); page.DeepMergeProps = ResolveDeepMergeProps(props); page.Props["errors"] = GetErrors(); @@ -201,9 +201,9 @@ protected internal async Task ProcessResponse() } /// - /// Resolve merge strategies for properties that should be merged with custom strategies. + /// Resolve match props on for properties that should be matched on specific keys. /// - private Dictionary? ResolveMergeStrategies(Dictionary props) + private Dictionary? ResolveMatchPropsOn(Dictionary props) { // Parse the "RESET" header into a collection of keys to reset var resetProps = new HashSet( @@ -235,24 +235,24 @@ protected internal async Task ProcessResponse() .Select(kv => kv.Key.ToCamelCase()) // Convert property name to camelCase .ToList(); - // Filter the props that have merge strategies - var mergeStrategies = _props.Where(o => o.Value is Mergeable mergeable && mergeable.ShouldMerge() && mergeable.GetMergeStrategies() != null) + // Filter the props that have match on keys + var matchPropsOn = _props.Where(o => o.Value is Mergeable mergeable && mergeable.ShouldMerge() && mergeable.GetMatchOn() != null) .Where(kv => !resetProps.Contains(kv.Key)) // Exclude reset keys .Where(kv => onlyProps.Count == 0 || onlyProps.Contains(kv.Key)) // Include only specified keys if any .Where(kv => !exceptProps.Contains(kv.Key)) // Exclude specified keys .Where(kv => resolvedProps.Contains(kv.Key.ToCamelCase())) // Filter only the props that are in the resolved props .ToDictionary( kv => kv.Key.ToCamelCase(), // Convert property name to camelCase - kv => ((Mergeable)kv.Value!).GetMergeStrategies()! + kv => ((Mergeable)kv.Value!).GetMatchOn()! ); - if (mergeStrategies.Count == 0) + if (matchPropsOn.Count == 0) { return null; } // Return the result - return mergeStrategies; + return matchPropsOn; } /// diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index 7689a11..870e667 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -32,21 +32,9 @@ internal interface IResponseFactory public MergeProp Merge(object? value); public MergeProp Merge(Func callback); public MergeProp Merge(Func> callback); - public MergeProp Merge(object? value, string strategy); - public MergeProp Merge(object? value, string[]? strategies); - public MergeProp Merge(Func callback, string strategy); - public MergeProp Merge(Func callback, string[]? strategies); - public MergeProp Merge(Func> callback, string strategy); - public MergeProp Merge(Func> callback, string[]? strategies); public DeepMergeProp DeepMerge(object? value); public DeepMergeProp DeepMerge(Func callback); public DeepMergeProp DeepMerge(Func> callback); - public DeepMergeProp DeepMerge(object? value, string strategy); - public DeepMergeProp DeepMerge(object? value, string[]? strategies); - public DeepMergeProp DeepMerge(Func callback, string strategy); - public DeepMergeProp DeepMerge(Func callback, string[]? strategies); - public DeepMergeProp DeepMerge(Func> callback, string strategy); - public DeepMergeProp DeepMerge(Func> callback, string[]? strategies); public OptionalProp Optional(Func callback); public OptionalProp Optional(Func> callback); } @@ -167,21 +155,9 @@ public void Share(IDictionary data) public MergeProp Merge(object? value) => new(value); public MergeProp Merge(Func callback) => new(callback); public MergeProp Merge(Func> callback) => new(callback); - public MergeProp Merge(object? value, string strategy) => new(value, strategy); - public MergeProp Merge(object? value, string[]? strategies) => new(value, strategies); - public MergeProp Merge(Func callback, string strategy) => new(callback, strategy); - public MergeProp Merge(Func callback, string[]? strategies) => new(callback, strategies); - public MergeProp Merge(Func> callback, string strategy) => new(callback, strategy); - public MergeProp Merge(Func> callback, string[]? strategies) => new(callback, strategies); public DeepMergeProp DeepMerge(object? value) => new(value); public DeepMergeProp DeepMerge(Func callback) => new(callback); public DeepMergeProp DeepMerge(Func> callback) => new(callback); - public DeepMergeProp DeepMerge(object? value, string strategy) => new(value, strategy); - public DeepMergeProp DeepMerge(object? value, string[]? strategies) => new(value, strategies); - public DeepMergeProp DeepMerge(Func callback, string strategy) => new(callback, strategy); - public DeepMergeProp DeepMerge(Func callback, string[]? strategies) => new(callback, strategies); - public DeepMergeProp DeepMerge(Func> callback, string strategy) => new(callback, strategy); - public DeepMergeProp DeepMerge(Func> callback, string[]? strategies) => new(callback, strategies); public OptionalProp Optional(Func callback) => new(callback); public OptionalProp Optional(Func> callback) => new(callback); } diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs index bcd3616..481b626 100644 --- a/InertiaCore/Utils/Mergeable.cs +++ b/InertiaCore/Utils/Mergeable.cs @@ -3,7 +3,7 @@ namespace InertiaCore.Utils; public interface Mergeable { public bool merge { get; set; } - public string[]? mergeStrategies { get; set; } + public string[]? matchOn { get; set; } public Mergeable Merge() { @@ -12,6 +12,12 @@ public Mergeable Merge() return this; } + public Mergeable MatchesOn(params string[] keys) + { + matchOn = keys; + return this; + } + public bool ShouldMerge() => merge; - public string[]? GetMergeStrategies() => mergeStrategies; + public string[]? GetMatchOn() => matchOn; } diff --git a/InertiaCoreTests/UnitTestDeepMergeData.cs b/InertiaCoreTests/UnitTestDeepMergeData.cs index 53faf5f..0ae581f 100644 --- a/InertiaCoreTests/UnitTestDeepMergeData.cs +++ b/InertiaCoreTests/UnitTestDeepMergeData.cs @@ -1,4 +1,5 @@ using InertiaCore.Models; +using InertiaCore.Utils; using Microsoft.AspNetCore.Http; namespace InertiaCoreTests; @@ -241,14 +242,14 @@ public async Task TestDeepMergePropsWithPartialOnly() } [Test] - [Description("Test if deep merge props work with strategies.")] - public async Task TestDeepMergeWithStrategies() + [Description("Test if deep merge props work with match on keys.")] + public async Task TestDeepMergeWithMatchOn() { var response = _factory.Render("Test/Page", new { Test = "Test", - TestDeepMerge1 = _factory.DeepMerge("Deep Merge1", "deep"), - TestDeepMerge2 = _factory.DeepMerge(() => "Deep Merge2", new[] { "shallow", "replace" }), + TestDeepMerge1 = ((Mergeable)_factory.DeepMerge("Deep Merge1")).MatchesOn("deep"), + TestDeepMerge2 = ((Mergeable)_factory.DeepMerge(() => "Deep Merge2")).MatchesOn("shallow", "replace"), TestNormal = "Normal" }); @@ -269,8 +270,8 @@ public async Task TestDeepMergeWithStrategies() })); Assert.That(page?.DeepMergeProps, Is.EqualTo(new List { "testDeepMerge1", "testDeepMerge2" })); - // Deep merge props should also appear in merge strategies since they inherit from Mergeable - Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + // Deep merge props should also appear in match props on since they inherit from Mergeable + Assert.That(page?.MatchPropsOn, Is.EqualTo(new Dictionary { { "testDeepMerge1", new[] { "deep" } }, { "testDeepMerge2", new[] { "shallow", "replace" } } diff --git a/InertiaCoreTests/UnitTestMergeData.cs b/InertiaCoreTests/UnitTestMergeData.cs index 606fa2e..2aec9cd 100644 --- a/InertiaCoreTests/UnitTestMergeData.cs +++ b/InertiaCoreTests/UnitTestMergeData.cs @@ -1,4 +1,5 @@ using InertiaCore.Models; +using InertiaCore.Utils; using Microsoft.AspNetCore.Http; namespace InertiaCoreTests; @@ -319,14 +320,14 @@ public async Task TestMergePropsWithPartialOnlyAndExcept() } [Test] - [Description("Test if merge strategies are resolved properly for merge props.")] - public async Task TestMergeStrategies() + [Description("Test if match props on are resolved properly for merge props.")] + public async Task TestMatchPropsOn() { var response = _factory.Render("Test/Page", new { Test = "Test", - TestMerge1 = _factory.Merge("Merge1", "deep"), - TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), + TestMerge1 = ((Mergeable)_factory.Merge("Merge1")).MatchesOn("deep"), + TestMerge2 = ((Mergeable)_factory.Merge(() => "Merge2")).MatchesOn("shallow", "replace"), TestNormal = "Normal" }); @@ -347,7 +348,7 @@ public async Task TestMergeStrategies() })); Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge2" })); - Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + Assert.That(page?.MatchPropsOn, Is.EqualTo(new Dictionary { { "testMerge1", new[] { "deep" } }, { "testMerge2", new[] { "shallow", "replace" } } @@ -355,14 +356,14 @@ public async Task TestMergeStrategies() } [Test] - [Description("Test if merge strategies are handled properly with partial props.")] - public async Task TestMergeStrategiesWithPartialProps() + [Description("Test if match props on are handled properly with partial props.")] + public async Task TestMatchPropsOnWithPartialProps() { var response = _factory.Render("Test/Page", new { - TestMerge1 = _factory.Merge("Merge1", "deep"), - TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), - TestMerge3 = _factory.Merge("Merge3", "custom") + TestMerge1 = ((Mergeable)_factory.Merge("Merge1")).MatchesOn("deep"), + TestMerge2 = ((Mergeable)_factory.Merge(() => "Merge2")).MatchesOn("shallow", "replace"), + TestMerge3 = ((Mergeable)_factory.Merge("Merge3")).MatchesOn("custom") }); var headers = new HeaderDictionary @@ -386,7 +387,7 @@ public async Task TestMergeStrategiesWithPartialProps() })); Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge1", "testMerge3" })); - Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + Assert.That(page?.MatchPropsOn, Is.EqualTo(new Dictionary { { "testMerge1", new[] { "deep" } }, { "testMerge3", new[] { "custom" } } @@ -394,14 +395,14 @@ public async Task TestMergeStrategiesWithPartialProps() } [Test] - [Description("Test if merge strategies are excluded when using PARTIAL_EXCEPT header.")] - public async Task TestMergeStrategiesWithPartialExcept() + [Description("Test if match props on are excluded when using PARTIAL_EXCEPT header.")] + public async Task TestMatchPropsOnWithPartialExcept() { var response = _factory.Render("Test/Page", new { Test = "Test", - TestMerge1 = _factory.Merge("Merge1", "deep"), - TestMerge2 = _factory.Merge(() => "Merge2", new[] { "shallow", "replace" }), + TestMerge1 = ((Mergeable)_factory.Merge("Merge1")).MatchesOn("deep"), + TestMerge2 = ((Mergeable)_factory.Merge(() => "Merge2")).MatchesOn("shallow", "replace"), TestNormal = "Normal" }); @@ -427,15 +428,15 @@ public async Task TestMergeStrategiesWithPartialExcept() })); Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge2" })); - Assert.That(page?.MergeStrategies, Is.EqualTo(new Dictionary + Assert.That(page?.MatchPropsOn, Is.EqualTo(new Dictionary { { "testMerge2", new[] { "shallow", "replace" } } })); } [Test] - [Description("Test if merge strategies are null when no merge props have strategies.")] - public async Task TestNoMergeStrategies() + [Description("Test if match props on are null when no merge props have match keys.")] + public async Task TestNoMatchPropsOn() { var response = _factory.Render("Test/Page", new { @@ -460,7 +461,7 @@ public async Task TestNoMergeStrategies() })); Assert.That(page?.MergeProps, Is.EqualTo(new List { "testMerge" })); - Assert.That(page?.MergeStrategies, Is.EqualTo(null)); + Assert.That(page?.MatchPropsOn, Is.EqualTo(null)); } } From 6ab08f53deb6882ed532b6895717daeff657a2da Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 22 Sep 2025 02:39:00 -0400 Subject: [PATCH 32/33] deep merge on mergable interface --- InertiaCore/Utils/Mergeable.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs index 481b626..283241f 100644 --- a/InertiaCore/Utils/Mergeable.cs +++ b/InertiaCore/Utils/Mergeable.cs @@ -3,6 +3,7 @@ namespace InertiaCore.Utils; public interface Mergeable { public bool merge { get; set; } + public bool deepMerge { get; set; } public string[]? matchOn { get; set; } public Mergeable Merge() @@ -12,6 +13,13 @@ public Mergeable Merge() return this; } + public Mergeable DeepMerge() + { + deepMerge = true; + + return this->Merge(); + } + public Mergeable MatchesOn(params string[] keys) { matchOn = keys; @@ -19,5 +27,6 @@ public Mergeable MatchesOn(params string[] keys) } public bool ShouldMerge() => merge; + public bool ShouldDeepMerge() => deepMerge; public string[]? GetMatchOn() => matchOn; } From e1899d5c596996341056f42fb803a21b1227a806 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Mon, 22 Sep 2025 02:39:50 -0400 Subject: [PATCH 33/33] fix merge prop --- InertiaCore/Props/MergeProp.cs | 1 + InertiaCore/Utils/Mergeable.cs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/InertiaCore/Props/MergeProp.cs b/InertiaCore/Props/MergeProp.cs index f0d55f8..b957d34 100644 --- a/InertiaCore/Props/MergeProp.cs +++ b/InertiaCore/Props/MergeProp.cs @@ -5,6 +5,7 @@ namespace InertiaCore.Utils; public class MergeProp : InvokableProp, Mergeable { public bool merge { get; set; } = true; + public bool deepMerge { get; set; } = false; public string[]? matchOn { get; set; } public MergeProp(object? value) : base(value) diff --git a/InertiaCore/Utils/Mergeable.cs b/InertiaCore/Utils/Mergeable.cs index 283241f..8b982bf 100644 --- a/InertiaCore/Utils/Mergeable.cs +++ b/InertiaCore/Utils/Mergeable.cs @@ -17,7 +17,9 @@ public Mergeable DeepMerge() { deepMerge = true; - return this->Merge(); + merge = true; + + return this; } public Mergeable MatchesOn(params string[] keys)