diff --git a/InertiaCore/Extensions/Configure.cs b/InertiaCore/Extensions/Configure.cs index 867a3fa..e31c527 100644 --- a/InertiaCore/Extensions/Configure.cs +++ b/InertiaCore/Extensions/Configure.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace InertiaCore.Extensions; @@ -48,6 +49,7 @@ public static IServiceCollection AddInertia(this IServiceCollection services, services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.Configure(mvcOptions => { mvcOptions.Filters.Add(); }); @@ -56,6 +58,16 @@ public static IServiceCollection AddInertia(this IServiceCollection services, return services; } + public static IServiceCollection UseInertiaSerializer(this IServiceCollection services) + where TImplementation : IInertiaSerializer + { + services.Replace( + new ServiceDescriptor(typeof(IInertiaSerializer), typeof(TImplementation), ServiceLifetime.Singleton) + ); + + return services; + } + public static IServiceCollection AddViteHelper(this IServiceCollection services, Action? options = null) { diff --git a/InertiaCore/Response.cs b/InertiaCore/Response.cs index 4b9ed72..c0829c7 100644 --- a/InertiaCore/Response.cs +++ b/InertiaCore/Response.cs @@ -1,5 +1,3 @@ -using System.Text.Json; -using System.Text.Json.Serialization; using InertiaCore.Extensions; using InertiaCore.Models; using InertiaCore.Props; @@ -16,13 +14,15 @@ public class Response : IActionResult private readonly Dictionary _props; private readonly string _rootView; private readonly string? _version; + private readonly IInertiaSerializer _serializer; private ActionContext? _context; private Page? _page; private IDictionary? _viewData; - internal Response(string component, Dictionary props, string rootView, string? version) - => (_component, _props, _rootView, _version) = (component, props, rootView, version); + internal Response(string component, Dictionary props, string rootView, string? version, + IInertiaSerializer serializer) + => (_component, _props, _rootView, _version, _serializer) = (component, props, rootView, version, serializer); public async Task ExecuteResultAsync(ActionContext context) { @@ -172,11 +172,7 @@ protected internal JsonResult GetJson() _context!.HttpContext.Response.Headers.Override("Vary", "Accept"); _context!.HttpContext.Response.StatusCode = 200; - return new JsonResult(_page, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }); + return _serializer.SerializeResult(_page); } private ViewResult GetView() diff --git a/InertiaCore/ResponseFactory.cs b/InertiaCore/ResponseFactory.cs index ad57af9..e88781e 100644 --- a/InertiaCore/ResponseFactory.cs +++ b/InertiaCore/ResponseFactory.cs @@ -1,6 +1,4 @@ using System.Net; -using System.Text.Json; -using System.Text.Json.Serialization; using InertiaCore.Models; using InertiaCore.Props; using InertiaCore.Ssr; @@ -33,12 +31,14 @@ internal class ResponseFactory : IResponseFactory { private readonly IHttpContextAccessor _contextAccessor; private readonly IGateway _gateway; + private readonly IInertiaSerializer _serializer; private readonly IOptions _options; private object? _version; - public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, IOptions options) => - (_contextAccessor, _gateway, _options) = (contextAccessor, gateway, options); + public ResponseFactory(IHttpContextAccessor contextAccessor, IGateway gateway, IInertiaSerializer serializer, + IOptions options) + => (_contextAccessor, _gateway, _serializer, _options) = (contextAccessor, gateway, serializer, options); public Response Render(string component, object? props = null) { @@ -50,7 +50,7 @@ public Response Render(string component, object? props = null) .ToDictionary(o => o.Name, o => o.GetValue(props)) }; - return new Response(component, dictProps, _options.Value.RootView, GetVersion()); + return new Response(component, dictProps, _options.Value.RootView, GetVersion(), _serializer); } public async Task Head(dynamic model) @@ -84,13 +84,7 @@ public async Task Html(dynamic model) } } - var data = JsonSerializer.Serialize(model, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }); - + var data = _serializer.Serialize(model); var encoded = WebUtility.HtmlEncode(data); return new HtmlString($"
"); diff --git a/InertiaCore/Ssr/Gateway.cs b/InertiaCore/Ssr/Gateway.cs index 1878c52..abad241 100644 --- a/InertiaCore/Ssr/Gateway.cs +++ b/InertiaCore/Ssr/Gateway.cs @@ -1,7 +1,6 @@ using System.Net.Http.Json; using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; +using InertiaCore.Utils; namespace InertiaCore.Ssr; @@ -13,17 +12,14 @@ internal interface IGateway internal class Gateway : IGateway { private readonly IHttpClientFactory _httpClientFactory; + private readonly IInertiaSerializer _serializer; - public Gateway(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; + public Gateway(IHttpClientFactory httpClientFactory, IInertiaSerializer serializer) + => (_httpClientFactory, _serializer) = (httpClientFactory, serializer); public async Task Dispatch(dynamic model, string url) { - var json = JsonSerializer.Serialize(model, - new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - ReferenceHandler = ReferenceHandler.IgnoreCycles - }); + var json = _serializer.Serialize(model); var content = new StringContent(json.ToString(), Encoding.UTF8, "application/json"); var client = _httpClientFactory.CreateClient(); diff --git a/InertiaCore/Utils/DefaultInertiaSerializer.cs b/InertiaCore/Utils/DefaultInertiaSerializer.cs new file mode 100644 index 0000000..1e15805 --- /dev/null +++ b/InertiaCore/Utils/DefaultInertiaSerializer.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; + +namespace InertiaCore.Utils; + +public class DefaultInertiaSerializer : IInertiaSerializer +{ + protected static JsonSerializerOptions GetOptions() + { + return new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + } + + public string Serialize(object? obj) + { + return JsonSerializer.Serialize(obj, GetOptions()); + } + + public JsonResult SerializeResult(object? obj) + { + return new JsonResult(obj, GetOptions()); + } +} diff --git a/InertiaCore/Utils/IInertiaSerializer.cs b/InertiaCore/Utils/IInertiaSerializer.cs new file mode 100644 index 0000000..4252a21 --- /dev/null +++ b/InertiaCore/Utils/IInertiaSerializer.cs @@ -0,0 +1,10 @@ +using Microsoft.AspNetCore.Mvc; + +namespace InertiaCore.Utils; + +public interface IInertiaSerializer +{ + public string Serialize(object? obj); + + public JsonResult SerializeResult(object? obj); +} diff --git a/InertiaCoreTests/Setup.cs b/InertiaCoreTests/Setup.cs index 5942c2b..55fd4f5 100644 --- a/InertiaCoreTests/Setup.cs +++ b/InertiaCoreTests/Setup.cs @@ -22,11 +22,12 @@ public void Setup() var contextAccessor = new Mock(); var httpClientFactory = new Mock(); - var gateway = new Gateway(httpClientFactory.Object); + var serializer = new DefaultInertiaSerializer(); + var gateway = new Gateway(httpClientFactory.Object, serializer); var options = new Mock>(); options.SetupGet(x => x.Value).Returns(new InertiaOptions()); - _factory = new ResponseFactory(contextAccessor.Object, gateway, options.Object); + _factory = new ResponseFactory(contextAccessor.Object, gateway, serializer, options.Object); } /// diff --git a/InertiaCoreTests/UnitTestConfiguration.cs b/InertiaCoreTests/UnitTestConfiguration.cs index 5cf8581..e07198c 100644 --- a/InertiaCoreTests/UnitTestConfiguration.cs +++ b/InertiaCoreTests/UnitTestConfiguration.cs @@ -9,6 +9,10 @@ namespace InertiaCoreTests; +internal class DummySerializer : DefaultInertiaSerializer +{ +} + public partial class Tests { [Test] @@ -28,6 +32,7 @@ public void TestConfiguration() Assert.That(builder.Services.Any(s => s.ServiceType == typeof(IResponseFactory)), Is.True); Assert.That(builder.Services.Any(s => s.ServiceType == typeof(IGateway)), Is.True); + Assert.That(builder.Services.Any(s => s.ServiceType == typeof(IInertiaSerializer)), Is.True); }); var mvcConfiguration = @@ -45,4 +50,30 @@ public void TestConfiguration() Assert.DoesNotThrow(() => Inertia.GetVersion()); } + + [Test] + [Description("Test if the configuration registers properly custom JSON serializer.")] + public void TestSerializerConfiguration() + { + var builder = WebApplication.CreateBuilder(); + builder.Services.AddInertia(); + + Assert.Multiple(() => + { + Assert.That(builder.Services.Any(s => s.ServiceType == typeof(IInertiaSerializer)), Is.True); + + Assert.That(builder.Services.Any(s => s.ImplementationType == typeof(DefaultInertiaSerializer)), Is.True); + Assert.That(builder.Services.Any(s => s.ImplementationType == typeof(DummySerializer)), Is.False); + }); + + Assert.DoesNotThrow(() => builder.Services.UseInertiaSerializer()); + + Assert.Multiple(() => + { + Assert.That(builder.Services.Any(s => s.ServiceType == typeof(IInertiaSerializer)), Is.True); + + Assert.That(builder.Services.Any(s => s.ImplementationType == typeof(DefaultInertiaSerializer)), Is.False); + Assert.That(builder.Services.Any(s => s.ImplementationType == typeof(DummySerializer)), Is.True); + }); + } } diff --git a/README.md b/README.md index a7ef668..cf87e7a 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,73 @@ builder.Services.AddInertia(options => }); ``` +### Custom JSON serializer + +You can use a custom JSON serializer in your app by creating a custom class implementing the `IInertiaSerializer` +interface: + +```csharp +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.AspNetCore.Mvc; + +public class CustomSerializer : IInertiaSerializer +{ + // Used in HTML responses + public string Serialize(object? obj) + { + // Default serialization + return JsonSerializer.Serialize(obj, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }); + } + + // Used in JSON responses + public JsonResult SerializeResult(object? obj) + { + // Default serialization + return new JsonResult(obj, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }); + } +} +``` + +or extending the `DefaultInertiaSerializer` class, which also implements the `IInertiaSerializer` interface: + +```csharp +public class CustomSerializer : DefaultInertiaSerializer +{ + protected new static JsonSerializerOptions GetOptions() + { + // Default options + return new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + ReferenceHandler = ReferenceHandler.IgnoreCycles + }; + } +} +``` + +You can then register it in the configuration: + +```csharp +builder.Services.AddInertia(); + +[...] + +builder.Services.UseInertiaSerializer(); + +[...] + +app.UseInertia(); +``` + ### Vite Helper A Vite helper class is available to automatically load your generated styles or scripts by simply using the `@Vite.Input("src/main.tsx")` helper. You can also enable HMR when using React by using the `@Vite.ReactRefresh()` helper. This pairs well with the `laravel-vite-plugin` npm package.