Skip to content

Commit cfc5968

Browse files
authored
[1.x] Properly resolve nested properties (#32)
* Resolve nested dictionaries * Revert changing the shared props mechanic, rename shared data to props * Fix a typo * Update dotnet.yml
1 parent fb027eb commit cfc5968

11 files changed

+236
-145
lines changed

.github/workflows/dotnet.yml

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ jobs:
2323
- name: Setup .NET
2424
uses: actions/setup-dotnet@v3
2525
with:
26-
dotnet-version: 9.0.x
26+
dotnet-version: |
27+
6.0.x
28+
7.0.x
29+
8.0.x
30+
9.0.x
2731
- name: Restore dependencies
2832
run: dotnet restore
2933
- name: Build

InertiaCore/Extensions/InertiaExtensions.cs

+19-25
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,6 @@ namespace InertiaCore.Extensions;
88

99
internal static class InertiaExtensions
1010
{
11-
internal static Dictionary<string, object?> OnlyProps(this ActionContext context, Dictionary<string, object?> props)
12-
{
13-
var onlyKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
14-
.ToString().Split(',')
15-
.Select(k => k.Trim())
16-
.Where(k => !string.IsNullOrEmpty(k))
17-
.ToList();
18-
19-
return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
20-
.ToDictionary(kv => kv.Key, kv => kv.Value);
21-
}
22-
23-
internal static Dictionary<string, object?> ExceptProps(this ActionContext context,
24-
Dictionary<string, object?> props)
25-
{
26-
var exceptKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
27-
.ToString().Split(',')
28-
.Select(k => k.Trim())
29-
.Where(k => !string.IsNullOrEmpty(k))
30-
.ToList();
31-
32-
return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
33-
.ToDictionary(kv => kv.Key, kv => kv.Value);
34-
}
35-
3611
internal static bool IsInertiaPartialComponent(this ActionContext context, string component) =>
3712
context.HttpContext.Request.Headers[InertiaHeader.PartialComponent] == component;
3813

@@ -55,4 +30,23 @@ internal static bool Override<TKey, TValue>(this IDictionary<TKey, TValue> dicti
5530

5631
return true;
5732
}
33+
34+
internal static Task<object?> ResolveAsync(this Func<object?> func)
35+
{
36+
var rt = func.Method.ReturnType;
37+
38+
if (!rt.IsGenericType || rt.GetGenericTypeDefinition() != typeof(Task<>))
39+
return Task.Run(func.Invoke);
40+
41+
var task = func.DynamicInvoke() as Task;
42+
return task!.ResolveResult();
43+
}
44+
45+
internal static async Task<object?> ResolveResult(this Task task)
46+
{
47+
await task.ConfigureAwait(false);
48+
var result = task.GetType().GetProperty("Result");
49+
50+
return result?.GetValue(task);
51+
}
5852
}

InertiaCore/Props/InvokableProp.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using InertiaCore.Extensions;
2+
13
namespace InertiaCore.Props;
24

35
public class InvokableProp
@@ -10,9 +12,9 @@ public class InvokableProp
1012
{
1113
return _value switch
1214
{
13-
Func<Task<object?>> asyncCallable => asyncCallable.Invoke(),
14-
Func<object?> callable => Task.Run(() => callable.Invoke()),
15-
Task<object?> value => value,
15+
Func<object?> f => f.ResolveAsync(),
16+
Task t => t.ResolveResult(),
17+
InvokableProp p => p.Invoke(),
1618
_ => Task.FromResult(_value)
1719
};
1820
}

InertiaCore/Response.cs

+120-61
Original file line numberDiff line numberDiff line change
@@ -13,54 +13,157 @@ namespace InertiaCore;
1313
public class Response : IActionResult
1414
{
1515
private readonly string _component;
16-
private readonly object _props;
16+
private readonly Dictionary<string, object?> _props;
1717
private readonly string _rootView;
1818
private readonly string? _version;
1919

2020
private ActionContext? _context;
2121
private Page? _page;
2222
private IDictionary<string, object>? _viewData;
2323

24-
public Response(string component, object props, string rootView, string? version)
24+
internal Response(string component, Dictionary<string, object?> props, string rootView, string? version)
2525
=> (_component, _props, _rootView, _version) = (component, props, rootView, version);
2626

2727
public async Task ExecuteResultAsync(ActionContext context)
2828
{
2929
SetContext(context);
3030
await ProcessResponse();
31-
3231
await GetResult().ExecuteResultAsync(_context!);
3332
}
3433

3534
protected internal async Task ProcessResponse()
3635
{
36+
var props = await ResolveProperties();
37+
3738
var page = new Page
3839
{
3940
Component = _component,
4041
Version = _version,
4142
Url = _context!.RequestedUri(),
42-
Props = await ResolveProperties(_props.GetType().GetProperties()
43-
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)))
43+
Props = props
4444
};
4545

46-
var shared = _context!.HttpContext.Features.Get<InertiaSharedData>();
47-
if (shared != null)
48-
page.Props = shared.GetMerged(page.Props);
49-
5046
page.Props["errors"] = GetErrors();
5147

5248
SetPage(page);
5349
}
5450

