From d11c6505e5293b23b4d7dd5ac7c0c3c4b2638d60 Mon Sep 17 00:00:00 2001 From: Tewr Date: Thu, 22 Feb 2024 19:18:20 +0100 Subject: [PATCH] =?UTF-8?q?Enables=20a=20custom=20expression=20serializer,?= =?UTF-8?q?=20and=E2=80=A6=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enables a custom expression serializer, and also a baseclass for just adding known types to the Serialize.Linq JsonSerialize --- .../Pages/ComplexSerialization.razor | 2 + .../Pages/ComplexSerialization.razor | 133 ++++++++++++++++++ .../Shared/CustomExpressionSerializer.cs | 47 +++++++ .../SharedPages/Shared/NavMenuLinksModel.cs | 7 +- .../BlazorWorker.BackgroundServiceFactory.xml | 9 ++ .../WorkerBackgroundServiceExtensions.cs | 28 +++- .../WorkerInitExtension.cs | 28 ++++ .../BlazorWorker.WorkerBackgroundService.xml | 12 ++ ...rializeLinqExpressionJsonSerializerBase.cs | 57 ++++++++ .../WebWorkerOptions.cs | 53 ++++++- .../WorkerInstanceManager.cs | 12 +- src/BlazorWorker.WorkerCore/InitOptions.cs | 33 +---- 12 files changed, 384 insertions(+), 37 deletions(-) create mode 100644 src/BlazorWorker.Demo/Net8/Client/BlazorWorker.Demo.Client/Pages/ComplexSerialization.razor create mode 100644 src/BlazorWorker.Demo/SharedPages/Pages/ComplexSerialization.razor create mode 100644 src/BlazorWorker.Demo/SharedPages/Shared/CustomExpressionSerializer.cs create mode 100644 src/BlazorWorker.ServiceFactory/WorkerInitExtension.cs create mode 100644 src/BlazorWorker.WorkerBackgroundService/SerializeLinqExpressionJsonSerializerBase.cs diff --git a/src/BlazorWorker.Demo/Net8/Client/BlazorWorker.Demo.Client/Pages/ComplexSerialization.razor b/src/BlazorWorker.Demo/Net8/Client/BlazorWorker.Demo.Client/Pages/ComplexSerialization.razor new file mode 100644 index 0000000..e937d86 --- /dev/null +++ b/src/BlazorWorker.Demo/Net8/Client/BlazorWorker.Demo.Client/Pages/ComplexSerialization.razor @@ -0,0 +1,2 @@ +@page "/ComplexSerialization" + \ No newline at end of file diff --git a/src/BlazorWorker.Demo/SharedPages/Pages/ComplexSerialization.razor b/src/BlazorWorker.Demo/SharedPages/Pages/ComplexSerialization.razor new file mode 100644 index 0000000..dac3d94 --- /dev/null +++ b/src/BlazorWorker.Demo/SharedPages/Pages/ComplexSerialization.razor @@ -0,0 +1,133 @@ +@inject IWorkerFactory workerFactory + +@using BlazorWorker.BackgroundServiceFactory +@using BlazorWorker.WorkerBackgroundService +@using BlazorWorker.Demo.Shared +@using BlazorWorker.Core +
+
+

.NET Worker Multithreading

+ + Complex / Custom Serialization
+

