Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1dc8b6b
added header constants
adrum Dec 14, 2024
3a33449
refactor resolve props
adrum Dec 14, 2024
c456148
added always prop
adrum Dec 14, 2024
7d0d3bf
fix some compile time warnings
adrum Dec 14, 2024
f9ad84b
added always prop test
adrum Dec 15, 2024
77473d6
added async task wrapper
adrum Dec 15, 2024
f64ca76
.net 8
adrum Dec 15, 2024
51fe030
restore header keys
adrum Dec 21, 2024
62a0864
added .net 9
adrum Dec 21, 2024
97b7c67
Merge branch 'fix/warnings' into feature/optional-prop
adrum Dec 21, 2024
2ab0765
added ignore first load
adrum Dec 21, 2024
bf08e03
add optional prop
adrum Dec 21, 2024
3e30450
added optional test
adrum Dec 21, 2024
1d74592
added merge prop
adrum Dec 21, 2024
05bf42d
added unit test for merge prop
adrum Dec 21, 2024
3cacdaa
update version in actions
adrum Dec 21, 2024
52ad4b8
update version in actions
adrum Dec 21, 2024
1e18696
dont resolve Lazy and Optional Props until they are invoked
adrum Dec 21, 2024
7860eea
dont include Merge props in the json
adrum Dec 21, 2024
21a8f65
fix formatting
adrum Dec 21, 2024
bd5b6c1
Merge branch 'main' into fork/adrum/feature/optional-prop
kapi2289 Jan 6, 2025
d71be3b
Merge branch 'main' into fork/adrum/feature/merge-prop
kapi2289 Jan 6, 2025
66312a3
Merge branch 'main' into fork/adrum/feature/optional-prop
kapi2289 Jan 6, 2025
cf2c45e
Minor fixes and changes
kapi2289 Jan 6, 2025
42396a7
Add missing Inertia static methods
kapi2289 Jan 6, 2025
6b383a3
Merge branch 'main' into feature/optional-prop
adrum Jan 11, 2025
ea4592e
make optional prop invokable
adrum Jan 11, 2025
f62c188
Merge branch 'main' into feature/merge-prop
adrum Jan 11, 2025
1e949b0
fix merge prop tests
adrum Jan 11, 2025
ad8c476
fix test sdks?
adrum Jan 11, 2025
6333daa
fix test sdks?
adrum Jan 11, 2025
a6a9fe0
Merge branch 'v1' into feature/optional-prop
adrum Feb 8, 2025
2ae0bff
revert formatting
adrum Feb 8, 2025
0e9353c
one more formatting fix
adrum Feb 8, 2025
eb076cc
Merge branch 'feature/optional-prop' into feature/merge-prop
adrum Feb 8, 2025
28afc55
Merge branch 'main' into fork/adrum/feature/merge-prop
kapi2289 Feb 23, 2025
d842ac8
[2.x] Keep only partial data in mergeProps
adrum Sep 20, 2025
58913c9
[2.x] Allow deepMerge on custom properties
adrum Sep 20, 2025
0d393c0
Add Inertia::deepMerge Method for Handling Complex Data Merges in Res…
adrum Sep 22, 2025
a82297a
[2.x] Refactor mergeStrategies argument to matchOn() method
adrum Sep 22, 2025
6ab08f5
deep merge on mergable interface
adrum Sep 22, 2025
e1899d5
fix merge prop
adrum Sep 22, 2025
b0c795a
Refactor merge resolution to match Laravel adapter
adrum Apr 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions InertiaCore/Inertia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ public static class Inertia
public static LazyProp Lazy(Func<object?> callback) => _factory.Lazy(callback);

public static LazyProp Lazy(Func<Task<object?>> callback) => _factory.Lazy(callback);

public static OptionalProp Optional(Func<object?> callback) => _factory.Optional(callback);

public static OptionalProp Optional(Func<Task<object?>> callback) => _factory.Optional(callback);

public static MergeProp Merge(object? value) => _factory.Merge(value);

public static MergeProp Merge(Func<object?> callback) => _factory.Merge(callback);

public static MergeProp Merge(Func<Task<object?>> callback) => _factory.Merge(callback);

public static DeepMergeProp DeepMerge(object? value) => _factory.DeepMerge(value);

public static DeepMergeProp DeepMerge(Func<object?> callback) => _factory.DeepMerge(callback);

public static DeepMergeProp DeepMerge(Func<Task<object?>> callback) => _factory.DeepMerge(callback);
}
14 changes: 14 additions & 0 deletions InertiaCore/Models/Page.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System.Text.Json.Serialization;

namespace InertiaCore.Models;