55-
private static async Task<Dictionary<string, object?>> PrepareProps(Dictionary<string, object?> props)
51+
/// <summary>
52+
/// Resolve the properties for the response.
53+
/// </summary>
54+
private async Task<Dictionary<string, object?>> ResolveProperties()
55+
{
56+
var props = _props;
57+
58+
props = ResolveSharedProps(props);
59+
props = ResolvePartialProperties(props);
60+
props = ResolveAlways(props);
61+
props = await ResolvePropertyInstances(props);
62+
63+
return props;
64+
}
65+
66+
/// <summary>
67+
/// Resolve `shared` props stored in the current request context.
68+
/// </summary>
69+
private Dictionary<string, object?> ResolveSharedProps(Dictionary<string, object?> props)
70+
{
71+
var shared = _context!.HttpContext.Features.Get<InertiaSharedProps>();
72+
if (shared != null)
73+
props = shared.GetMerged(props);
74+
75+
return props;
76+
}
77+
78+
/// <summary>
79+
/// Resolve the `only` and `except` partial request props.
80+
/// </summary>
81+
private Dictionary<string, object?> ResolvePartialProperties(Dictionary<string, object?> props)
82+
{
83+
var isPartial = _context!.IsInertiaPartialComponent(_component);
84+
85+
if (!isPartial)
86+
return props
87+
.Where(kv => kv.Value is not LazyProp)
88+
.ToDictionary(kv => kv.Key, kv => kv.Value);
89+
90+
props = props.ToDictionary(kv => kv.Key, kv => kv.Value);
91+
92+
if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
93+
props = ResolveOnly(props);
94+
95+
if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
96+
props = ResolveExcept(props);
97+
98+
return props;
99+
}
100+
101+
/// <summary>
102+
/// Resolve the `only` partial request props.
103+
/// </summary>
104+
private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
105+
{
106+
var onlyKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
107+
.ToString().Split(',')
108+
.Select(k => k.Trim())
109+
.Where(k => !string.IsNullOrEmpty(k))
110+
.ToList();
111+
112+
return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
113+
.ToDictionary(kv => kv.Key, kv => kv.Value);
114+
}
115+
116+
/// <summary>
117+
/// Resolve the `except` partial request props.
118+
/// </summary>
119+
private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
120+
{
121+
var exceptKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
122+
.ToString().Split(',')
123+
.Select(k => k.Trim())
124+
.Where(k => !string.IsNullOrEmpty(k))
125+
.ToList();
126+
127+
return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
128+
.ToDictionary(kv => kv.Key, kv => kv.Value);
129+
}
130+
131+
/// <summary>
132+
/// Resolve `always` properties that should always be included on all visits, regardless of "only" or "except" requests.
133+
/// </summary>
134+
private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> props)
135+
{
136+
var alwaysProps = _props.Where(o => o.Value is AlwaysProp);
137+
138+
return props
139+
.Where(kv => kv.Value is not AlwaysProp)
140+
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
141+
}
142+
143+
/// <summary>
144+
/// Resolve all necessary class instances in the given props.
145+
/// </summary>
146+
private static async Task<Dictionary<string, object?>> ResolvePropertyInstances(Dictionary<string, object?> props)
56147
{
57-
return (await Task.WhenAll(props.Select(async pair => pair.Value switch
148+
return (await Task.WhenAll(props.Select(async pair =>
58149
{
59-
Func<object?> f => (pair.Key, f.Invoke()),
60-
LazyProp l => (pair.Key, await l.Invoke()),
61-
AlwaysProp l => (pair.Key, await l.Invoke()),
62-
_ => (pair.Key, pair.Value)
63-
}))).ToDictionary(pair => pair.Key, pair => pair.Item2);
150+
var key = pair.Key.ToCamelCase();
151+
152+
var value = pair.Value switch
153+
{
154+
Func<object?> f => (key, await f.ResolveAsync()),
155+
Task t => (key, await t.ResolveResult()),
156+
InvokableProp p => (key, await p.Invoke()),
157+
_ => (key, pair.Value)
158+
};
159+
160+
if (value.Item2 is Dictionary<string, object?> dict)
161+
{
162+
value = (key, await ResolvePropertyInstances(dict));
163+
}
164+
165+
return value;
166+
}))).ToDictionary(pair => pair.key, pair => pair.Item2);
64167
}
65168

