diff --git a/Directory.Build.props b/Directory.Build.props index e0f7e40..096b2b8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -51,13 +51,13 @@ SOFTWARE. --> net8.0 2.0 2.0.3 - git - https://github.com/s2quake/communication - https://github.com/s2quake/communication/blob/main/LICENSE.md - Copyright (c) 2024 Jeesu Choi - grpc-based communication library for crema project - s2quake - $(FileVersion) + git + https://github.com/s2quake/communication + https://github.com/s2quake/communication/blob/main/LICENSE.md + Copyright (c) 2024 Jeesu Choi + grpc-based communication library for crema project + s2quake + $(FileVersion) https://github.com/s2quake/communication LICENSE.md README.md @@ -67,9 +67,9 @@ SOFTWARE. --> $(MSBuildThisFileDirectory)\ Debug $(Configuration) - true - true - false + true + true + false enable true communication diff --git a/src/JSSoft.Communication/Converters/ArgumentExceptionConverter.cs b/src/JSSoft.Communication/Converters/ArgumentExceptionConverter.cs new file mode 100644 index 0000000..ff60483 --- /dev/null +++ b/src/JSSoft.Communication/Converters/ArgumentExceptionConverter.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ArgumentExceptionConverter : JsonConverter +{ + public const string ParamName = "paramName"; + public const string Message = "message"; + + public override ArgumentException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var props = reader.ReadObject(capacity: 2); + var message = props[Message] ?? throw new JsonException($"{Message} is not found"); + return new ArgumentException(message); + } + + public override void Write( + Utf8JsonWriter writer, ArgumentException value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Message, value.Message); + writer.WriteString(ParamName, value.ParamName); + writer.WriteEndObject(); + } +} diff --git a/src/JSSoft.Communication/Converters/ArgumentNullExceptionConverter.cs b/src/JSSoft.Communication/Converters/ArgumentNullExceptionConverter.cs new file mode 100644 index 0000000..b350b1a --- /dev/null +++ b/src/JSSoft.Communication/Converters/ArgumentNullExceptionConverter.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ArgumentNullExceptionConverter : JsonConverter +{ + public const string ParamName = "paramName"; + public const string Message = "message"; + + public override ArgumentNullException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var props = reader.ReadObject(capacity: 2); + var message = props[Message] ?? throw new JsonException($"{Message} is not found"); + return new ArgumentNullException(message); + } + + public override void Write( + Utf8JsonWriter writer, ArgumentNullException value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(ParamName, value.ParamName); + writer.WriteString(Message, value.Message); + writer.WriteEndObject(); + } +} diff --git a/src/JSSoft.Communication/Converters/ArgumentOutOfRangeExceptionConverter.cs b/src/JSSoft.Communication/Converters/ArgumentOutOfRangeExceptionConverter.cs new file mode 100644 index 0000000..c971803 --- /dev/null +++ b/src/JSSoft.Communication/Converters/ArgumentOutOfRangeExceptionConverter.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ArgumentOutOfRangeExceptionConverter + : JsonConverter +{ + public const string ParamName = "paramName"; + public const string Message = "message"; + + public override ArgumentOutOfRangeException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var props = reader.ReadObject(capacity: 2); + var message = props[Message] ?? throw new JsonException($"{Message} is not found"); + return new ArgumentOutOfRangeException(message); + } + + public override void Write( + Utf8JsonWriter writer, ArgumentOutOfRangeException value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(ParamName, value.ParamName); + writer.WriteString(Message, value.Message); + writer.WriteEndObject(); + } +} diff --git a/src/JSSoft.Communication/Converters/ConverterExtensions.cs b/src/JSSoft.Communication/Converters/ConverterExtensions.cs new file mode 100644 index 0000000..a1b6822 --- /dev/null +++ b/src/JSSoft.Communication/Converters/ConverterExtensions.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System.Collections.Generic; +using System.Text.Json; + +namespace JSSoft.Communication.Converters; + +internal static class ConverterExtensions +{ + public static IDictionary ReadObject( + this ref Utf8JsonReader @this, int capacity) + { + var props = new Dictionary(capacity); + if (@this.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Invalid Json format"); + } + + while (@this.Read()) + { + if (@this.TokenType == JsonTokenType.EndObject) + { + @this.Read(); + return props; + } + + if (@this.TokenType != JsonTokenType.PropertyName) + { + throw new JsonException("Invalid Json format"); + } + + var key = @this.GetString() ?? throw new JsonException("Invalid Json format"); + @this.Read(); + var value = @this.GetString(); + props.Add(key, value); + } + + throw new JsonException("Invalid Json format"); + } +} diff --git a/src/JSSoft.Communication/Converters/ExceptionConverter.cs b/src/JSSoft.Communication/Converters/ExceptionConverter.cs new file mode 100644 index 0000000..ff5f19a --- /dev/null +++ b/src/JSSoft.Communication/Converters/ExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ExceptionConverter : JsonConverter +{ + public override Exception? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new Exception(message) : null; + + public override void Write( + Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/Converters/ExceptionConverterFactory.cs b/src/JSSoft.Communication/Converters/ExceptionConverterFactory.cs new file mode 100644 index 0000000..b2db278 --- /dev/null +++ b/src/JSSoft.Communication/Converters/ExceptionConverterFactory.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ExceptionConverterFactory : JsonConverterFactory +{ + private readonly Dictionary _converterByType = []; + + public override bool CanConvert(Type typeToConvert) + => typeof(Exception).IsAssignableFrom(typeToConvert); + + public override JsonConverter? CreateConverter( + Type typeToConvert, JsonSerializerOptions options) + { + if (_converterByType.TryGetValue(typeToConvert, out var converter) != true) + { + var genericType = typeof(InternalJsonConverter<>).MakeGenericType(typeToConvert); + converter = (JsonConverter)Activator.CreateInstance(genericType)!; + _converterByType.Add(typeToConvert, converter); + } + + return converter; + } + + private sealed class InternalJsonConverter : JsonConverter + where T : Exception + { + public override T? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message + ? (T)Activator.CreateInstance(typeToConvert, message)! + : null; + + public override void Write( + Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); + } +} diff --git a/src/JSSoft.Communication/Converters/IndexOutOfRangeExceptionConverter.cs b/src/JSSoft.Communication/Converters/IndexOutOfRangeExceptionConverter.cs new file mode 100644 index 0000000..f5c201f --- /dev/null +++ b/src/JSSoft.Communication/Converters/IndexOutOfRangeExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class IndexOutOfRangeExceptionConverter : JsonConverter +{ + public override IndexOutOfRangeException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new IndexOutOfRangeException(message) : null; + + public override void Write( + Utf8JsonWriter writer, IndexOutOfRangeException value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/Converters/InvalidOperationExceptionConverter.cs b/src/JSSoft.Communication/Converters/InvalidOperationExceptionConverter.cs new file mode 100644 index 0000000..37489dd --- /dev/null +++ b/src/JSSoft.Communication/Converters/InvalidOperationExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class InvalidOperationExceptionConverter : JsonConverter +{ + public override InvalidOperationException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new InvalidOperationException(message) : null; + + public override void Write( + Utf8JsonWriter writer, InvalidOperationException value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/Converters/NotSupportedExceptionConverter.cs b/src/JSSoft.Communication/Converters/NotSupportedExceptionConverter.cs new file mode 100644 index 0000000..dcd2742 --- /dev/null +++ b/src/JSSoft.Communication/Converters/NotSupportedExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class NotSupportedExceptionConverter : JsonConverter +{ + public override NotSupportedException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new NotSupportedException(message) : null; + + public override void Write( + Utf8JsonWriter writer, NotSupportedException value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/Converters/NullReferenceExceptionConverter.cs b/src/JSSoft.Communication/Converters/NullReferenceExceptionConverter.cs new file mode 100644 index 0000000..c1ad23b --- /dev/null +++ b/src/JSSoft.Communication/Converters/NullReferenceExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class NullReferenceExceptionConverter : JsonConverter +{ + public override NullReferenceException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new NullReferenceException(message) : null; + + public override void Write( + Utf8JsonWriter writer, NullReferenceException value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/Converters/ObjectDisposedExceptionConverter.cs b/src/JSSoft.Communication/Converters/ObjectDisposedExceptionConverter.cs new file mode 100644 index 0000000..e420a4f --- /dev/null +++ b/src/JSSoft.Communication/Converters/ObjectDisposedExceptionConverter.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class ObjectDisposedExceptionConverter : JsonConverter +{ + public const string ObjectName = "objectName"; + public const string Message = "message"; + + public override ObjectDisposedException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var props = reader.ReadObject(capacity: 2); + var objectName = props[ObjectName] ?? throw new JsonException($"{ObjectName} is not found"); + var message = props[Message] ?? throw new JsonException($"{Message} is not found"); + return new ObjectDisposedException(objectName, message); + } + + public override void Write( + Utf8JsonWriter writer, ObjectDisposedException value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(ObjectName, value.ObjectName); + writer.WriteString(Message, value.Message); + writer.WriteEndObject(); + } +} diff --git a/src/JSSoft.Communication/Converters/SystemExceptionConverter.cs b/src/JSSoft.Communication/Converters/SystemExceptionConverter.cs new file mode 100644 index 0000000..b3b67ff --- /dev/null +++ b/src/JSSoft.Communication/Converters/SystemExceptionConverter.cs @@ -0,0 +1,21 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace JSSoft.Communication.Converters; + +internal sealed class SystemExceptionConverter : JsonConverter +{ + public override SystemException? Read( + ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => reader.GetString() is string message ? new SystemException(message) : null; + + public override void Write( + Utf8JsonWriter writer, SystemException value, JsonSerializerOptions options) + => writer.WriteStringValue(value.Message); +} diff --git a/src/JSSoft.Communication/DefaultSerializer.cs b/src/JSSoft.Communication/DefaultSerializer.cs new file mode 100644 index 0000000..9fe955d --- /dev/null +++ b/src/JSSoft.Communication/DefaultSerializer.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using System; +using System.Text.Json; +using JSSoft.Communication.Converters; + +namespace JSSoft.Communication; + +internal sealed class DefaultSerializer : ISerializer +{ + private static readonly JsonSerializerOptions Options = new() + { + IncludeFields = true, + Converters = + { + new ArgumentExceptionConverter(), + new ArgumentNullExceptionConverter(), + new ArgumentOutOfRangeExceptionConverter(), + new ExceptionConverter(), + new IndexOutOfRangeExceptionConverter(), + new InvalidOperationExceptionConverter(), + new ObjectDisposedExceptionConverter(), + new NotSupportedExceptionConverter(), + new NullReferenceExceptionConverter(), + new SystemExceptionConverter(), + new ExceptionConverterFactory(), + }, + }; + + public string Serialize(Type type, object? data) + => JsonSerializer.Serialize(data, type, Options); + + public object? Deserialize(Type type, string text) + => JsonSerializer.Deserialize(text, type, Options); +} diff --git a/src/JSSoft.Communication/JsonSerializerProvider.cs b/src/JSSoft.Communication/DefaultSerializerProvider.cs similarity index 59% rename from src/JSSoft.Communication/JsonSerializerProvider.cs rename to src/JSSoft.Communication/DefaultSerializerProvider.cs index fadb296..5d12e66 100644 --- a/src/JSSoft.Communication/JsonSerializerProvider.cs +++ b/src/JSSoft.Communication/DefaultSerializerProvider.cs @@ -1,18 +1,18 @@ -// +// // Copyright (c) 2024 Jeesu Choi. All Rights Reserved. // Licensed under the MIT License. See LICENSE.md in the project root for license information. // namespace JSSoft.Communication; -internal sealed class JsonSerializerProvider : ISerializerProvider +internal sealed class DefaultSerializerProvider : ISerializerProvider { public const string DefaultName = "json"; - public static readonly JsonSerializerProvider Default = new(); + public static readonly DefaultSerializerProvider Default = new(); public string Name => DefaultName; public ISerializer Create(IServiceContext serviceContext) - => new JsonSerializer(); + => new DefaultSerializer(); } diff --git a/src/JSSoft.Communication/Grpc/AdaptorClient.cs b/src/JSSoft.Communication/Grpc/AdaptorClient.cs index 9de71e1..5270a58 100644 --- a/src/JSSoft.Communication/Grpc/AdaptorClient.cs +++ b/src/JSSoft.Communication/Grpc/AdaptorClient.cs @@ -12,7 +12,6 @@ using Grpc.Core; using JSSoft.Communication.Extensions; using JSSoft.Communication.Logging; -using Newtonsoft.Json; #if NETSTANDARD using GrpcChannel = Grpc.Core.Channel; @@ -448,7 +447,7 @@ private void ThrowException(Type exceptionType, string data) throw new InvalidOperationException("serializer is not set."); } - if (JsonConvert.DeserializeObject(data, exceptionType) is Exception exception) + if (_serializer.Deserialize(exceptionType, data) is Exception exception) { throw exception; } diff --git a/src/JSSoft.Communication/Grpc/AdaptorServer.cs b/src/JSSoft.Communication/Grpc/AdaptorServer.cs index 2895b3e..094b292 100644 --- a/src/JSSoft.Communication/Grpc/AdaptorServer.cs +++ b/src/JSSoft.Communication/Grpc/AdaptorServer.cs @@ -146,7 +146,7 @@ public async Task InvokeAsync(InvokeRequest request, ServerCallCont var reply = new InvokeReply() { ID = string.Empty, - Data = _serializer.Serialize(typeof(void), null), + Data = string.Empty, }; var methodShortName = methodDescriptor.ShortName; @@ -160,7 +160,9 @@ public async Task InvokeAsync(InvokeRequest request, ServerCallCont var reply = new InvokeReply() { ID = result.AssemblyQualifiedName, - Data = _serializer.Serialize(result.ValueType, result.Value), + Data = result.ValueType == typeof(void) + ? string.Empty + : _serializer.Serialize(result.ValueType, result.Value), }; _serviceContext.Debug($"{id} Invoke: {methodDescriptor.Name}"); diff --git a/src/JSSoft.Communication/JSSoft.Communication.csproj b/src/JSSoft.Communication/JSSoft.Communication.csproj index 8f70487..3947925 100644 --- a/src/JSSoft.Communication/JSSoft.Communication.csproj +++ b/src/JSSoft.Communication/JSSoft.Communication.csproj @@ -35,7 +35,6 @@ SOFTWARE. --> runtime; build; native; contentfiles; analyzers; buildtransitive all - \ No newline at end of file diff --git a/src/JSSoft.Communication/JsonSerializer.cs b/src/JSSoft.Communication/JsonSerializer.cs deleted file mode 100644 index 86b66b8..0000000 --- a/src/JSSoft.Communication/JsonSerializer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// -// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. -// Licensed under the MIT License. See LICENSE.md in the project root for license information. -// - -using System; -using Newtonsoft.Json; - -namespace JSSoft.Communication; - -internal sealed class JsonSerializer : ISerializer -{ - private static readonly JsonSerializerSettings Settings = new(); - - public string Serialize(Type type, object? data) - => JsonConvert.SerializeObject(data, type, Settings); - - public object? Deserialize(Type type, string text) - => JsonConvert.DeserializeObject(text, type, Settings); -} diff --git a/src/JSSoft.Communication/ServiceContextBase.cs b/src/JSSoft.Communication/ServiceContextBase.cs index 90f36f4..465edd1 100644 --- a/src/JSSoft.Communication/ServiceContextBase.cs +++ b/src/JSSoft.Communication/ServiceContextBase.cs @@ -51,7 +51,7 @@ protected ServiceContextBase(IService[] services) public virtual IAdaptorProvider AdaptorProvider => Communication.AdaptorProvider.Default; - public virtual ISerializerProvider SerializerProvider => JsonSerializerProvider.Default; + public virtual ISerializerProvider SerializerProvider => DefaultSerializerProvider.Default; public ServiceCollection Services { get; } diff --git a/test/JSSoft.Communication.Tests/Exceptions/SystemExceptionTest.cs b/test/JSSoft.Communication.Tests/Exceptions/SystemExceptionTest.cs new file mode 100644 index 0000000..a76ec85 --- /dev/null +++ b/test/JSSoft.Communication.Tests/Exceptions/SystemExceptionTest.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using Xunit.Abstractions; + +namespace JSSoft.Communication.Tests.Exceptions; + +public sealed class SystemExceptionTest(ITestOutputHelper logger) + : ExceptionTestBase(logger) +{ +} diff --git a/test/JSSoft.Communication.Tests/Exceptions/TestException.cs b/test/JSSoft.Communication.Tests/Exceptions/TestException.cs new file mode 100644 index 0000000..0faab9c --- /dev/null +++ b/test/JSSoft.Communication.Tests/Exceptions/TestException.cs @@ -0,0 +1,18 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +namespace JSSoft.Communication.Tests.Exceptions; + +public sealed class TestException : Exception +{ + public TestException() + { + } + + public TestException(string message) + : base(message) + { + } +} diff --git a/test/JSSoft.Communication.Tests/Exceptions/TestExceptionTest.cs b/test/JSSoft.Communication.Tests/Exceptions/TestExceptionTest.cs new file mode 100644 index 0000000..d32e1ea --- /dev/null +++ b/test/JSSoft.Communication.Tests/Exceptions/TestExceptionTest.cs @@ -0,0 +1,13 @@ +// +// Copyright (c) 2024 Jeesu Choi. All Rights Reserved. +// Licensed under the MIT License. See LICENSE.md in the project root for license information. +// + +using Xunit.Abstractions; + +namespace JSSoft.Communication.Tests.Exceptions; + +public sealed class TestExceptionTest(ITestOutputHelper logger) + : ExceptionTestBase(logger) +{ +}