Skip to content
Closed
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
105 changes: 105 additions & 0 deletions InertiaCore/Contracts/IInertia.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using InertiaCore.Props;
using Microsoft.AspNetCore.Mvc;

namespace InertiaCore.Contracts;

/// <summary>
/// Main Inertia.js service interface for server-side adapter operations.
/// Inject this interface into your controllers and services via constructor injection.
/// </summary>
public interface IInertia
{
/// <summary>
/// Render an Inertia page component with the given props.
/// </summary>
/// <param name="component">The JavaScript component name (e.g. "Admin/Users").</param>
/// <param name="props">Optional props object, anonymous type, or dictionary.</param>
/// <returns>An <see cref="IActionResult"/> that returns either a JSON envelope or a full page view.</returns>
Task<IActionResult> Render(string component, object? props = null);

/// <summary>
/// Return an external redirect via an Inertia Location response (409 + X-Inertia-Location).
/// </summary>
IActionResult Location(string url);

/// <summary>
/// Share a value across all Inertia responses for the current request.
/// </summary>
/// <param name="key">The prop key (will be camelCased).</param>
/// <param name="value">The value to share.</param>
void Share(string key, object? value);

/// <summary>
/// Share multiple values across all Inertia responses for the current request.
/// </summary>
void Share(IDictionary<string, object?> data);

/// <summary>
/// Share a flash prop that will only be available for the current request.
/// Flash props are merged after shared props but before component props.
/// </summary>
void Flash(string key, object? value);

/// <summary>
/// Set the asset version to a fixed string.
/// </summary>
void Version(string? version);

/// <summary>
/// Set the asset version to a lazily-evaluated function.
/// </summary>
void Version(Func<string?> versionFunc);

/// <summary>
/// Get the currently configured asset version.
/// </summary>
string? GetVersion();

/// <summary>
/// Create an AlwaysProp that will always be included in all responses,
/// even during partial reloads where its key would otherwise be excluded.
/// </summary>
AlwaysProp Always(object? value);

/// <summary>
/// Create an AlwaysProp with a synchronous factory.
/// </summary>
AlwaysProp Always(Func<object?> callback);

/// <summary>
/// Create an AlwaysProp with an asynchronous factory.
/// </summary>
AlwaysProp Always(Func<Task<object?>> callback);

/// <summary>
/// Create a LazyProp that is excluded from the initial page load
/// and only resolved on subsequent partial reloads.
/// </summary>
LazyProp Lazy(Func<object?> callback);

/// <summary>
/// Create a LazyProp with an asynchronous factory.
/// </summary>
LazyProp Lazy(Func<Task<object?>> callback);

/// <summary>
/// Create a DeferredProp that is excluded from the initial full page load
/// and resolved only when explicitly requested via X-Inertia-Partial-Data.
/// </summary>
DeferredProp Defer(Func<object?> factory);

/// <summary>
/// Create a DeferredProp with an asynchronous factory.
/// </summary>
DeferredProp Defer(Func<Task<object?>> factory);

/// <summary>
/// Set whether to encrypt history state on the client side.
/// </summary>
void EncryptHistory(bool encrypt = true);

/// <summary>
/// Set whether to clear history state on the client side.
/// </summary>
void ClearHistory(bool clear = true);
}
45 changes: 39 additions & 6 deletions InertiaCore/Extensions/Configure.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
using System.Net;
using InertiaCore.Contracts;
using InertiaCore.Models;
using InertiaCore.Services;
using InertiaCore.Ssr;
using InertiaCore.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace InertiaCore.Extensions;

