From 4df972c7a49d38a28a0bf1bb22f4a9091439e66b Mon Sep 17 00:00:00 2001 From: Pilzinsel64 Date: Thu, 15 Aug 2024 09:19:21 +0200 Subject: [PATCH] some work work on api --- Pilz.Extensions/Pilz.Extensions.csproj | 13 ++ .../Reflection/MethodInfoExtensions.cs | 24 +++ Pilz.Networking/Api/ApiServer.cs | 152 ++++++++++++++---- .../Api/DefaultMessageSerializer.cs | 24 ++- Pilz.Networking/Api/IApiServer.cs | 4 +- Pilz.Networking/Api/IMessageSerializer.cs | 2 +- Pilz.Networking/Pilz.Networking.csproj | 4 + Pilz.sln | 10 ++ 8 files changed, 194 insertions(+), 39 deletions(-) create mode 100644 Pilz.Extensions/Pilz.Extensions.csproj create mode 100644 Pilz.Extensions/Reflection/MethodInfoExtensions.cs diff --git a/Pilz.Extensions/Pilz.Extensions.csproj b/Pilz.Extensions/Pilz.Extensions.csproj new file mode 100644 index 0000000..d713480 --- /dev/null +++ b/Pilz.Extensions/Pilz.Extensions.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + 2.0.0 + + + diff --git a/Pilz.Extensions/Reflection/MethodInfoExtensions.cs b/Pilz.Extensions/Reflection/MethodInfoExtensions.cs new file mode 100644 index 0000000..cb2f9d7 --- /dev/null +++ b/Pilz.Extensions/Reflection/MethodInfoExtensions.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace Pilz.Extensions.Reflection; + +public static class MethodInfoExtensions +{ + /// + /// Source: https://stackoverflow.com/questions/940675/getting-a-delegate-from-methodinfo + /// + /// + /// + /// + public static Delegate CreateDelegate(this MethodInfo methodInfo, object? target) + { + var parmTypes = methodInfo.GetParameters().Select(parm => parm.ParameterType); + var parmAndReturnTypes = parmTypes.Append(methodInfo.ReturnType).ToArray(); + var delegateType = Expression.GetDelegateType(parmAndReturnTypes); + + if (methodInfo.IsStatic) + return methodInfo.CreateDelegate(delegateType); + return methodInfo.CreateDelegate(delegateType, target); + } +} diff --git a/Pilz.Networking/Api/ApiServer.cs b/Pilz.Networking/Api/ApiServer.cs index 4b65db1..723c3f7 100644 --- a/Pilz.Networking/Api/ApiServer.cs +++ b/Pilz.Networking/Api/ApiServer.cs @@ -1,49 +1,139 @@ -using System.Reflection; +using Pilz.Extensions.Reflection; +using System; +using System.Net; +using System.Reflection; +using System.Reflection.PortableExecutable; namespace Pilz.Networking.Api; -public class ApiServer : ApiClient, IApiServer +public class ApiServer(string apiUrl) : ApiClient, IApiServer { - protected readonly Dictionary handlers = []; + protected readonly Dictionary handlers = []; protected readonly Dictionary serializers = []; + protected readonly HttpListener httpListener = new(); + + public string ApiUrl { get; } = apiUrl; public IMessageSerializer Serializer { get; set; } = new DefaultMessageSerializer(); - public virtual void RegisterHandler(string url, T instance) where T : class + public virtual void Start() { - var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public); - - foreach (var method in methods) - { - if (method.GetCustomAttribute() is not MessageHandlerAttribute attribute - || method.GetParameters().FirstOrDefault() is not ParameterInfo parameterInfo - || !parameterInfo.ParameterType.IsAssignableTo(typeof(ApiMessage))) - continue; - - - var serializer = Serializer; - if (attribute.Serializer is not null) - { - if (serializers.TryGetValue(attribute.Serializer, out var s) && s is not null) - serializer = s; - else if (Activator.CreateInstance(attribute.Serializer) is IMessageSerializer ss) - { - serializer = ss; - serializers.Add(attribute.Serializer, ss); - } - } - - // ... - } + httpListener.Start(); + Listen(); } - public virtual void RegisterHandler(string url, Delegate handler) + public virtual void RegisterHandler(T instance) where T : class { - // ... + // Get all public instance methods + var methods = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public); + + // Register each method + foreach (var method in methods) + RegisterHandler(method.CreateDelegate(instance)); + } + + public virtual void RegisterHandler(Delegate handler) + { + var method = handler.Method; + + // Sanity checks + if (method.GetCustomAttribute() is not MessageHandlerAttribute attribute + || method.GetParameters().FirstOrDefault() is not ParameterInfo parameter + || !parameter.ParameterType.IsAssignableTo(typeof(ApiMessage)) + || !method.ReturnType.IsAssignableTo(typeof(ApiMessage))) + throw new NotSupportedException("The first parameter needs to be of type ApiMessage and must also return an ApiMessage object and the method must have the MessageHAndlerAttribute."); + + // Add handler + handlers.Add(ApiUrl + attribute.Url, handler); + } + + protected virtual void Listen() + { + while (httpListener.IsListening) + { + var context = httpListener.GetContext(); + + if (context.Request.HttpMethod != HttpMethod.Post.Method + || context.Request.ContentType is not string contentType + || contentType.Contains("application/json") + || context.Request.AcceptTypes is null + || context.Request.AcceptTypes.Contains("application/json")) + { + close(); + continue; + } + + // Parse url + var path = context.Request.Url?.PathAndQuery.Replace(ApiUrl, string.Empty); + if (string.IsNullOrWhiteSpace(path) || context.Request.ContentLength64 <= 0) + { + close(); + continue; + } + + // Read input content + using StreamReader input = new(context.Request.InputStream); + var contentJson = input.ReadToEnd(); + + // Handle message + if (HandleMessage(path, contentJson) is not string resultJson) + { + close(); + continue; + } + + // Set response parameters + context.Response.StatusCode = (int)args.ResponseStatusCode; + context.Response.StatusDescription = args.ResponseStatusDescription; + + // Write response content + context.Response.ContentType = ContentTypes.CONTENT_TYPE_JSON; + using StreamWriter output = new(context.Response.OutputStream); + output.Write(resultJson); + + close(); + void close() => context.Response.OutputStream.Close(); + } } protected virtual string? HandleMessage(string url, string json) { - // ... + // Get handler + if (!handlers.TryGetValue(url, out var handler) + || handler.Method.GetCustomAttribute() is not MessageHandlerAttribute attribute) + return null; + + // Get required infos + var targetType = handler.Method.GetParameters().First().ParameterType; + var serializer = GetSerializer(attribute.Serializer); + + // Deserialize + if (serializer.Deserialize(json, targetType) is not ApiMessage message) + return null; + + // Invoke handler + if (handler.DynamicInvoke(message) is not ApiMessage result) + return null; + + // Serializer + if (serializer.Serialize(result) is not string resultStr) + return null; + + return resultStr; + } + + protected virtual IMessageSerializer GetSerializer(Type? t) + { + if (t is not null) + { + if (serializers.TryGetValue(t, out var s) && s is not null) + return s; + else if (Activator.CreateInstance(t) is IMessageSerializer ss) + { + serializers.Add(t, ss); + return ss; + } + } + return Serializer; } } diff --git a/Pilz.Networking/Api/DefaultMessageSerializer.cs b/Pilz.Networking/Api/DefaultMessageSerializer.cs index df98251..20ad3be 100644 --- a/Pilz.Networking/Api/DefaultMessageSerializer.cs +++ b/Pilz.Networking/Api/DefaultMessageSerializer.cs @@ -1,14 +1,28 @@ -namespace Pilz.Networking.Api; +using Newtonsoft.Json; + +namespace Pilz.Networking.Api; public class DefaultMessageSerializer : IMessageSerializer { - public ApiMessage? Deserialize(string json) + private static JsonSerializerSettings? defaultSerializerSettings; + + public JsonSerializerSettings DefaultSerializerSettings => defaultSerializerSettings ??= CreateDefaultSerializerSettings(); + + protected virtual JsonSerializerSettings CreateDefaultSerializerSettings() { - // ... + return new JsonSerializerSettings + { + TypeNameHandling = TypeNameHandling.Auto, + }; } - public string? Serialize(ApiMessage message) + public virtual string? Serialize(ApiMessage message) { - // ... + return JsonConvert.SerializeObject(message, DefaultSerializerSettings); + } + + public virtual ApiMessage? Deserialize(string json, Type target) + { + return JsonConvert.DeserializeObject(json, target, DefaultSerializerSettings) as ApiMessage; } } diff --git a/Pilz.Networking/Api/IApiServer.cs b/Pilz.Networking/Api/IApiServer.cs index dfdc4b6..4ebfa4e 100644 --- a/Pilz.Networking/Api/IApiServer.cs +++ b/Pilz.Networking/Api/IApiServer.cs @@ -2,6 +2,6 @@ public interface IApiServer : IApiClient { - void RegisterHandler(string url, T instance) where T : class; - void RegisterHandler(string url, Delegate handler); + void RegisterHandler(T instance) where T : class; + void RegisterHandler(Delegate handler); } diff --git a/Pilz.Networking/Api/IMessageSerializer.cs b/Pilz.Networking/Api/IMessageSerializer.cs index 84597fa..84eda48 100644 --- a/Pilz.Networking/Api/IMessageSerializer.cs +++ b/Pilz.Networking/Api/IMessageSerializer.cs @@ -3,5 +3,5 @@ public interface IMessageSerializer { string? Serialize(ApiMessage message); - ApiMessage? Deserialize(string json); + ApiMessage? Deserialize(string json, Type target); } diff --git a/Pilz.Networking/Pilz.Networking.csproj b/Pilz.Networking/Pilz.Networking.csproj index cad8bb1..d33d5b4 100644 --- a/Pilz.Networking/Pilz.Networking.csproj +++ b/Pilz.Networking/Pilz.Networking.csproj @@ -15,4 +15,8 @@ + + + + \ No newline at end of file diff --git a/Pilz.sln b/Pilz.sln index ebbb460..1846eca 100644 --- a/Pilz.sln +++ b/Pilz.sln @@ -43,6 +43,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.Plugins.Advanced.UI", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pilz.Plugins.Advanced.UI.Telerik", "Pilz.Plugins.Advanced.UI.Telerik\Pilz.Plugins.Advanced.UI.Telerik.csproj", "{0A837BD6-A19C-4A05-A57D-CBB0CD64B244}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Pilz.Extensions", "Pilz.Extensions\Pilz.Extensions.csproj", "{63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -211,6 +213,14 @@ Global {0A837BD6-A19C-4A05-A57D-CBB0CD64B244}.Release|Any CPU.Build.0 = Release|Any CPU {0A837BD6-A19C-4A05-A57D-CBB0CD64B244}.Release|x86.ActiveCfg = Release|Any CPU {0A837BD6-A19C-4A05-A57D-CBB0CD64B244}.Release|x86.Build.0 = Release|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Debug|x86.ActiveCfg = Debug|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Debug|x86.Build.0 = Debug|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Release|Any CPU.Build.0 = Release|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Release|x86.ActiveCfg = Release|Any CPU + {63DA7581-F35E-4EDD-BEAE-01E281B0BDC3}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE