From c7aebf2e38b5e477fa930125b934137e53288908 Mon Sep 17 00:00:00 2001 From: Russ Cam Date: Thu, 11 Apr 2019 17:13:54 +1000 Subject: [PATCH] Low level client exception structure should match SimpleJson strategy This commit introduces an ExceptionFormatters that serializes exceptions to the same structure as that used in 6.x Fixes #3656 --- .../ElasticsearchNetFormatterResolver.cs | 1 + .../Formatters/ExceptionFormatter.cs | 118 ++++++++++++++++++ .../Resolvers/DynamicObjectResolver.cs | 2 +- .../ExceptionSerializationTests.cs | 71 +++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 src/Elasticsearch.Net/Serialization/Formatters/ExceptionFormatter.cs create mode 100644 src/Tests/Tests/Framework/SerializationTests/ExceptionSerializationTests.cs diff --git a/src/Elasticsearch.Net/Serialization/ElasticsearchNetFormatterResolver.cs b/src/Elasticsearch.Net/Serialization/ElasticsearchNetFormatterResolver.cs index f5b3348bf54..e0487673f64 100644 --- a/src/Elasticsearch.Net/Serialization/ElasticsearchNetFormatterResolver.cs +++ b/src/Elasticsearch.Net/Serialization/ElasticsearchNetFormatterResolver.cs @@ -29,6 +29,7 @@ internal sealed class InnerResolver : IJsonFormatterResolver ElasticsearchNetEnumResolver.Instance, // Specialized Enum handling AttributeFormatterResolver.Instance, // [JsonFormatter] DynamicGenericResolver.Instance, // T[], List, etc... + ExceptionFormatterResolver.Instance }; private readonly IJsonFormatterResolver _finalFormatter; diff --git a/src/Elasticsearch.Net/Serialization/Formatters/ExceptionFormatter.cs b/src/Elasticsearch.Net/Serialization/Formatters/ExceptionFormatter.cs new file mode 100644 index 00000000000..1f9bf4815ac --- /dev/null +++ b/src/Elasticsearch.Net/Serialization/Formatters/ExceptionFormatter.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Runtime.Serialization; + +namespace Elasticsearch.Net +{ + internal class ExceptionFormatterResolver : IJsonFormatterResolver + { + public static ExceptionFormatterResolver Instance = new ExceptionFormatterResolver(); + + private ExceptionFormatterResolver() { } + + private static readonly ExceptionFormatter ExceptionFormatter = new ExceptionFormatter(); + + public IJsonFormatter GetFormatter() + { + if (typeof(Exception).IsAssignableFrom(typeof(T))) + return (IJsonFormatter)ExceptionFormatter; + + return null; + } + } + + internal class ExceptionFormatter : IJsonFormatter + { + private List> FlattenExceptions(Exception e) + { + var maxExceptions = 20; + var exceptions = new List>(maxExceptions); + var depth = 0; + do + { + var o = ToDictionary(e, depth); + exceptions.Add(o); + depth++; + e = e.InnerException; + } while (depth < maxExceptions && e != null); + + return exceptions; + } + + private static Dictionary ToDictionary(Exception e, int depth) + { + var o = new Dictionary(10); + var si = new SerializationInfo(e.GetType(), new FormatterConverter()); + var sc = new StreamingContext(); + e.GetObjectData(si, sc); + + var helpUrl = si.GetString("HelpURL"); + var stackTrace = si.GetString("StackTraceString"); + var remoteStackTrace = si.GetString("RemoteStackTraceString"); + var remoteStackIndex = si.GetInt32("RemoteStackIndex"); + var exceptionMethod = si.GetString("ExceptionMethod"); + var hresult = si.GetInt32("HResult"); + var source = si.GetString("Source"); + var className = si.GetString("ClassName"); + + o.Add("Depth", depth); + o.Add("ClassName", className); + o.Add("Message", e.Message); + o.Add("Source", source); + o.Add("StackTraceString", stackTrace); + o.Add("RemoteStackTraceString", remoteStackTrace); + o.Add("RemoteStackIndex", remoteStackIndex); + o.Add("HResult", hresult); + o.Add("HelpURL", helpUrl); + + WriteStructuredExceptionMethod(o, exceptionMethod); + return o; + } + + private static void WriteStructuredExceptionMethod(Dictionary o, string exceptionMethodString) + { + if (string.IsNullOrWhiteSpace(exceptionMethodString)) return; + + var args = exceptionMethodString.Split('\0', '\n'); + + if (args.Length != 5) return; + + var memberType = int.Parse(args[0], CultureInfo.InvariantCulture); + var name = args[1]; + var assemblyName = args[2]; + var className = args[3]; + var signature = args[4]; + var an = new AssemblyName(assemblyName); + var exceptionMethod = new Dictionary(7) + { + { "Name", name }, + { "AssemblyName", an.Name }, + { "AssemblyVersion", an.Version.ToString() }, + { "AssemblyCulture", an.CultureName }, + { "ClassName", className }, + { "Signature", signature }, + { "MemberType", memberType } + }; + + o.Add("ExceptionMethod", exceptionMethod); + } + + public void Serialize(ref JsonWriter writer, Exception value, IJsonFormatterResolver formatterResolver) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + var flattenedExceptions = FlattenExceptions(value); + var formatter = formatterResolver.GetFormatter>>(); + formatter.Serialize(ref writer, flattenedExceptions, formatterResolver); + } + + public Exception Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver) => + throw new NotSupportedException(); + } +} diff --git a/src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs b/src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs index a1b4c498488..31746ade2a1 100644 --- a/src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs +++ b/src/Elasticsearch.Net/Utf8Json/Resolvers/DynamicObjectResolver.cs @@ -715,7 +715,7 @@ public static object BuildAnonymousFormatter(Type type, Func nam { var ignoreSet = new HashSet(new[] { - "HelpLink", "TargetSite", "HResult", "Data", "ClassName", "InnerException" + "TargetSite", "ClassName", "InnerException" }.Select(x => nameMutator(x))); // special case for exception, modify diff --git a/src/Tests/Tests/Framework/SerializationTests/ExceptionSerializationTests.cs b/src/Tests/Tests/Framework/SerializationTests/ExceptionSerializationTests.cs new file mode 100644 index 00000000000..ac20c5bc808 --- /dev/null +++ b/src/Tests/Tests/Framework/SerializationTests/ExceptionSerializationTests.cs @@ -0,0 +1,71 @@ +using System; +using Elastic.Xunit.XunitPlumbing; +using Elasticsearch.Net; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Tests.Framework.SerializationTests +{ + public class ExceptionSerializationTests + { + private readonly IElasticsearchSerializer _elasticsearchNetSerializer; + + private readonly Exception Exception = new Exception("outer_exception", + new InnerException("inner_exception", + new InnerInnerException("inner_inner_exception"))); + + public ExceptionSerializationTests() + { + var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200")); + var connection = new InMemoryConnection(); + var values = new ConnectionConfiguration(pool, connection); + var lowlevelClient = new ElasticLowLevelClient(values); + _elasticsearchNetSerializer = lowlevelClient.Serializer; + } + + [U] + public void LowLevelExceptionSerializationMatchesJsonNet() + { + var serialized = _elasticsearchNetSerializer.SerializeToString(Exception); + + object CreateException(Type exceptionType, string message, int depth) + { + return new + { + Depth = depth, + ClassName = exceptionType.FullName, + Message = message, + Source = (object)null, + StackTraceString = (object)null, + RemoteStackTraceString = (object)null, + RemoteStackIndex = 0, + HResult = -2146233088, + HelpURL = (object)null + }; + } + + var simpleJsonException = new[] + { + CreateException(typeof(Exception), "outer_exception", 0), + CreateException(typeof(InnerException), "inner_exception", 1), + CreateException(typeof(InnerInnerException), "inner_inner_exception", 2), + }; + + var jArray = JArray.Parse(serialized); + var jArray2 = JArray.Parse(JsonConvert.SerializeObject(simpleJsonException)); + + JToken.DeepEquals(jArray, jArray2).Should().BeTrue(); + } + + public class InnerException : Exception + { + public InnerException(string message, Exception innerException) : base(message, innerException) { } + } + + public class InnerInnerException : Exception + { + public InnerInnerException(string message) : base(message) { } + } + } +}