public static class Configure
{
#pragma warning disable CS0618 // Internal usage of the deprecated static facade is intentional
public static IApplicationBuilder UseInertia(this IApplicationBuilder app)
{
var factory = app.ApplicationServices.GetRequiredService<IResponseFactory>();
Inertia.UseFactory(factory);
// IResponseFactory is scoped (shares InertiaState with the per-request IInertia).
// We create a scope here to resolve it without triggering the "scoped from root"
// validation. The scope lives for the app lifetime since the factory is held
// by the static Inertia facade.
var startupScope = app.ApplicationServices.CreateScope();
var factory = startupScope.ServiceProvider.GetRequiredService<IResponseFactory>();
var contextAccessor = app.ApplicationServices.GetRequiredService<IHttpContextAccessor>();
Inertia.UseFactory(factory, contextAccessor);

var viteBuilder = app.ApplicationServices.GetService<IViteBuilder>();
if (viteBuilder != null)
Expand All @@ -26,9 +36,13 @@ public static IApplicationBuilder UseInertia(this IApplicationBuilder app)

app.Use(async (context, next) =>
{
if (context.IsInertiaRequest()
&& context.Request.Method == "GET"
&& context.Request.Headers[InertiaHeader.Version] != Inertia.GetVersion())
// Per the Inertia.js protocol, asset version conflicts must be reported
// on ALL Inertia requests, not just GET.
// See: https://inertiajs.com/the-protocol#asset-versioning
var serverVersion = Inertia.GetVersion();
if (serverVersion != null
&& context.IsInertiaRequest()
&& context.Request.Headers[InertiaHeader.Version] != serverVersion)
{
await OnVersionChange(context, app);
return;
Expand All @@ -46,7 +60,25 @@ public static IServiceCollection AddInertia(this IServiceCollection services,
services.AddHttpContextAccessor();
services.AddHttpClient();

services.AddSingleton<IResponseFactory, ResponseFactory>();
// Per-request state container — seed Version from InertiaOptions so that
// the asset version is available consistently across the middleware check
// and response body without requiring middleware wiring or static facade calls.
services.AddScoped<InertiaState>(sp =>
{
var version = sp.GetRequiredService<IOptions<InertiaOptions>>().Value.Version;
return new InertiaState { Version = version };
});

// InertiaService is the public API that consumers should inject via IInertia.
// Registered as Scoped — one instance per HTTP request.
services.AddScoped<IInertia, InertiaService>();

// Internal factory — scoped so it shares the same per-request InertiaState
// as InertiaService, ensuring Share(), Flash(), and Version calls via IInertia
// are visible to the response pipeline.
services.AddScoped<IResponseFactory, ResponseFactory>();

// Gateway is safe as Singleton since IHttpClientFactory manages HttpClient lifetimes.
services.AddSingleton<IGateway, Gateway>();

services.Configure<MvcOptions>(mvcOptions => { mvcOptions.Filters.Add<InertiaActionFilter>(); });
Expand Down Expand Up @@ -78,3 +110,4 @@ private static async Task OnVersionChange(HttpContext context, IApplicationBuild
await context.Response.CompleteAsync();
}
}
#pragma warning restore CS0618
20 changes: 9 additions & 11 deletions InertiaCore/Extensions/InertiaExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,21 @@ internal static bool Override<TKey, TValue>(this IDictionary<TKey, TValue> dicti
return true;
}

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

if (!rt.IsGenericType || rt.GetGenericTypeDefinition() != typeof(Task<>))
return Task.Run(func.Invoke);
if (result is Task task)
{
return await task.UnwrapResultAsync();
}

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

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

return result?.GetValue(task);
return task.UnwrapResultAsync();
}

internal static string MD5(this string s)
Expand Down
100 changes: 97 additions & 3 deletions InertiaCore/Inertia.cs
Original file line number Diff line number Diff line change
@@ -1,43 +1,137 @@
using System.Runtime.CompilerServices;
using InertiaCore.Contracts;
using InertiaCore.Props;
using InertiaCore.Utils;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;

[assembly: InternalsVisibleTo("InertiaCoreTests")]

namespace InertiaCore;

/// <summary>
/// Static facade for Inertia.js operations.
/// <para>
/// This class is deprecated. Inject <see cref="IInertia"/> via constructor injection
/// instead. This static facade will be removed in a future major version.
/// </para>
/// <code>
/// public class MyController : Controller
/// {
/// private readonly IInertia _inertia;
/// public MyController(IInertia inertia) => _inertia = inertia;
/// public IActionResult Index() => await _inertia.Render("Page", new { });
/// }
/// </code>
/// </summary>
[Obsolete("The static Inertia facade is deprecated. Inject IInertia via constructor injection instead.")]
public static class Inertia
{
private static IResponseFactory _factory = default!;

internal static void UseFactory(IResponseFactory factory) => _factory = factory;

private static IHttpContextAccessor? _contextAccessor;

internal static void UseFactory(IResponseFactory factory, IHttpContextAccessor? contextAccessor = null)
{
_factory = factory;
_contextAccessor = contextAccessor;
}

private static IInertia ResolveService()
{
if (_contextAccessor?.HttpContext != null)
{
var service = _contextAccessor.HttpContext.RequestServices.GetService(typeof(IInertia));
if (service is IInertia inertia)
return inertia;
}

// Fallback: wrap the internal factory
var state = new Services.InertiaState();
var options = Microsoft.Extensions.Options.Options.Create(new Models.InertiaOptions());
return new Services.InertiaService(_factory, state, options);
}

/// <summary>
/// Render an Inertia page component.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static Response Render(string component, object? props = null) => _factory.Render(component, props);

/// <summary>
/// Render the SSR head content.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static Task<IHtmlContent> Head(dynamic model) => _factory.Head(model);

/// <summary>
/// Render the SSR body / non-SSR placeholder HTML.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static Task<IHtmlContent> Html(dynamic model) => _factory.Html(model);

/// <summary>
/// Set the asset version to a fixed string.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static void Version(string? version) => _factory.Version(version);

/// <summary>
/// Set the asset version via a lazily-evaluated function.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static void Version(Func<string?> version) => _factory.Version(version);

/// <summary>
/// Get the currently configured asset version.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static string? GetVersion() => _factory.GetVersion();

/// <summary>
/// Return an external redirect via Inertia Location response.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static LocationResult Location(string url) => _factory.Location(url);

/// <summary>
/// Share a value across all Inertia responses for the current request.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static void Share(string key, object? value) => _factory.Share(key, value);

/// <summary>
/// Share multiple values across all Inertia responses for the current request.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static void Share(IDictionary<string, object?> data) => _factory.Share(data);

/// <summary>
/// Create an AlwaysProp.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static AlwaysProp Always(string value) => _factory.Always(value);

/// <summary>
/// Create an AlwaysProp with a synchronous callback.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static AlwaysProp Always(Func<string> callback) => _factory.Always(callback);

/// <summary>
/// Create an AlwaysProp with an asynchronous callback.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static AlwaysProp Always(Func<Task<object?>> callback) => _factory.Always(callback);

/// <summary>
/// Create a LazyProp.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static LazyProp Lazy(Func<object?> callback) => _factory.Lazy(callback);

/// <summary>
/// Create a LazyProp with an asynchronous callback.
/// </summary>
[Obsolete("Inject IInertia via constructor injection instead.")]
public static LazyProp Lazy(Func<Task<object?>> callback) => _factory.Lazy(callback);
}
5 changes: 2 additions & 3 deletions InertiaCore/InertiaCore.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Version>0.0.9</Version>
<TargetFrameworks>net6.0;net7.0;net8.0;net9.0</TargetFrameworks>
<Version>0.0.10</Version>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<PackageId>AspNetCore.InertiaCore</PackageId>
<Authors>kapi2289</Authors>
<Description>Inertia.js ASP.NET Adapter. https://inertiajs.com/</Description>
Expand All @@ -22,7 +22,6 @@

<ItemGroup>
<PackageReference Include="System.IO.Abstractions" Version="19.2.16" />
<PackageReference Include="TypeMerger" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions InertiaCore/Models/InertiaOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ public class InertiaOptions
public bool SsrEnabled { get; set; } = false;
public string SsrUrl { get; set; } = "http://127.0.0.1:13714/render";
public bool EncryptHistory { get; set; } = false;

public string? Version { get; set; }
}
Loading