Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
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);
}
11 changes: 11 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,13 @@ 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 Dictionary<string, string[]>? MatchPropsOn { get; set; }

[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<string>? DeepMergeProps { get; set; }
}
30 changes: 30 additions & 0 deletions InertiaCore/Props/DeepMergeProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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 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
27 changes: 27 additions & 0 deletions InertiaCore/Props/MergeProp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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 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)
{
}
}
166 changes: 165 additions & 1 deletion InertiaCore/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ protected internal async Task ProcessResponse()
ClearHistory = _clearHistory,
};

page.MergeProps = ResolveMergeProps(props);
page.MatchPropsOn = ResolveMatchPropsOn(props);
page.DeepMergeProps = ResolveDeepMergeProps(props);
page.Props["errors"] = GetErrors();

SetPage(page);
Expand Down Expand Up @@ -88,7 +91,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 +147,167 @@ protected internal async Task ProcessResponse()
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve `merge` properties that should be appended to the existing values by the front-end.
/// </summary>
private List<string>? ResolveMergeProps(Dictionary<string, object?> props)
{
// Parse the "RESET" header into a collection of keys to reset
var resetProps = new HashSet<string>(
_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<string>(
_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 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();

if (mergeProps.Count == 0)
{
return null;
}

// Return the result
return mergeProps;
}

/// <summary>
/// Resolve match props on for properties that should be matched on specific keys.
/// </summary>
private Dictionary<string, string[]>? ResolveMatchPropsOn(Dictionary<string, object?> props)
{
// Parse the "RESET" header into a collection of keys to reset
var resetProps = new HashSet<string>(
_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<string>(
_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 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!).GetMatchOn()!
);

if (matchPropsOn.Count == 0)
{
return null;
}

// Return the result
return matchPropsOn;
}

/// <summary>
/// Resolve deep merge properties that should be deeply merged with existing values by the front-end.
/// </summary>
private List<string>? ResolveDeepMergeProps(Dictionary<string, object?> props)
{
// Parse the "RESET" header into a collection of keys to reset
var resetProps = new HashSet<string>(
_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<string>(
_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;
}

/// <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";
}
34 changes: 34 additions & 0 deletions InertiaCore/Utils/Mergeable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace InertiaCore.Utils;

public interface Mergeable
{
public bool merge { get; set; }
public bool deepMerge { get; set; }
public string[]? matchOn { get; set; }

public Mergeable Merge()
{
merge = true;

return this;
}

public Mergeable DeepMerge()
{
deepMerge = true;

merge = true;

return this;
}

public Mergeable MatchesOn(params string[] keys)
{
matchOn = keys;
return this;
}

public bool ShouldMerge() => merge;
public bool ShouldDeepMerge() => deepMerge;
public string[]? GetMatchOn() => matchOn;
}
Loading