From 11f8bb08712e0a7905086061c20f424dd1284067 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 16:24:08 -0400 Subject: [PATCH 1/3] added with function to response --- InertiaCore/Response.cs | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 7bdd2cb..337ff14 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -218,4 +218,74 @@ public Response WithViewData(IDictionary viewData) _viewData = viewData; return this; } + + /// + /// Add additional properties to the page. + /// + /// The property key, a dictionary of properties, or a ProvidesInertiaProperties object + /// The property value (only used when key is a string) + /// The Response instance for method chaining + public Response With(string key, object? value) + { + _props[key] = value; + return this; + } + + /// + /// Add additional properties to the page from a dictionary. + /// + /// Dictionary of properties to add + /// The Response instance for method chaining + public Response With(IDictionary properties) + { + foreach (var kvp in properties) + { + _props[kvp.Key] = kvp.Value; + } + return this; + } + + /// + /// Add additional properties to the page from a ProvidesInertiaProperties object. + /// + /// The property provider + /// The Response instance for method chaining + public Response With(ProvidesInertiaProperties provider) + { + // Generate a unique key for the provider + var providerKey = $"__provider_{Guid.NewGuid():N}"; + _props[providerKey] = provider; + return this; + } + + /// + /// Add additional properties to the page from an anonymous object. + /// + /// Anonymous object with properties to add + /// The Response instance for method chaining + public Response With(object properties) + { + if (properties == null) return this; + + if (properties is IDictionary dict) + { + return With(dict); + } + + if (properties is ProvidesInertiaProperties provider) + { + return With(provider); + } + + // Convert anonymous object to dictionary + var props = properties.GetType().GetProperties() + .ToDictionary(p => p.Name, p => p.GetValue(properties)); + + foreach (var kvp in props) + { + _props[kvp.Key] = kvp.Value; + } + + return this; + } } From e4ec34618dec8f8543998a1126a32966cc946830 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 16:24:51 -0400 Subject: [PATCH 2/3] [2.x] Introduce ProvidesInertiaProperties interface https://github.com/inertiajs/inertia-laravel/pull/769 --- InertiaCore/Response.cs | 27 +++ .../Utils/ProvidesInertiaProperties.cs | 16 ++ InertiaCore/Utils/RenderContext.cs | 30 +++ .../UnitTestInertiaPropertyProviders.cs | 139 +++++++++++ InertiaCoreTests/UnitTestResponseWith.cs | 221 ++++++++++++++++++ 5 files changed, 433 insertions(+) create mode 100644 InertiaCore/Utils/ProvidesInertiaProperties.cs create mode 100644 InertiaCore/Utils/RenderContext.cs create mode 100644 InertiaCoreTests/UnitTestInertiaPropertyProviders.cs create mode 100644 InertiaCoreTests/UnitTestResponseWith.cs diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 337ff14..5536d31 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -60,6 +60,7 @@ protected internal async Task ProcessResponse() var props = _props; props = ResolveSharedProps(props); + props = ResolveInertiaPropertyProviders(props); props = ResolvePartialProperties(props); props = ResolveAlways(props); props = await ResolvePropertyInstances(props); @@ -79,6 +80,32 @@ protected internal async Task ProcessResponse() return props; } + /// + /// Resolve properties from objects implementing ProvidesInertiaProperties. + /// + private Dictionary ResolveInertiaPropertyProviders(Dictionary props) + { + var context = new RenderContext(_component, _context!.HttpContext.Request); + + foreach (var pair in props.ToList()) + { + if (pair.Value is ProvidesInertiaProperties provider) + { + // Remove the provider object itself + props.Remove(pair.Key); + + // Add the properties it provides + var providedProps = provider.ToInertiaProperties(context); + foreach (var providedProp in providedProps) + { + props[providedProp.Key] = providedProp.Value; + } + } + } + + return props; + } + /// /// Resolve the `only` and `except` partial request props. /// diff --git a/InertiaCore/Utils/ProvidesInertiaProperties.cs b/InertiaCore/Utils/ProvidesInertiaProperties.cs new file mode 100644 index 0000000..2f29b59 --- /dev/null +++ b/InertiaCore/Utils/ProvidesInertiaProperties.cs @@ -0,0 +1,16 @@ +using System.Collections; + +namespace InertiaCore.Utils; + +/// +/// Interface for objects that can provide dynamic Inertia properties. +/// +public interface ProvidesInertiaProperties +{ + /// + /// Generates Inertia properties based on the current render context. + /// + /// The render context containing component name and request information + /// An enumerable of key-value pairs representing the properties + IEnumerable> ToInertiaProperties(RenderContext context); +} \ No newline at end of file diff --git a/InertiaCore/Utils/RenderContext.cs b/InertiaCore/Utils/RenderContext.cs new file mode 100644 index 0000000..6a9eeee --- /dev/null +++ b/InertiaCore/Utils/RenderContext.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Http; + +namespace InertiaCore.Utils; + +/// +/// Provides context information for Inertia property generation. +/// +public class RenderContext +{ + /// + /// The name of the component being rendered. + /// + public string Component { get; } + + /// + /// The current HTTP request. + /// + public HttpRequest Request { get; } + + /// + /// Initializes a new instance of the RenderContext class. + /// + /// The component name + /// The HTTP request + public RenderContext(string component, HttpRequest request) + { + Component = component; + Request = request; + } +} \ No newline at end of file diff --git a/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs b/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs new file mode 100644 index 0000000..e151195 --- /dev/null +++ b/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs @@ -0,0 +1,139 @@ +using InertiaCore.Models; +using InertiaCore.Utils; +using Microsoft.AspNetCore.Http; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test if ProvidesInertiaProperties interface works correctly.")] + public async Task TestInertiaPropertyProvider() + { + var provider = new TestPropertyProvider(); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + Provider = provider + }); + + 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" }, + { "user", "John Doe" }, + { "permissions", new List { "read", "write" } }, + { "component", "Test/Page" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if multiple ProvidesInertiaProperties objects work together.")] + public async Task TestMultipleInertiaPropertyProviders() + { + var userProvider = new UserPropertyProvider(); + var settingsProvider = new SettingsPropertyProvider(); + + var response = _factory.Render("Dashboard/Index", new + { + Test = "Test", + UserProvider = userProvider, + SettingsProvider = settingsProvider + }); + + 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" }, + { "user", "Alice" }, + { "role", "admin" }, + { "theme", "dark" }, + { "language", "en" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if ProvidesInertiaProperties receives correct RenderContext.")] + public async Task TestInertiaPropertyProviderContext() + { + var provider = new ContextAwarePropertyProvider(); + + var response = _factory.Render("User/Profile", new + { + Provider = provider + }); + + var headers = new HeaderDictionary + { + { "X-Custom-Header", "test-value" } + }; + + 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 + { + { "component", "User/Profile" }, + { "hasCustomHeader", true }, + { "errors", new Dictionary(0) } + })); + } +} + +// Test implementations of ProvidesInertiaProperties +internal class TestPropertyProvider : ProvidesInertiaProperties +{ + public IEnumerable> ToInertiaProperties(RenderContext context) + { + yield return new KeyValuePair("user", "John Doe"); + yield return new KeyValuePair("permissions", new List { "read", "write" }); + yield return new KeyValuePair("component", context.Component); + } +} + +internal class UserPropertyProvider : ProvidesInertiaProperties +{ + public IEnumerable> ToInertiaProperties(RenderContext context) + { + yield return new KeyValuePair("user", "Alice"); + yield return new KeyValuePair("role", "admin"); + } +} + +internal class SettingsPropertyProvider : ProvidesInertiaProperties +{ + public IEnumerable> ToInertiaProperties(RenderContext context) + { + yield return new KeyValuePair("theme", "dark"); + yield return new KeyValuePair("language", "en"); + } +} + +internal class ContextAwarePropertyProvider : ProvidesInertiaProperties +{ + public IEnumerable> ToInertiaProperties(RenderContext context) + { + yield return new KeyValuePair("component", context.Component); + yield return new KeyValuePair("hasCustomHeader", + context.Request.Headers.ContainsKey("X-Custom-Header")); + } +} \ No newline at end of file diff --git a/InertiaCoreTests/UnitTestResponseWith.cs b/InertiaCoreTests/UnitTestResponseWith.cs new file mode 100644 index 0000000..04440a3 --- /dev/null +++ b/InertiaCoreTests/UnitTestResponseWith.cs @@ -0,0 +1,221 @@ +using InertiaCore.Models; +using InertiaCore.Utils; + +namespace InertiaCoreTests; + +public partial class Tests +{ + [Test] + [Description("Test With method adding a single property.")] + public async Task TestResponseWithSingleProperty() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + response.With("Additional", "Property"); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "additional", "Property" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test With method adding multiple properties from dictionary.")] + public async Task TestResponseWithDictionary() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + var additionalProps = new Dictionary + { + { "Property1", "Value1" }, + { "Property2", 42 }, + { "Property3", true } + }; + + response.With(additionalProps); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "property1", "Value1" }, + { "property2", 42 }, + { "property3", true }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test With method adding properties from anonymous object.")] + public async Task TestResponseWithAnonymousObject() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + response.With(new + { + Property1 = "Value1", + Property2 = 42, + Property3 = true + }); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "property1", "Value1" }, + { "property2", 42 }, + { "property3", true }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test With method adding ProvidesInertiaProperties object.")] + public async Task TestResponseWithPropertyProvider() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + var provider = new TestPropertyProviderForWith(); + response.With(provider); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "providedUser", "Jane Doe" }, + { "providedRole", "admin" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test fluent chaining of With method.")] + public async Task TestResponseWithFluentChaining() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + response + .With("Property1", "Value1") + .With(new { Property2 = 42 }) + .With(new Dictionary { { "Property3", true } }); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "property1", "Value1" }, + { "property2", 42 }, + { "property3", true }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test With method overwriting existing property.")] + public async Task TestResponseWithOverwriteProperty() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value", + ToOverwrite = "OldValue" + }); + + response.With("ToOverwrite", "NewValue"); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "toOverwrite", "NewValue" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test With method combined with WithViewData.")] + public async Task TestResponseWithAndViewData() + { + var response = _factory.Render("Test/Page", new + { + Initial = "Value" + }); + + response + .With("AdditionalProp", "AdditionalValue") + .WithViewData(new Dictionary + { + { "ViewDataKey", "ViewDataValue" } + }); + + var context = PrepareContext(); + response.SetContext(context); + await response.ProcessResponse(); + + var page = response.GetJson().Value as Page; + + Assert.That(page?.Props, Is.EqualTo(new Dictionary + { + { "initial", "Value" }, + { "additionalProp", "AdditionalValue" }, + { "errors", new Dictionary(0) } + })); + } +} + +// Test property provider for With method tests +internal class TestPropertyProviderForWith : ProvidesInertiaProperties +{ + public IEnumerable> ToInertiaProperties(RenderContext context) + { + yield return new KeyValuePair("providedUser", "Jane Doe"); + yield return new KeyValuePair("providedRole", "admin"); + } +} \ No newline at end of file From 239f5abc4c767d20a5e42ba7b00928f6686da3c5 Mon Sep 17 00:00:00 2001 From: Austin Drummond Date: Sat, 20 Sep 2025 16:32:22 -0400 Subject: [PATCH 3/3] [2.x] Introduce ProvidesInertiaProp interface https://github.com/inertiajs/inertia-laravel/pull/746 --- InertiaCore/Response.cs | 8 +- InertiaCore/Utils/PropertyContext.cs | 37 ++++++ InertiaCore/Utils/ProvidesInertiaProperty.cs | 14 ++ .../UnitTestInertiaPropertyProviders.cs | 122 ++++++++++++++++++ 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 InertiaCore/Utils/PropertyContext.cs create mode 100644 InertiaCore/Utils/ProvidesInertiaProperty.cs diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 5536d31..2082a03 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -4,6 +4,7 @@ using InertiaCore.Models; using InertiaCore.Props; using InertiaCore.Utils; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ViewFeatures; @@ -63,7 +64,7 @@ protected internal async Task ProcessResponse() props = ResolveInertiaPropertyProviders(props); props = ResolvePartialProperties(props); props = ResolveAlways(props); - props = await ResolvePropertyInstances(props); + props = await ResolvePropertyInstances(props, _context!.HttpContext.Request); return props; } @@ -174,7 +175,7 @@ protected internal async Task ProcessResponse() /// /// Resolve all necessary class instances in the given props. /// - private static async Task> ResolvePropertyInstances(Dictionary props) + private static async Task> ResolvePropertyInstances(Dictionary props, HttpRequest request) { return (await Task.WhenAll(props.Select(async pair => { @@ -185,12 +186,13 @@ protected internal async Task ProcessResponse() Func f => (key, await f.ResolveAsync()), Task t => (key, await t.ResolveResult()), InvokableProp p => (key, await p.Invoke()), + ProvidesInertiaProperty pip => (key, pip.ToInertiaProperty(new PropertyContext(key, props, request))), _ => (key, pair.Value) }; if (value.Item2 is Dictionary dict) { - value = (key, await ResolvePropertyInstances(dict)); + value = (key, await ResolvePropertyInstances(dict, request)); } return value; diff --git a/InertiaCore/Utils/PropertyContext.cs b/InertiaCore/Utils/PropertyContext.cs new file mode 100644 index 0000000..6c0acfd --- /dev/null +++ b/InertiaCore/Utils/PropertyContext.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Http; + +namespace InertiaCore.Utils; + +/// +/// Provides context information for individual property transformation. +/// +public class PropertyContext +{ + /// + /// The key of the property being transformed. + /// + public string Key { get; } + + /// + /// All properties in the response. + /// + public Dictionary Props { get; } + + /// + /// The current HTTP request. + /// + public HttpRequest Request { get; } + + /// + /// Initializes a new instance of the PropertyContext class. + /// + /// The property key + /// All properties + /// The HTTP request + public PropertyContext(string key, Dictionary props, HttpRequest request) + { + Key = key; + Props = props; + Request = request; + } +} \ No newline at end of file diff --git a/InertiaCore/Utils/ProvidesInertiaProperty.cs b/InertiaCore/Utils/ProvidesInertiaProperty.cs new file mode 100644 index 0000000..2911625 --- /dev/null +++ b/InertiaCore/Utils/ProvidesInertiaProperty.cs @@ -0,0 +1,14 @@ +namespace InertiaCore.Utils; + +/// +/// Interface for objects that can transform themselves into an Inertia property value. +/// +public interface ProvidesInertiaProperty +{ + /// + /// Transforms the object into an Inertia property value based on the given context. + /// + /// The property context containing key, props, and request information + /// The transformed property value + object? ToInertiaProperty(PropertyContext context); +} \ No newline at end of file diff --git a/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs b/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs index e151195..23cfb95 100644 --- a/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs +++ b/InertiaCoreTests/UnitTestInertiaPropertyProviders.cs @@ -97,6 +97,95 @@ public async Task TestInertiaPropertyProviderContext() { "errors", new Dictionary(0) } })); } + + [Test] + [Description("Test if ProvidesInertiaProperty interface works correctly.")] + public async Task TestSingleInertiaPropertyProvider() + { + var provider = new TestInertiaPropertyProvider(); + + var response = _factory.Render("Test/Page", new + { + Test = "Test", + PropertyProvider = provider + }); + + 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" }, + { "propertyProvider", "Transformed: " }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if ProvidesInertiaProperty receives correct PropertyContext.")] + public async Task TestSingleInertiaPropertyProviderWithContext() + { + var provider = new ContextAwareInertiaPropertyProvider(); + + var response = _factory.Render("User/Profile", new + { + InitialProp = "initial", + ContextProvider = provider + }); + + var headers = new HeaderDictionary + { + { "Authorization", "Bearer token123" } + }; + + 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 + { + { "initialProp", "initial" }, + { "contextProvider", "contextProvider::True" }, + { "errors", new Dictionary(0) } + })); + } + + [Test] + [Description("Test if multiple ProvidesInertiaProperty objects work correctly.")] + public async Task TestMultipleSingleInertiaPropertyProviders() + { + var provider1 = new SimpleInertiaPropertyProvider("Value1"); + var provider2 = new SimpleInertiaPropertyProvider("Value2"); + + var response = _factory.Render("Dashboard/Index", new + { + Test = "Test", + Provider1 = provider1, + Provider2 = provider2 + }); + + 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" }, + { "provider1", "Processed: Value1" }, + { "provider2", "Processed: Value2" }, + { "errors", new Dictionary(0) } + })); + } } // Test implementations of ProvidesInertiaProperties @@ -136,4 +225,37 @@ internal class ContextAwarePropertyProvider : ProvidesInertiaProperties yield return new KeyValuePair("hasCustomHeader", context.Request.Headers.ContainsKey("X-Custom-Header")); } +} + +// Test implementations of ProvidesInertiaProperty +internal class TestInertiaPropertyProvider : ProvidesInertiaProperty +{ + public object? ToInertiaProperty(PropertyContext context) + { + return $"Transformed: {context.Request.Path}"; + } +} + +internal class ContextAwareInertiaPropertyProvider : ProvidesInertiaProperty +{ + public object? ToInertiaProperty(PropertyContext context) + { + var hasAuth = context.Request.Headers.ContainsKey("Authorization"); + return $"{context.Key}:{context.Request.Path}:{hasAuth}"; + } +} + +internal class SimpleInertiaPropertyProvider : ProvidesInertiaProperty +{ + private readonly string _value; + + public SimpleInertiaPropertyProvider(string value) + { + _value = value; + } + + public object? ToInertiaProperty(PropertyContext context) + { + return $"Processed: {_value}"; + } } \ No newline at end of file