+
+ +
+
+ Output: +
+
+@output
+
+
+
+ +
+
+@code { + + string output; + + IWorker worker; + IWorkerBackgroundService backgroundService; + + string RunDisabled => Running ? "disabled" : null; + bool Running = false; + + + public class ComplexService + { + public ComplexServiceResponse ComplexCall(ComplexServiceArg arg) + { + return new ComplexServiceResponse + { + OriginalArg = arg, + OnlyInResponse = "This is only in response." + }; + } + } + + public class ComplexServiceArg + { + public string ThisIsJustAString { get; set; } + public OhLookARecord ARecord { get; set; } + public Dictionary ADictionary { get; set; } + public ComplexServiceArg TypeRecursive { get; set; } + } + + public class ComplexServiceResponse + { + public ComplexServiceArg OriginalArg { get; set; } + public string OnlyInResponse { get; set; } + } + + public record OhLookARecord + { + public int Number { get; set; } + } + + public async Task OnClick(EventArgs _) + { + Running = true; + await this.InvokeAsync(StateHasChanged); + + output = ""; + var rn = Environment.NewLine; + try + { + if (worker == null) + { + output += $"Starting worker...{rn}"; + await this.InvokeAsync(StateHasChanged); + worker = await workerFactory.CreateAsync(); + } + if (backgroundService == null) + { + + output += $"Starting BackgroundService...{rn}"; + await this.InvokeAsync(StateHasChanged); + /* + * have a look here. This is the essence of this example. + * */ + + backgroundService = await worker.CreateBackgroundServiceAsync(options + => options.UseCustomExpressionSerializer(typeof(CustomSerializeLinqExpressionJsonSerializer))); + } + + var sw = System.Diagnostics.Stopwatch.StartNew(); + + var complexArgInstance = new ComplexServiceArg + { + ThisIsJustAString = "just a string", + ARecord = new OhLookARecord { Number = 5 }, + ADictionary = new Dictionary + { + { "Test", "TestValue" } + }, + TypeRecursive = new ComplexServiceArg + { + ThisIsJustAString = "SubString" + } + }; + + var result = await backgroundService.RunAsync(service => service.ComplexCall(complexArgInstance)); + var elapsed = sw.ElapsedMilliseconds; + output += $"{rn}result: " + System.Text.Json.JsonSerializer.Serialize(result); + output += $"{rn}roundtrip to worker in {elapsed}ms"; + } + catch (Exception e) + { + output += $"{rn}Error = {e}"; + } + finally + { + Running = false; + output += $"{rn}Done."; + } + } + + private string LogDate() + { + return DateTime.Now.ToString("HH:mm:ss:fff"); + } +} diff --git a/src/BlazorWorker.Demo/SharedPages/Shared/CustomExpressionSerializer.cs b/src/BlazorWorker.Demo/SharedPages/Shared/CustomExpressionSerializer.cs new file mode 100644 index 0000000..4cfbef2 --- /dev/null +++ b/src/BlazorWorker.Demo/SharedPages/Shared/CustomExpressionSerializer.cs @@ -0,0 +1,47 @@ +using BlazorWorker.WorkerBackgroundService; +using Serialize.Linq.Serializers; +using System; +using System.Linq.Expressions; +using static BlazorWorker.Demo.SharedPages.Pages.ComplexSerialization; + +namespace BlazorWorker.Demo.SharedPages.Shared +{ + /// + /// Example 1: Simple custom expression Serializer using + /// as base class, but explicitly adds complex types as known types. + /// + public class CustomSerializeLinqExpressionJsonSerializer : SerializeLinqExpressionJsonSerializerBase + { + public override Type[] GetKnownTypes() => + [typeof(ComplexServiceArg), typeof(ComplexServiceResponse), typeof(OhLookARecord)]; + } + + /// + /// Fully custom Expression Serializer, which uses but you could use an alternative implementation. + /// + public class CustomExpressionSerializer : IExpressionSerializer + { + private readonly ExpressionSerializer serializer; + + public CustomExpressionSerializer() + { + var specificSerializer = new JsonSerializer(); + specificSerializer.AddKnownType(typeof(ComplexServiceArg)); + specificSerializer.AddKnownType(typeof(ComplexServiceResponse)); + specificSerializer.AddKnownType(typeof(OhLookARecord)); + + this.serializer = new ExpressionSerializer(specificSerializer); + } + + public Expression Deserialize(string expressionString) + { + return serializer.DeserializeText(expressionString); + } + + public string Serialize(Expression expression) + { + return serializer.SerializeText(expression); + } + } + +} diff --git a/src/BlazorWorker.Demo/SharedPages/Shared/NavMenuLinksModel.cs b/src/BlazorWorker.Demo/SharedPages/Shared/NavMenuLinksModel.cs index 7c6f4c1..942f138 100644 --- a/src/BlazorWorker.Demo/SharedPages/Shared/NavMenuLinksModel.cs +++ b/src/BlazorWorker.Demo/SharedPages/Shared/NavMenuLinksModel.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components; +using BlazorWorker.Demo.SharedPages.Pages; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; using System; using System.Collections.Generic; @@ -34,6 +35,10 @@ public class NavMenuLinksModel { new() { Icon = "document", Href="IndexedDB", Text = "IndexedDB" } }, + { + new() { Icon = "document", Href="ComplexSerialization", Text = "ComplexSerialization" } + }, + { new() { Icon = "fork", Href="https://github.com/tewr/BlazorWorker", Text = "To the source!" } }, diff --git a/src/BlazorWorker.ServiceFactory/BlazorWorker.BackgroundServiceFactory.xml b/src/BlazorWorker.ServiceFactory/BlazorWorker.BackgroundServiceFactory.xml index dc8ded8..3c99b4f 100644 --- a/src/BlazorWorker.ServiceFactory/BlazorWorker.BackgroundServiceFactory.xml +++ b/src/BlazorWorker.ServiceFactory/BlazorWorker.BackgroundServiceFactory.xml @@ -30,5 +30,14 @@ Attached objects, notably parent worker proxy which may have been created without consumer being directly able to dispose + + + Sets a custom ExpressionSerializer type. Must implement .. + + + A type that implements + + + diff --git a/src/BlazorWorker.ServiceFactory/WorkerBackgroundServiceExtensions.cs b/src/BlazorWorker.ServiceFactory/WorkerBackgroundServiceExtensions.cs index 9d7c686..22951c6 100644 --- a/src/BlazorWorker.ServiceFactory/WorkerBackgroundServiceExtensions.cs +++ b/src/BlazorWorker.ServiceFactory/WorkerBackgroundServiceExtensions.cs @@ -26,13 +26,17 @@ public static async Task> CreateBackgroundServiceAsy { throw new ArgumentNullException(nameof(webWorkerProxy)); } - - var proxy = new WorkerBackgroundServiceProxy(webWorkerProxy, new WebWorkerOptions()); if (workerInitOptions == null) { workerInitOptions = new WorkerInitOptions(); } + var webWorkerOptions = new WebWorkerOptions(); + webWorkerOptions.SetExpressionSerializerFromInitOptions(workerInitOptions); + + var proxy = new WorkerBackgroundServiceProxy(webWorkerProxy, webWorkerOptions); + + await proxy.InitAsync(workerInitOptions); return proxy; } @@ -68,7 +72,17 @@ public static async Task> CreateBackgroundSer { workerInitOptionsModifier(workerInitOptions); } - var factoryProxy = new WorkerBackgroundServiceProxy(webWorkerProxy, new WebWorkerOptions()); + + if (workerInitOptions == null) + { + workerInitOptions = new WorkerInitOptions(); + } + + var webWorkerOptions = new WebWorkerOptions(); + webWorkerOptions.SetExpressionSerializerFromInitOptions(workerInitOptions); + + + var factoryProxy = new WorkerBackgroundServiceProxy(webWorkerProxy, webWorkerOptions); await factoryProxy.InitAsync(workerInitOptions); var newProxy = await factoryProxy.InitFromFactoryAsync(factoryExpression); @@ -76,6 +90,14 @@ public static async Task> CreateBackgroundSer return newProxy; } + private static void SetExpressionSerializerFromInitOptions(this WebWorkerOptions target, WorkerInitOptions workerInitOptions) + { + if (workerInitOptions.EnvMap?.TryGetValue(WebWorkerOptions.ExpressionSerializerTypeEnvKey, out var serializerType) == true) + { + target.ExpressionSerializerType = Type.GetType(serializerType); + } + } + /// /// Creates a new background service using the specified /// diff --git a/src/BlazorWorker.ServiceFactory/WorkerInitExtension.cs b/src/BlazorWorker.ServiceFactory/WorkerInitExtension.cs new file mode 100644 index 0000000..e0ce40a --- /dev/null +++ b/src/BlazorWorker.ServiceFactory/WorkerInitExtension.cs @@ -0,0 +1,28 @@ +using BlazorWorker.Core; +using BlazorWorker.WorkerBackgroundService; +using System; + +namespace BlazorWorker.BackgroundServiceFactory +{ + public static class WorkerInitExtension + { + + /// + /// Sets a custom ExpressionSerializer type. Must implement .. + /// + /// + /// A type that implements + /// + /// + public static WorkerInitOptions UseCustomExpressionSerializer(this WorkerInitOptions source, Type expressionSerializerType) + { + if (source == null) + { + throw new ArgumentNullException(nameof(source)); + } + + source.SetEnv(WebWorkerOptions.ExpressionSerializerTypeEnvKey, expressionSerializerType.AssemblyQualifiedName); + return source; + } + } +} diff --git a/src/BlazorWorker.WorkerBackgroundService/BlazorWorker.WorkerBackgroundService.xml b/src/BlazorWorker.WorkerBackgroundService/BlazorWorker.WorkerBackgroundService.xml index 0a10643..52dba3d 100644 --- a/src/BlazorWorker.WorkerBackgroundService/BlazorWorker.WorkerBackgroundService.xml +++ b/src/BlazorWorker.WorkerBackgroundService/BlazorWorker.WorkerBackgroundService.xml @@ -54,5 +54,17 @@ Unregisters the event corresponding to the specified + + + Name of environment variable to be used for transferring the serializer typename + + + + + Ensures that the provided type implements + + + + diff --git a/src/BlazorWorker.WorkerBackgroundService/SerializeLinqExpressionJsonSerializerBase.cs b/src/BlazorWorker.WorkerBackgroundService/SerializeLinqExpressionJsonSerializerBase.cs new file mode 100644 index 0000000..0131970 --- /dev/null +++ b/src/BlazorWorker.WorkerBackgroundService/SerializeLinqExpressionJsonSerializerBase.cs @@ -0,0 +1,57 @@ +using Serialize.Linq.Serializers; +using System; +using System.Linq.Expressions; + +namespace BlazorWorker.WorkerBackgroundService +{ + /// + /// Base class for adding known types to a serializer using . + /// + public abstract class SerializeLinqExpressionJsonSerializerBase : IExpressionSerializer + { + private ExpressionSerializer serializer; + + private ExpressionSerializer Serializer + => this.serializer ?? (this.serializer = GetSerializer()); + + /// + /// Automatically adds known types as array types. If set to true, sets to false. + /// + public bool? AutoAddKnownTypesAsArrayTypes { get; set; } + + /// + /// Automatically adds known types as list types. If set to true, sets to false. + /// + public bool? AutoAddKnownTypesAsListTypes { get; set; } + + public abstract Type[] GetKnownTypes(); + + private ExpressionSerializer GetSerializer() + { + var jsonSerializer = new JsonSerializer(); + foreach (var type in GetKnownTypes()) + { + jsonSerializer.AddKnownType(type); + } + if (this.AutoAddKnownTypesAsArrayTypes.HasValue) { + jsonSerializer.AutoAddKnownTypesAsArrayTypes = this.AutoAddKnownTypesAsArrayTypes.Value; + } + if (this.AutoAddKnownTypesAsListTypes.HasValue) + { + jsonSerializer.AutoAddKnownTypesAsListTypes = this.AutoAddKnownTypesAsListTypes.Value; + } + + return new ExpressionSerializer(jsonSerializer); + } + + public Expression Deserialize(string expressionString) + { + return this.Serializer.DeserializeText(expressionString); + } + + public string Serialize(Expression expression) + { + return this.Serializer.SerializeText(expression); + } + } +} diff --git a/src/BlazorWorker.WorkerBackgroundService/WebWorkerOptions.cs b/src/BlazorWorker.WorkerBackgroundService/WebWorkerOptions.cs index 1326d45..d816df8 100644 --- a/src/BlazorWorker.WorkerBackgroundService/WebWorkerOptions.cs +++ b/src/BlazorWorker.WorkerBackgroundService/WebWorkerOptions.cs @@ -1,18 +1,65 @@ -namespace BlazorWorker.WorkerBackgroundService +using System; + +namespace BlazorWorker.WorkerBackgroundService { public class WebWorkerOptions { + /// + /// Name of environment variable to be used for transferring the serializer typename + /// + public static readonly string ExpressionSerializerTypeEnvKey = "BLAZORWORKER_EXPRESSIONSERIALIZER"; + private ISerializer messageSerializer; private IExpressionSerializer expressionSerializer; + private Type expressionSerializerType; + + public ISerializer MessageSerializer { get => messageSerializer ?? (messageSerializer = new DefaultMessageSerializer()); set => messageSerializer = value; } - public IExpressionSerializer ExpressionSerializer { - get => expressionSerializer ?? (expressionSerializer = new SerializeLinqExpressionSerializer()) ; + public IExpressionSerializer ExpressionSerializer { + get => expressionSerializer ?? (expressionSerializer = CreateSerializerInstance()); set => expressionSerializer = value; } + + public Type ExpressionSerializerType + { + get => expressionSerializerType ?? typeof(SerializeLinqExpressionSerializer); + set => expressionSerializerType = ValidateExpressionSerializerType(value); + } + + /// + /// Ensures that the provided type implements + /// + /// + /// + private Type ValidateExpressionSerializerType(Type sourceType) + { + if (sourceType == null) + { + return null; + } + + if (!sourceType.IsClass) + { + throw new Exception($"The {nameof(ExpressionSerializerType)} '{sourceType.AssemblyQualifiedName}' must be a class."); + } + + if (!typeof(IExpressionSerializer).IsAssignableFrom(sourceType)) + { + throw new Exception($"The {nameof(ExpressionSerializerType)} '{sourceType.AssemblyQualifiedName}' must be assignable to {nameof(IExpressionSerializer)}"); + } + + return sourceType; + } + + private IExpressionSerializer CreateSerializerInstance() + { + var instance = Activator.CreateInstance(ExpressionSerializerType); + return (IExpressionSerializer)instance; + } } } diff --git a/src/BlazorWorker.WorkerBackgroundService/WorkerInstanceManager.cs b/src/BlazorWorker.WorkerBackgroundService/WorkerInstanceManager.cs index 79e943f..b0cb6fa 100644 --- a/src/BlazorWorker.WorkerBackgroundService/WorkerInstanceManager.cs +++ b/src/BlazorWorker.WorkerBackgroundService/WorkerInstanceManager.cs @@ -13,12 +13,12 @@ namespace BlazorWorker.WorkerBackgroundService public partial class WorkerInstanceManager { private readonly ConcurrentDictionary events = - new ConcurrentDictionary(); + new(); private static readonly MessageHandlerRegistry messageHandlerRegistry - = new MessageHandlerRegistry(wim => wim.serializer); + = new(wim => wim.serializer); - public static readonly WorkerInstanceManager Instance = new WorkerInstanceManager(); + public static readonly WorkerInstanceManager Instance = new(); internal readonly ISerializer serializer; private readonly WebWorkerOptions options; @@ -40,6 +40,12 @@ public WorkerInstanceManager() { this.serializer = new DefaultMessageSerializer(); this.options = new WebWorkerOptions(); + var expressionSerializerType = Environment.GetEnvironmentVariable(WebWorkerOptions.ExpressionSerializerTypeEnvKey); + if (expressionSerializerType != null) + { + this.options.ExpressionSerializerType = Type.GetType(expressionSerializerType); + } + this.simpleInstanceService = SimpleInstanceService.Instance; this.messageHandler = messageHandlerRegistry.GetRegistryForInstance(this); diff --git a/src/BlazorWorker.WorkerCore/InitOptions.cs b/src/BlazorWorker.WorkerCore/InitOptions.cs index f13db69..8cf27e6 100644 --- a/src/BlazorWorker.WorkerCore/InitOptions.cs +++ b/src/BlazorWorker.WorkerCore/InitOptions.cs @@ -14,7 +14,7 @@ public class WorkerInitOptions /// public WorkerInitOptions() { - DependentAssemblyFilenames = new string[] { }; + DependentAssemblyFilenames = []; #if NETSTANDARD21 DeployPrefix = "_framework/_bin"; @@ -49,7 +49,7 @@ public WorkerInitOptions() public string DeployPrefix { get; } /// - /// Specifieds the location of the wasm binary + /// Specifies the location of the wasm binary /// public string WasmRoot { get; } @@ -70,7 +70,7 @@ public WorkerInitOptions() public MethodIdentifier MessageEndPoint { get; set; } /// - /// Mono-wasm-annotated endpoint for instanciating the worker. Experts only. + /// Mono-wasm-annotated endpoint for instantiating the worker. Experts only. /// public MethodIdentifier InitEndPoint { get; set; } @@ -119,10 +119,6 @@ public WorkerInitOptions MergeWith(WorkerInitOptions initOptions) return new WorkerInitOptions { CallbackMethod = initOptions.CallbackMethod ?? this.CallbackMethod, - /*DependentAssemblyFilenames = this.DependentAssemblyFilenames - .Concat(initOptions.DependentAssemblyFilenames) - .Distinct() - .ToArray(),*/ UseConventionalServiceAssembly = initOptions.UseConventionalServiceAssembly, MessageEndPoint = initOptions.MessageEndPoint ?? this.MessageEndPoint, InitEndPoint = initOptions.InitEndPoint ?? this.InitEndPoint, @@ -139,7 +135,7 @@ public static class WorkerInitOptionsExtensions { /// - /// Adds the specified assembly filesnames as dependencies + /// Adds the specified assembly filenames as dependencies /// /// /// @@ -160,7 +156,6 @@ public static WorkerInitOptions AddAssemblies(this WorkerInitOptions source, par [Obsolete("Manual dependency optimization is silently ignored in this version of BlazorWorker.")] public static WorkerInitOptions AddConventionalAssemblyOfService(this WorkerInitOptions source) { - source.UseConventionalServiceAssembly = true; return source; } @@ -172,7 +167,7 @@ public static WorkerInitOptions AddConventionalAssemblyOfService(this WorkerInit [Obsolete("Manual dependency optimization is silently ignored in this version of BlazorWorker.")] public static WorkerInitOptions AddAssemblyOf(this WorkerInitOptions source) { - return source.AddAssemblyOfType(typeof(T)); + return source; } /// @@ -184,12 +179,11 @@ public static WorkerInitOptions AddAssemblyOf(this WorkerInitOptions source) [Obsolete("Manual dependency optimization is silently ignored in this version of BlazorWorker.")] public static WorkerInitOptions AddAssemblyOfType(this WorkerInitOptions source, Type type) { - source.AddAssemblies($"{type.Assembly.GetName().Name}.dll"); return source; } /// - /// Registers the neccessary dependencies for injecting or instanciating in the background service. + /// Registers the necessary dependencies for injecting or instantiating in the background service. /// /// /// @@ -197,21 +191,6 @@ public static WorkerInitOptions AddAssemblyOfType(this WorkerInitOptions source, [Obsolete("Manual dependency optimization is silently ignored in this version of BlazorWorker.")] public static WorkerInitOptions AddHttpClient(this WorkerInitOptions source) { -#if NETSTANDARD21 - source.AddAssemblies("System.Net.Http.dll", "System.Net.Http.WebAssemblyHttpHandler.dll"); -#endif - -#if NET5_0_OR_GREATER - source.AddAssemblies( - "System.Net.Http.dll", - "System.Security.Cryptography.X509Certificates.dll", - "System.Net.Primitives.dll", - "System.Net.Requests.dll", - "System.Net.Security.dll", - "System.Net.dll", - "System.Diagnostics.Tracing.dll"); -#endif - return source; }