internal class Page
Expand All @@ -8,4 +10,16 @@ internal class Page
public string Url { get; set; } = default!;
public bool EncryptHistory { get; set; } = false;
public bool ClearHistory { get; set; } = false;

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? MergeProps { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? MatchPropsOn { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? PrependProps { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? DeepMergeProps { get; set; }
}
33 changes: 33 additions & 0 deletions InertiaCore/Props/DeepMergeProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using InertiaCore.Utils;

namespace InertiaCore.Props;

public class DeepMergeProp : InvokableProp, Mergeable
{
public bool merge { get; set; } = true;
public string[]? matchOn { get; set; }
public bool deepMerge { get; set; } = true;
public bool Append { get; set; } = true;
public List<string> AppendsAtPaths { get; } = new();
public List<string> PrependsAtPaths { get; } = new();

public DeepMergeProp(object? value) : base(value)
{
merge = true;
deepMerge = true;
}

internal DeepMergeProp(Func<object?> value) : base(value)
{
merge = true;
deepMerge = true;
}

internal DeepMergeProp(Func<Task<object?>> value) : base(value)
{
merge = true;
deepMerge = true;
}

public bool ShouldDeepMerge() => deepMerge;
}
4 changes: 3 additions & 1 deletion InertiaCore/Props/LazyProp.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using InertiaCore.Utils;

namespace InertiaCore.Props;

public class LazyProp : InvokableProp
public class LazyProp : InvokableProp, IIgnoresFirstLoad
{
internal LazyProp(Func<object?> value) : base(value)
{
Expand Down
31 changes: 31 additions & 0 deletions InertiaCore/Props/MergeProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using InertiaCore.Props;

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 bool Append { get; set; } = true;
public List<string> AppendsAtPaths { get; } = new();
public List<string> PrependsAtPaths { get; } = new();

public MergeProp(object? value) : base(value)
{
merge = true;
}

internal MergeProp(Func<object?> value) : base(value)
{
merge = true;
}

internal MergeProp(Func<Task<object?>> value) : base(value)
{
merge = true;
}
}


14 changes: 14 additions & 0 deletions InertiaCore/Props/OptionalProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using InertiaCore.Utils;

namespace InertiaCore.Props;

public class OptionalProp : InvokableProp, IIgnoresFirstLoad
{
internal OptionalProp(Func<object?> value) : base(value)
{
}

internal OptionalProp(Func<Task<object?>> value) : base(value)
{
}
}
163 changes: 162 additions & 1 deletion InertiaCore/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ protected internal async Task ProcessResponse()
ClearHistory = _clearHistory,
};

var mergeable = GetMergeablePropsForRequest();
page.MergeProps = ResolveMergeProps(mergeable);
page.PrependProps = ResolvePrependProps(mergeable);
page.DeepMergeProps = ResolveDeepMergeProps(mergeable);
page.MatchPropsOn = ResolveMatchPropsOn(mergeable);
page.Props["errors"] = GetErrors();

SetPage(page);
Expand Down Expand Up @@ -88,7 +93,7 @@ protected internal async Task ProcessResponse()

if (!isPartial)
return props
.Where(kv => kv.Value is not LazyProp)
.Where(kv => kv.Value is not IIgnoresFirstLoad)
.ToDictionary(kv => kv.Key, kv => kv.Value);

props = props.ToDictionary(kv => kv.Key, kv => kv.Value);
Expand Down Expand Up @@ -144,6 +149,162 @@ protected internal async Task ProcessResponse()
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Get the props eligible for merging on this request.
/// Mirrors Laravel's Response::getMergePropsForRequest():
/// filters _props to only Mergeable && ShouldMerge(), removes any keys
/// listed in the X-Inertia-Reset header, then applies Partial-Only /
/// Partial-Except filtering.
/// </summary>
private Dictionary<string, object?> GetMergeablePropsForRequest(bool rejectResetProps = true)
{
var headers = _context!.HttpContext.Request.Headers;

var resetKeys = rejectResetProps
? ParseHeaderList(headers[InertiaHeader.Reset].ToString())
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var hasPartialOnly = headers.ContainsKey(InertiaHeader.PartialOnly);
var onlyKeys = hasPartialOnly
? ParseHeaderList(headers[InertiaHeader.PartialOnly].ToString())
: new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var exceptKeys = ParseHeaderList(headers[InertiaHeader.PartialExcept].ToString());

var result = new Dictionary<string, object?>();
foreach (var kv in _props)
{
if (kv.Value is not Mergeable m || !m.ShouldMerge()) continue;

var camel = kv.Key.ToCamelCase();

if (resetKeys.Contains(camel)) continue;
if (hasPartialOnly && !onlyKeys.Contains(camel)) continue;
if (exceptKeys.Contains(camel)) continue;

result[kv.Key] = kv.Value;
}

return result;
}

private static HashSet<string> ParseHeaderList(string? value)
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrEmpty(value)) return set;

foreach (var part in value!.Split(','))
{
var trimmed = part.Trim();
if (trimmed.Length > 0) set.Add(trimmed);
}
return set;
}