66169
protected internal JsonResult GetJson()
@@ -93,7 +196,7 @@ private ViewResult GetView()
93196

94197
protected internal IActionResult GetResult() => _context!.IsInertiaRequest() ? GetJson() : GetView();
95198

96-
private IDictionary<string, string> GetErrors()
199+
private Dictionary<string, string> GetErrors()
97200
{
98201
if (!_context!.ModelState.IsValid)
99202
return _context!.ModelState.ToDictionary(o => o.Key.ToCamelCase(),
@@ -111,48 +214,4 @@ public Response WithViewData(IDictionary<string, object> viewData)
111214
_viewData = viewData;
112215
return this;
113216
}
114-
115-
private async Task<Dictionary<string, object?>> ResolveProperties(Dictionary<string, object?> props)
116-
{
117-
var isPartial = _context!.IsInertiaPartialComponent(_component);
118-
119-
if (!isPartial)
120-
{
121-
props = props
122-
.Where(kv => kv.Value is not LazyProp)
123-
.ToDictionary(kv => kv.Key, kv => kv.Value);
124-
}
125-
else
126-
{
127-
props = props.ToDictionary(kv => kv.Key, kv => kv.Value);
128-
129-
if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
130-
props = ResolveOnly(props);
131-
132-
if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
133-
props = ResolveExcept(props);
134-
}
135-
136-
props = ResolveAlways(props);
137-
props = await PrepareProps(props);
138-
139-
return props;
140-
}
141-
142-
private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
143-
=> _context!.OnlyProps(props);
144-
145-
private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
146-
=> _context!.ExceptProps(props);
147-
148-
private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> props)
149-
{
150-
var alwaysProps = _props.GetType().GetProperties()
151-
.Where(o => o.PropertyType == typeof(AlwaysProp))
152-
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props));
153-
154-
return props
155-
.Where(kv => kv.Value is not AlwaysProp)
156-
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
157-
}
158217
}

InertiaCore/ResponseFactory.cs

+16-10
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,14 @@ public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, I
4242
public Response Render(string component, object? props = null)
4343
{
4444
props ??= new { };
45+
var dictProps = props switch
46+
{
47+
Dictionary<string, object?> dict => dict,
48+
_ => props.GetType().GetProperties()
49+
.ToDictionary(o => o.Name, o => o.GetValue(props))
50+
};
4551

46-
return new Response(component, props, _options.Value.RootView, GetVersion());
52+
return new Response(component, dictProps, _options.Value.RootView, GetVersion());
4753
}
4854

4955
public async Task<IHtmlContent> Head(dynamic model)
@@ -104,8 +110,8 @@ public void Share(string key, object? value)
104110
{
105111
var context = _contextAccessor.HttpContext!;
106112

107-
var sharedData = context.Features.Get<InertiaSharedData>();
108-
sharedData ??= new InertiaSharedData();
113+
var sharedData = context.Features.Get<InertiaSharedProps>();
114+
sharedData ??= new InertiaSharedProps();
109115
sharedData.Set(key, value);
110116

111117
context.Features.Set(sharedData);
@@ -115,16 +121,16 @@ public void Share(IDictionary<string, object?> data)
115121
{
116122
var context = _contextAccessor.HttpContext!;
117123

118-
var sharedData = context.Features.Get<InertiaSharedData>();
119-
sharedData ??= new InertiaSharedData();
124+
var sharedData = context.Features.Get<InertiaSharedProps>();
125+
sharedData ??= new InertiaSharedProps();
120126
sharedData.Merge(data);
121127

122128
context.Features.Set(sharedData);
123129
}
124130

125-
public LazyProp Lazy(Func<object?> callback) => new LazyProp(callback);
126-
public LazyProp Lazy(Func<Task<object?>> callback) => new LazyProp(callback);
127-
public AlwaysProp Always(object? value) => new AlwaysProp(value);
128-
public AlwaysProp Always(Func<object?> callback) => new AlwaysProp(callback);
129-
public AlwaysProp Always(Func<Task<object?>> callback) => new AlwaysProp(callback);
131+
public LazyProp Lazy(Func<object?> callback) => new(callback);
132+
public LazyProp Lazy(Func<Task<object?>> callback) => new(callback);
133+
public AlwaysProp Always(object? value) => new(value);
134+
public AlwaysProp Always(Func<object?> callback) => new(callback);
135+
public AlwaysProp Always(Func<Task<object?>> callback) => new(callback);
130136
}

InertiaCore/Utils/InertiaSharedData.cs renamed to InertiaCore/Utils/InertiaSharedProps.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace InertiaCore.Utils;
44

5-
internal class InertiaSharedData
5+
internal class InertiaSharedProps
66
{
77
private IDictionary<string, object?>? Data { get; set; }
88

0 commit comments

Comments
 (0)