Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1.x] Properly resolve nested properties #32

Merged
merged 4 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
44 changes: 19 additions & 25 deletions InertiaCore/Extensions/InertiaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,6 @@ namespace InertiaCore.Extensions;

internal static class InertiaExtensions
{
internal static Dictionary<string, object?> OnlyProps(this ActionContext context, Dictionary<string, object?> props)
{
var onlyKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

internal static Dictionary<string, object?> ExceptProps(this ActionContext context,
Dictionary<string, object?> props)
{
var exceptKeys = context.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

internal static bool IsInertiaPartialComponent(this ActionContext context, string component) =>
context.HttpContext.Request.Headers[InertiaHeader.PartialComponent] == component;

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

return true;
}

internal static Task<object?> ResolveAsync(this Func<object?> func)
{
var rt = func.Method.ReturnType;

if (!rt.IsGenericType || rt.GetGenericTypeDefinition() != typeof(Task<>))
return Task.Run(func.Invoke);

var task = func.DynamicInvoke() as Task;
return task!.ResolveResult();
}

internal static async Task<object?> ResolveResult(this Task task)
{
await task.ConfigureAwait(false);
var result = task.GetType().GetProperty("Result");

return result?.GetValue(task);
}
}
8 changes: 5 additions & 3 deletions InertiaCore/Props/InvokableProp.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using InertiaCore.Extensions;

namespace InertiaCore.Props;

public class InvokableProp
Expand All @@ -10,9 +12,9 @@ public class InvokableProp
{
return _value switch
{
Func<Task<object?>> asyncCallable => asyncCallable.Invoke(),
Func<object?> callable => Task.Run(() => callable.Invoke()),
Task<object?> value => value,
Func<object?> f => f.ResolveAsync(),
Task t => t.ResolveResult(),
InvokableProp p => p.Invoke(),
_ => Task.FromResult(_value)
};
}
Expand Down
181 changes: 120 additions & 61 deletions InertiaCore/Response.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,54 +13,157 @@ namespace InertiaCore;
public class Response : IActionResult
{
private readonly string _component;
private readonly object _props;
private readonly Dictionary<string, object?> _props;
private readonly string _rootView;
private readonly string? _version;

private ActionContext? _context;
private Page? _page;
private IDictionary<string, object>? _viewData;

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

public async Task ExecuteResultAsync(ActionContext context)
{
SetContext(context);
await ProcessResponse();

await GetResult().ExecuteResultAsync(_context!);
}

protected internal async Task ProcessResponse()
{
var props = await ResolveProperties();

var page = new Page
{
Component = _component,
Version = _version,
Url = _context!.RequestedUri(),
Props = await ResolveProperties(_props.GetType().GetProperties()
.ToDictionary(o => o.Name.ToCamelCase(), o => o.GetValue(_props)))
Props = props
};

var shared = _context!.HttpContext.Features.Get<InertiaSharedData>();
if (shared != null)
page.Props = shared.GetMerged(page.Props);

page.Props["errors"] = GetErrors();

SetPage(page);
}

private static async Task<Dictionary<string, object?>> PrepareProps(Dictionary<string, object?> props)
/// <summary>
/// Resolve the properties for the response.
/// </summary>
private async Task<Dictionary<string, object?>> ResolveProperties()
{
var props = _props;

props = ResolveSharedProps(props);
props = ResolvePartialProperties(props);
props = ResolveAlways(props);
props = await ResolvePropertyInstances(props);

return props;
}

/// <summary>
/// Resolve `shared` props stored in the current request context.
/// </summary>
private Dictionary<string, object?> ResolveSharedProps(Dictionary<string, object?> props)
{
var shared = _context!.HttpContext.Features.Get<InertiaSharedProps>();
if (shared != null)
props = shared.GetMerged(props);

return props;
}

/// <summary>
/// Resolve the `only` and `except` partial request props.
/// </summary>
private Dictionary<string, object?> ResolvePartialProperties(Dictionary<string, object?> props)
{
var isPartial = _context!.IsInertiaPartialComponent(_component);

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

props = props.ToDictionary(kv => kv.Key, kv => kv.Value);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
props = ResolveOnly(props);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
props = ResolveExcept(props);

return props;
}

/// <summary>
/// Resolve the `only` partial request props.
/// </summary>
private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
{
var onlyKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialOnly]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => onlyKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase))
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve the `except` partial request props.
/// </summary>
private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
{
var exceptKeys = _context!.HttpContext.Request.Headers[InertiaHeader.PartialExcept]
.ToString().Split(',')
.Select(k => k.Trim())
.Where(k => !string.IsNullOrEmpty(k))
.ToList();

return props.Where(kv => exceptKeys.Contains(kv.Key, StringComparer.OrdinalIgnoreCase) == false)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve `always` properties that should always be included on all visits, regardless of "only" or "except" requests.
/// </summary>
private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> props)
{
var alwaysProps = _props.Where(o => o.Value is AlwaysProp);

return props
.Where(kv => kv.Value is not AlwaysProp)
.Concat(alwaysProps).ToDictionary(kv => kv.Key, kv => kv.Value);
}