/// <summary>
/// Resolve merge props that should be appended (excludes deep merge and prepend props).
/// Returns a flat list of prop keys or key.path entries.
/// </summary>
private static List<string>? ResolveMergeProps(Dictionary<string, object?> mergeProps)
{
var mergeableProps = mergeProps
.Where(kv => kv.Value is Mergeable m && !m.ShouldDeepMerge())
.ToList();

if (mergeableProps.Count == 0) return null;

var result = new List<string>();

foreach (var kv in mergeableProps)
{
var m = (Mergeable)kv.Value!;
var key = kv.Key.ToCamelCase();

if (m.AppendsAtRoot())
{
result.Add(key);
}

foreach (var path in m.AppendsAtPaths)
{
result.Add($"{key}.{path}");
}
}

return result.Count > 0 ? result : null;
}

/// <summary>
/// Resolve props that should be prepended during merging.
/// Returns a flat list of prop keys or key.path entries.
/// </summary>
private static List<string>? ResolvePrependProps(Dictionary<string, object?> mergeProps)
{
var mergeableProps = mergeProps
.Where(kv => kv.Value is Mergeable m && !m.ShouldDeepMerge())
.ToList();

if (mergeableProps.Count == 0) return null;

var result = new List<string>();

foreach (var kv in mergeableProps)
{
var m = (Mergeable)kv.Value!;
var key = kv.Key.ToCamelCase();

if (m.PrependsAtRoot())
{
result.Add(key);
}

foreach (var path in m.PrependsAtPaths)
{
result.Add($"{key}.{path}");
}
}

return result.Count > 0 ? result : null;
}

/// <summary>
/// Resolve props that should be deep merged.
/// </summary>
private static List<string>? ResolveDeepMergeProps(Dictionary<string, object?> mergeProps)
{
var deepMergeProps = mergeProps
.Where(kv => kv.Value is Mergeable m && m.ShouldDeepMerge())
.Select(kv => kv.Key.ToCamelCase())
.ToList();

return deepMergeProps.Count > 0 ? deepMergeProps : null;
}

/// <summary>
/// Resolve the match-on keys for merge props as a flat list.
/// Returns entries like "propKey.strategy" matching Laravel's format.
/// </summary>
private static List<string>? ResolveMatchPropsOn(Dictionary<string, object?> mergeProps)
{
var result = new List<string>();

foreach (var kv in mergeProps)
{
if (kv.Value is not Mergeable m) continue;

var matchOnKeys = m.GetMatchOn();
if (matchOnKeys == null || matchOnKeys.Length == 0) continue;

var key = kv.Key.ToCamelCase();
foreach (var matchOnItem in matchOnKeys)
{
result.Add($"{key}.{matchOnItem}");
}
}

return result.Count > 0 ? result : null;
}

/// <summary>
/// Resolve all necessary class instances in the given props.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ internal interface IResponseFactory
public AlwaysProp Always(Func<Task<object?>> callback);
public LazyProp Lazy(Func<object?> callback);
public LazyProp Lazy(Func<Task<object?>> callback);
public MergeProp Merge(object? value);
public MergeProp Merge(Func<object?> callback);
public MergeProp Merge(Func<Task<object?>> callback);
public DeepMergeProp DeepMerge(object? value);
public DeepMergeProp DeepMerge(Func<object?> callback);
public DeepMergeProp DeepMerge(Func<Task<object?>> callback);
public OptionalProp Optional(Func<object?> callback);
public OptionalProp Optional(Func<Task<object?>> callback);
}

internal class ResponseFactory : IResponseFactory
Expand Down Expand Up @@ -144,4 +152,12 @@ public void Share(IDictionary<string, object?> data)
public AlwaysProp Always(object? value) => new(value);
public AlwaysProp Always(Func<object?> callback) => new(callback);
public AlwaysProp Always(Func<Task<object?>> callback) => new(callback);
public MergeProp Merge(object? value) => new(value);
public MergeProp Merge(Func<object?> callback) => new(callback);
public MergeProp Merge(Func<Task<object?>> callback) => new(callback);
public DeepMergeProp DeepMerge(object? value) => new(value);
public DeepMergeProp DeepMerge(Func<object?> callback) => new(callback);
public DeepMergeProp DeepMerge(Func<Task<object?>> callback) => new(callback);
public OptionalProp Optional(Func<object?> callback) => new(callback);
public OptionalProp Optional(Func<Task<object?>> callback) => new(callback);
}
5 changes: 5 additions & 0 deletions InertiaCore/Utils/IIgnoresFirstLoad.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace InertiaCore.Utils;

public interface IIgnoresFirstLoad
{
}
2 changes: 2 additions & 0 deletions InertiaCore/Utils/InertiaHeader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public static class InertiaHeader
public const string PartialOnly = "X-Inertia-Partial-Data";

public const string PartialExcept = "X-Inertia-Partial-Except";

public const string Reset = "X-Inertia-Reset";
}
Loading
Loading