/// <summary>
/// Resolve all necessary class instances in the given props.
/// </summary>
private static async Task<Dictionary<string, object?>> ResolvePropertyInstances(Dictionary<string, object?> props)
{
return (await Task.WhenAll(props.Select(async pair => pair.Value switch
return (await Task.WhenAll(props.Select(async pair =>
{
Func<object?> f => (pair.Key, f.Invoke()),
LazyProp l => (pair.Key, await l.Invoke()),
AlwaysProp l => (pair.Key, await l.Invoke()),
_ => (pair.Key, pair.Value)
}))).ToDictionary(pair => pair.Key, pair => pair.Item2);
var key = pair.Key.ToCamelCase();

var value = pair.Value switch
{
Func<object?> f => (key, await f.ResolveAsync()),
Task t => (key, await t.ResolveResult()),
InvokableProp p => (key, await p.Invoke()),
_ => (key, pair.Value)
};

if (value.Item2 is Dictionary<string, object?> dict)
{
value = (key, await ResolvePropertyInstances(dict));
}

return value;
}))).ToDictionary(pair => pair.key, pair => pair.Item2);
}

protected internal JsonResult GetJson()
Expand Down Expand Up @@ -93,7 +196,7 @@ private ViewResult GetView()

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

private IDictionary<string, string> GetErrors()
private Dictionary<string, string> GetErrors()
{
if (!_context!.ModelState.IsValid)
return _context!.ModelState.ToDictionary(o => o.Key.ToCamelCase(),
Expand All @@ -111,48 +214,4 @@ public Response WithViewData(IDictionary<string, object> viewData)
_viewData = viewData;
return this;
}

private async Task<Dictionary<string, object?>> ResolveProperties(Dictionary<string, object?> props)
{
var isPartial = _context!.IsInertiaPartialComponent(_component);

if (!isPartial)
{
props = props
.Where(kv => kv.Value is not LazyProp)
.ToDictionary(kv => kv.Key, kv => kv.Value);
}
else
{
props = props.ToDictionary(kv => kv.Key, kv => kv.Value);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialOnly))
props = ResolveOnly(props);

if (_context!.HttpContext.Request.Headers.ContainsKey(InertiaHeader.PartialExcept))
props = ResolveExcept(props);
}

props = ResolveAlways(props);
props = await PrepareProps(props);

return props;
}

private Dictionary<string, object?> ResolveOnly(Dictionary<string, object?> props)
=> _context!.OnlyProps(props);

private Dictionary<string, object?> ResolveExcept(Dictionary<string, object?> props)
=> _context!.ExceptProps(props);

private Dictionary<string, object?> ResolveAlways(Dictionary<string, object?> 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);
}
}
26 changes: 16 additions & 10 deletions InertiaCore/ResponseFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,14 @@ public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, I
public Response Render(string component, object? props = null)
{
props ??= new { };
var dictProps = props switch
{
Dictionary<string, object?> dict => dict,
_ => props.GetType().GetProperties()
.ToDictionary(o => o.Name, o => o.GetValue(props))
};

return new Response(component, props, _options.Value.RootView, GetVersion());
return new Response(component, dictProps, _options.Value.RootView, GetVersion());
}

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

var sharedData = context.Features.Get<InertiaSharedData>();
sharedData ??= new InertiaSharedData();
var sharedData = context.Features.Get<InertiaSharedProps>();
sharedData ??= new InertiaSharedProps();
sharedData.Set(key, value);

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

var sharedData = context.Features.Get<InertiaSharedData>();
sharedData ??= new InertiaSharedData();
var sharedData = context.Features.Get<InertiaSharedProps>();
sharedData ??= new InertiaSharedProps();
sharedData.Merge(data);

context.Features.Set(sharedData);
}

public LazyProp Lazy(Func<object?> callback) => new LazyProp(callback);
public LazyProp Lazy(Func<Task<object?>> callback) => new LazyProp(callback);
public AlwaysProp Always(object? value) => new AlwaysProp(value);
public AlwaysProp Always(Func<object?> callback) => new AlwaysProp(callback);
public AlwaysProp Always(Func<Task<object?>> callback) => new AlwaysProp(callback);
public LazyProp Lazy(Func<object?> callback) => new(callback);
public LazyProp Lazy(Func<Task<object?>> callback) => new(callback);
public AlwaysProp Always(object? value) => new(value);
public AlwaysProp Always(Func<object?> callback) => new(callback);
public AlwaysProp Always(Func<Task<object?>> callback) => new(callback);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace InertiaCore.Utils;

internal class InertiaSharedData
internal class InertiaSharedProps
{
private IDictionary<string, object?>? Data { get; set; }

Expand Down
Loading
Loading