diff --git a/src/Exceptionless/Extensions/StringExtensions.cs b/src/Exceptionless/Extensions/StringExtensions.cs index dcbd0e77..e436df1c 100644 --- a/src/Exceptionless/Extensions/StringExtensions.cs +++ b/src/Exceptionless/Extensions/StringExtensions.cs @@ -5,23 +5,6 @@ namespace Exceptionless.Extensions { public static class StringExtensions { - internal static string ToLowerUnderscoredWords(this string value) { - var builder = new StringBuilder(value.Length + 10); - for (int index = 0; index < value.Length; index++) { - char c = value[index]; - if (Char.IsUpper(c)) { - if (index > 0 && value[index - 1] != '_') - builder.Append('_'); - - builder.Append(Char.ToLower(c)); - } else { - builder.Append(c); - } - } - - return builder.ToString(); - } - public static bool AnyWildcardMatches(this string value, IEnumerable patternsToMatch, bool ignoreCase = true) { if (patternsToMatch == null || value == null) return false; diff --git a/src/Exceptionless/Models/Client/ClientConfiguration.cs b/src/Exceptionless/Models/Client/ClientConfiguration.cs index e0bf547a..c93cfdc6 100644 --- a/src/Exceptionless/Models/Client/ClientConfiguration.cs +++ b/src/Exceptionless/Models/Client/ClientConfiguration.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class ClientConfiguration { public ClientConfiguration() { Settings = new SettingsDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/EnvironmentInfo.cs b/src/Exceptionless/Models/Client/Data/EnvironmentInfo.cs index 0db19ef2..897e1d1d 100644 --- a/src/Exceptionless/Models/Client/Data/EnvironmentInfo.cs +++ b/src/Exceptionless/Models/Client/Data/EnvironmentInfo.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class EnvironmentInfo : IData { public EnvironmentInfo() { Data = new DataDictionary(); @@ -66,11 +67,13 @@ public EnvironmentInfo() { /// /// The OS name that the error occurred on. /// + [Json.JsonProperty("o_s_name")] public string OSName { get; set; } /// /// The OS version that the error occurred on. /// + [Json.JsonProperty("o_s_version")] public string OSVersion { get; set; } /// diff --git a/src/Exceptionless/Models/Client/Data/Error.cs b/src/Exceptionless/Models/Client/Data/Error.cs index b559f16e..152bb01e 100644 --- a/src/Exceptionless/Models/Client/Data/Error.cs +++ b/src/Exceptionless/Models/Client/Data/Error.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Error : InnerError { public Error() { Modules = new ModuleCollection(); diff --git a/src/Exceptionless/Models/Client/Data/InnerError.cs b/src/Exceptionless/Models/Client/Data/InnerError.cs index 33850149..e58c8e96 100644 --- a/src/Exceptionless/Models/Client/Data/InnerError.cs +++ b/src/Exceptionless/Models/Client/Data/InnerError.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class InnerError : IData { public InnerError() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/ManualStackingInfo.cs b/src/Exceptionless/Models/Client/Data/ManualStackingInfo.cs index 84f99577..ddee5d5f 100644 --- a/src/Exceptionless/Models/Client/Data/ManualStackingInfo.cs +++ b/src/Exceptionless/Models/Client/Data/ManualStackingInfo.cs @@ -3,6 +3,7 @@ using Exceptionless.Extensions; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class ManualStackingInfo { public ManualStackingInfo() { SignatureData = new Dictionary(); diff --git a/src/Exceptionless/Models/Client/Data/Method.cs b/src/Exceptionless/Models/Client/Data/Method.cs index c338b788..6cdccac3 100644 --- a/src/Exceptionless/Models/Client/Data/Method.cs +++ b/src/Exceptionless/Models/Client/Data/Method.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Method : IData { public Method() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/Module.cs b/src/Exceptionless/Models/Client/Data/Module.cs index 3139f4d2..2a7f56b2 100644 --- a/src/Exceptionless/Models/Client/Data/Module.cs +++ b/src/Exceptionless/Models/Client/Data/Module.cs @@ -2,6 +2,7 @@ using System.Text; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Module : IData { public Module() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/Parameter.cs b/src/Exceptionless/Models/Client/Data/Parameter.cs index 6ed7fe87..dc76a882 100644 --- a/src/Exceptionless/Models/Client/Data/Parameter.cs +++ b/src/Exceptionless/Models/Client/Data/Parameter.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Parameter : IData { public Parameter() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/RequestInfo.cs b/src/Exceptionless/Models/Client/Data/RequestInfo.cs index 41cd9929..d003e5b7 100644 --- a/src/Exceptionless/Models/Client/Data/RequestInfo.cs +++ b/src/Exceptionless/Models/Client/Data/RequestInfo.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class RequestInfo : IData { public RequestInfo() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/SimpleError.cs b/src/Exceptionless/Models/Client/Data/SimpleError.cs index 24e78e6a..592a3c8f 100644 --- a/src/Exceptionless/Models/Client/Data/SimpleError.cs +++ b/src/Exceptionless/Models/Client/Data/SimpleError.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class SimpleError : SimpleInnerError { public SimpleError() { Modules = new ModuleCollection(); diff --git a/src/Exceptionless/Models/Client/Data/SimpleInnerError.cs b/src/Exceptionless/Models/Client/Data/SimpleInnerError.cs index 05138ee3..3fce4134 100644 --- a/src/Exceptionless/Models/Client/Data/SimpleInnerError.cs +++ b/src/Exceptionless/Models/Client/Data/SimpleInnerError.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class SimpleInnerError : IData { public SimpleInnerError() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/StackFrame.cs b/src/Exceptionless/Models/Client/Data/StackFrame.cs index e69231d9..279485e0 100644 --- a/src/Exceptionless/Models/Client/Data/StackFrame.cs +++ b/src/Exceptionless/Models/Client/Data/StackFrame.cs @@ -1,4 +1,5 @@ namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class StackFrame : Method { public string FileName { get; set; } public int LineNumber { get; set; } diff --git a/src/Exceptionless/Models/Client/Data/UserDescription.cs b/src/Exceptionless/Models/Client/Data/UserDescription.cs index b11a45e6..efd3c47d 100644 --- a/src/Exceptionless/Models/Client/Data/UserDescription.cs +++ b/src/Exceptionless/Models/Client/Data/UserDescription.cs @@ -1,6 +1,7 @@ using System; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class UserDescription : IData { public UserDescription() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Data/UserInfo.cs b/src/Exceptionless/Models/Client/Data/UserInfo.cs index e45e7ad4..51abbd60 100644 --- a/src/Exceptionless/Models/Client/Data/UserInfo.cs +++ b/src/Exceptionless/Models/Client/Data/UserInfo.cs @@ -1,6 +1,7 @@ using System; namespace Exceptionless.Models.Data { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class UserInfo : IData { public UserInfo() { Data = new DataDictionary(); diff --git a/src/Exceptionless/Models/Client/Event.cs b/src/Exceptionless/Models/Client/Event.cs index 7e6e02ad..31d233d9 100644 --- a/src/Exceptionless/Models/Client/Event.cs +++ b/src/Exceptionless/Models/Client/Event.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; namespace Exceptionless.Models { + [Json.JsonObject(NamingStrategyType = typeof(Json.Serialization.SnakeCaseNamingStrategy))] public class Event : IData { public Event() { Tags = new TagSet(); diff --git a/src/Exceptionless/Serializer/DataDictionaryConverter.cs b/src/Exceptionless/Serializer/DataDictionaryConverter.cs index 6168cec6..8354d607 100644 --- a/src/Exceptionless/Serializer/DataDictionaryConverter.cs +++ b/src/Exceptionless/Serializer/DataDictionaryConverter.cs @@ -18,10 +18,10 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist var dictionary = new DataDictionary(); foreach (string key in result.Keys) { object value = result[key]; - if (value is JObject) - dictionary[key] = value.ToString(); - else if (value is JArray) - dictionary[key] = value.ToString(); + if (value is JObject jObject) + dictionary[key] = jObject.ToString(serializer.Formatting); + else if (value is JArray jArray) + dictionary[key] = jArray.ToString(serializer.Formatting); else dictionary[key] = value; } diff --git a/src/Exceptionless/Serializer/DefaultJsonSerializer.cs b/src/Exceptionless/Serializer/DefaultJsonSerializer.cs index e957b4ef..18944b91 100644 --- a/src/Exceptionless/Serializer/DefaultJsonSerializer.cs +++ b/src/Exceptionless/Serializer/DefaultJsonSerializer.cs @@ -18,6 +18,7 @@ public DefaultJsonSerializer() { _serializerSettings = new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, PreserveReferencesHandling = PreserveReferencesHandling.None, + FloatParseHandling = FloatParseHandling.Decimal, ContractResolver = new ExceptionlessContractResolver() }; @@ -48,13 +49,11 @@ public virtual string Serialize(object model, string[] exclusions = null, int ma using (var sw = new StringWriter()) { using (var jw = new JsonTextWriterWithExclusions(sw, exclusions)) { - jw.Formatting = Formatting.None; Func include = (property, value) => ShouldSerialize(jw, property, value, maxDepth, exclusions); - var resolver = new ExceptionlessContractResolver(include); - serializer.ContractResolver = resolver; + serializer.ContractResolver = new ExceptionlessContractResolver(include); if (continueOnSerializationError) serializer.Error += (sender, args) => { args.ErrorContext.Handled = true; }; - + serializer.Serialize(jw, model); } @@ -78,26 +77,6 @@ private bool ShouldSerialize(JsonTextWriterWithDepth jw, JsonProperty property, bool isPastMaxDepth = !(isPrimitiveType ? jw.CurrentDepth <= maxDepth : jw.CurrentDepth < maxDepth); if (isPastMaxDepth) return false; - - if (isPrimitiveType) - return true; - - object value = property.ValueProvider.GetValue(obj); - if (value == null) - return true; - - if (typeof(ICollection).GetTypeInfo().IsAssignableFrom(property.PropertyType.GetTypeInfo())) { - var collection = value as ICollection; - if (collection != null) - return collection.Count > 0; - } - - var collectionType = value.GetType().GetInterfaces().FirstOrDefault(i => i.GetTypeInfo().IsGenericType && i.GetGenericTypeDefinition() == typeof(ICollection<>)); - if (collectionType != null) { - var countProperty = collectionType.GetProperty("Count"); - if (countProperty != null) - return (int)countProperty.GetValue(value, null) > 0; - } } catch (Exception) {} return true; diff --git a/src/Exceptionless/Serializer/ExceptionlessContractResolver.cs b/src/Exceptionless/Serializer/ExceptionlessContractResolver.cs index 09d87e9c..53934ad5 100644 --- a/src/Exceptionless/Serializer/ExceptionlessContractResolver.cs +++ b/src/Exceptionless/Serializer/ExceptionlessContractResolver.cs @@ -2,7 +2,6 @@ using System.Reflection; using Exceptionless.Json; using Exceptionless.Json.Serialization; -using Exceptionless.Extensions; namespace Exceptionless.Serializer { internal class ExceptionlessContractResolver : DefaultContractResolver { @@ -21,15 +20,5 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ property.ShouldSerialize = obj => _includeProperty(property, obj) && (shouldSerialize == null || shouldSerialize(obj)); return property; } - - protected override JsonDictionaryContract CreateDictionaryContract(Type objectType) { - var contract = base.CreateDictionaryContract(objectType); - contract.DictionaryKeyResolver = propertyName => propertyName; - return contract; - } - - protected override string ResolvePropertyName(string propertyName) { - return propertyName.ToLowerUnderscoredWords(); - } } } \ No newline at end of file diff --git a/src/Exceptionless/Serializer/JsonTextWriterWithExclusions.cs b/src/Exceptionless/Serializer/JsonTextWriterWithExclusions.cs index 7c969634..3748847f 100644 --- a/src/Exceptionless/Serializer/JsonTextWriterWithExclusions.cs +++ b/src/Exceptionless/Serializer/JsonTextWriterWithExclusions.cs @@ -12,9 +12,7 @@ public JsonTextWriterWithExclusions(TextWriter textWriter, string[] excludedProp public override bool ShouldWriteProperty(string name) { var exclusions = _excludedPropertyNames; - return exclusions is null || - exclusions.Length == 0 || - !name.AnyWildcardMatches(exclusions, ignoreCase: true); + return exclusions == null || exclusions.Length == 0 || !name.AnyWildcardMatches(exclusions, ignoreCase: true); } } } \ No newline at end of file diff --git a/test/Exceptionless.Tests/Configuration/DataExclusionTests.cs b/test/Exceptionless.Tests/Configuration/DataExclusionTests.cs index 4b77968c..f98fd6be 100644 --- a/test/Exceptionless.Tests/Configuration/DataExclusionTests.cs +++ b/test/Exceptionless.Tests/Configuration/DataExclusionTests.cs @@ -67,7 +67,7 @@ public void CanHandleObject() { var ev = new Event(); ev.SetProperty(nameof(order), order, excludedPropertyNames: new [] { nameof(order.CardLast4) }); Assert.Single(ev.Data); - Assert.Equal("{\"id\":\"1234\",\"data\":{}}", ev.Data.GetString(nameof(order))); + Assert.Equal("{\"Id\":\"1234\",\"Data\":{}}", ev.Data.GetString(nameof(order))); } [InlineData("Credit*", true)] diff --git a/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs b/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs index 68dd7bc6..e33a81fa 100644 --- a/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/JsonSerializerTests.cs @@ -6,11 +6,13 @@ using Xunit; using Exceptionless.Extensions; using Exceptionless.Models; +using Exceptionless.Models.Data; using Exceptionless.Serializer; using Exceptionless.Tests.Log; using Exceptionless.Tests.Utility; using Xunit.Abstractions; using LogLevel = Exceptionless.Logging.LogLevel; +using Module = Exceptionless.Models.Data.Module; namespace Exceptionless.Tests.Serializer { public class JsonSerializerTests { @@ -24,32 +26,324 @@ protected virtual IJsonSerializer GetSerializer() { } [Fact] - public void CanSerializeEvent() { + public void Serialize_Event_IsValidJSON() { + // Arrange var ev = new Event { - Date = DateTime.Now, - Message = "Testing" + Type = Event.KnownTypes.Log, + Source = "SampleApp", + Date = new DateTimeOffset(2023, 5, 2, 14, 30, 0, TimeSpan.Zero), + Tags = { Event.KnownTags.Critical, "tag2" }, + Message = "An error occurred", + Geo = "40.7128,-74.0060", + Value = 42.0m, + Count = 2, + Data = { + ["FirstName"] = "Blake", + [Event.KnownDataKeys.Level] = "Warn", + [Event.KnownDataKeys.TraceLog] = new List { "log 1" }, + [Event.KnownDataKeys.UserDescription] = new UserDescription { + EmailAddress = "test@example.com", + Description = "Test user description" + } + }, + ReferenceId = "ref123" + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(ev); + + // Assert + Assert.Equal("{\"type\":\"log\",\"source\":\"SampleApp\",\"date\":\"2023-05-02T14:30:00+00:00\",\"tags\":[\"Critical\",\"tag2\"],\"message\":\"An error occurred\",\"geo\":\"40.7128,-74.0060\",\"value\":42.0,\"count\":2,\"data\":{\"FirstName\":\"Blake\",\"@level\":\"Warn\",\"@trace\":[\"log 1\"],\"@user_description\":{\"email_address\":\"test@example.com\",\"description\":\"Test user description\",\"data\":{}}},\"reference_id\":\"ref123\"}", json); + } + + [Fact] + public void Serialize_EnvironmentInfo_IsValidJSON() { + // Arrange + var environmentInfo = new EnvironmentInfo { + ProcessorCount = 4, + TotalPhysicalMemory = 8192, + AvailablePhysicalMemory = 4096, + CommandLine = "TestCommandLine", + ProcessName = "TestProcess", + ProcessId = "12345", + ProcessMemorySize = 2048, + ThreadName = "Thread", + ThreadId = "67890", + Architecture = "x64", + OSName = "Windows", + OSVersion = "10.0.19042", + IpAddress = "192.168.1.1", + MachineName = "Machine", + InstallId = "InstallId", + RuntimeVersion = "5.0.0", + Data = { ["FrameworkDescription"] = "Test" } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(environmentInfo); + + // Assert + Assert.Equal("{\"processor_count\":4,\"total_physical_memory\":8192,\"available_physical_memory\":4096,\"command_line\":\"TestCommandLine\",\"process_name\":\"TestProcess\",\"process_id\":\"12345\",\"process_memory_size\":2048,\"thread_name\":\"Thread\",\"thread_id\":\"67890\",\"architecture\":\"x64\",\"o_s_name\":\"Windows\",\"o_s_version\":\"10.0.19042\",\"ip_address\":\"192.168.1.1\",\"machine_name\":\"Machine\",\"install_id\":\"InstallId\",\"runtime_version\":\"5.0.0\",\"data\":{\"FrameworkDescription\":\"Test\"}}", json); + } + + [Fact] + public void Serialize_Error_IsValidJSON() { + // Arrange + var error = new Error { + Message = "Test error message", + Type = "System.Exception", + Code = "1001", + Data = { + [Error.KnownDataKeys.ExtraProperties] = new { OrderNumber = 10 } + }, + Inner = new InnerError { + Message = "Inner error message", + Type = "System.ArgumentException", + Code = "2002", + StackTrace = new StackFrameCollection + { + new StackFrame { Name = "InnerMethodName", LineNumber = 20 } + } + }, + StackTrace = new StackFrameCollection + { + new StackFrame + { + FileName = "TestFile.cs", + LineNumber = 20, + Column = 5, + IsSignatureTarget = true, + DeclaringNamespace = "TestNamespace", + DeclaringType = "TestClass", + Name = "InnerMethodName", + ModuleId = 1, + Data = { ["StackFrameKey"] = "StackFrameValue" }, + GenericArguments = new GenericArguments { "T" }, + Parameters = new ParameterCollection + { + new Parameter + { + Name = "param1", + Type = "System.String", + TypeNamespace = "System", + Data = { ["ParameterKey"] = "ParameterValue" }, + GenericArguments = new GenericArguments { "U" } + } + } + } + }, + Modules = new ModuleCollection + { + new Module + { + ModuleId = 1, + Name = "TestModule", + Version = "1.0.0", + IsEntry = true, + CreatedDate = new DateTime(2023, 5, 1, 12, 0, 0, DateTimeKind.Utc), + ModifiedDate = new DateTime(2023, 5, 2, 12, 0, 0, DateTimeKind.Utc), + Data = { ["PublicKeyToken"] = "b03f5f7f11d50a3a" } + } + } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(error); + + // Assert + Assert.Equal("{\"modules\":[{\"module_id\":1,\"name\":\"TestModule\",\"version\":\"1.0.0\",\"is_entry\":true,\"created_date\":\"2023-05-01T12:00:00Z\",\"modified_date\":\"2023-05-02T12:00:00Z\",\"data\":{\"PublicKeyToken\":\"b03f5f7f11d50a3a\"}}],\"message\":\"Test error message\",\"type\":\"System.Exception\",\"code\":\"1001\",\"data\":{\"@ext\":{\"OrderNumber\":10}},\"inner\":{\"message\":\"Inner error message\",\"type\":\"System.ArgumentException\",\"code\":\"2002\",\"data\":{},\"inner\":null,\"stack_trace\":[{\"file_name\":null,\"line_number\":20,\"column\":0,\"is_signature_target\":false,\"declaring_namespace\":null,\"declaring_type\":null,\"name\":\"InnerMethodName\",\"module_id\":0,\"data\":{},\"generic_arguments\":[],\"parameters\":[]}],\"target_method\":null},\"stack_trace\":[{\"file_name\":\"TestFile.cs\",\"line_number\":20,\"column\":5,\"is_signature_target\":true,\"declaring_namespace\":\"TestNamespace\",\"declaring_type\":\"TestClass\",\"name\":\"InnerMethodName\",\"module_id\":1,\"data\":{\"StackFrameKey\":\"StackFrameValue\"},\"generic_arguments\":[\"T\"],\"parameters\":[{\"name\":\"param1\",\"type\":\"System.String\",\"type_namespace\":\"System\",\"data\":{\"ParameterKey\":\"ParameterValue\"},\"generic_arguments\":[\"U\"]}]}],\"target_method\":null}", json); + } + + [Fact] + public void Serialize_ManualStackingInfo_IsValidJSON() { + // Arrange + var manualStackingInfo = new ManualStackingInfo { + Title = "Test Title", + SignatureData = new Dictionary + { + { "Key1", "Value1" }, + { "Key2", "Value2" } + } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(manualStackingInfo); + + // Assert + Assert.Equal("{\"title\":\"Test Title\",\"signature_data\":{\"Key1\":\"Value1\",\"Key2\":\"Value2\"}}", json); + } + + [Fact] + public void Serialize_RequestInfo_IsValidJSON() { + // Arrange + var requestInfo = new RequestInfo { + UserAgent = "Mozilla/5.0", + HttpMethod = "GET", + IsSecure = true, + Host = "www.example.com", + Port = 443, + Path = "/test", + Referrer = "https://www.google.com", + ClientIpAddress = "192.168.1.1", + Headers = new Dictionary + { + { "Content-Type", new[] { "application/json" } } + }, + Cookies = new Dictionary + { + { "session", "abc123" } + }, + QueryString = new Dictionary + { + { "q", "test" } + }, + Data = + { + [RequestInfo.KnownDataKeys.Browser] = "Mozilla Firefox", + [RequestInfo.KnownDataKeys.BrowserVersion] = "97.0", + [RequestInfo.KnownDataKeys.BrowserMajorVersion] = "97", + [RequestInfo.KnownDataKeys.Device] = "Desktop", + [RequestInfo.KnownDataKeys.OS] = "Windows", + [RequestInfo.KnownDataKeys.OSVersion] = "10.0", + [RequestInfo.KnownDataKeys.OSMajorVersion] = "10", + [RequestInfo.KnownDataKeys.IsBot] = "False" + } }; - ev.Data["FirstName"] = "Blake"; - var exclusions = new[] { nameof(Event.Type), nameof(Event.Source), "Date", nameof(Event.Geo), nameof(Event.Count), nameof(Event.ReferenceId), nameof(Event.Tags), nameof(Event.Value) }; var serializer = GetSerializer(); - string json = serializer.Serialize(ev, exclusions); - Assert.Equal(@"{""message"":""Testing"",""data"":{""FirstName"":""Blake""}}", json); + + // Act + string json = serializer.Serialize(requestInfo); + + // Assert + Assert.Equal("{\"user_agent\":\"Mozilla/5.0\",\"http_method\":\"GET\",\"is_secure\":true,\"host\":\"www.example.com\",\"port\":443,\"path\":\"/test\",\"referrer\":\"https://www.google.com\",\"client_ip_address\":\"192.168.1.1\",\"headers\":{\"Content-Type\":[\"application/json\"]},\"cookies\":{\"session\":\"abc123\"},\"post_data\":null,\"query_string\":{\"q\":\"test\"},\"data\":{\"@browser\":\"Mozilla Firefox\",\"@browser_version\":\"97.0\",\"@browser_major_version\":\"97\",\"@device\":\"Desktop\",\"@os\":\"Windows\",\"@os_version\":\"10.0\",\"@os_major_version\":\"10\",\"@is_bot\":\"False\"}}", json); } [Fact] - public void CanExcludeProperties() { + public void Serialize_SimpleError_IsValidJSON() { + // Arrange + var simpleError = new SimpleError { + Message = "Test error message", + Type = "System.Exception", + StackTrace = "at TestClass.TestMethod()", + Data = + { + [SimpleError.KnownDataKeys.ExtraProperties] = new { OrderNumber = 10 } + }, + Inner = new SimpleInnerError { + Message = "Inner error message", + Type = "System.NullReferenceException", + StackTrace = "at InnerTestClass.InnerTestMethod()" + }, + Modules = new ModuleCollection + { + new Module + { + ModuleId = 1, + Name = "TestModule", + Version = "1.0.0", + IsEntry = true, + CreatedDate = new DateTime(2023, 5, 1, 12, 0, 0, DateTimeKind.Utc), + ModifiedDate = new DateTime(2023, 5, 2, 12, 0, 0, DateTimeKind.Utc), + Data = { ["PublicKeyToken"] = "b77a5c561934e089" } + } + } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(simpleError); + + // Assert + Assert.Equal("{\"modules\":[{\"module_id\":1,\"name\":\"TestModule\",\"version\":\"1.0.0\",\"is_entry\":true,\"created_date\":\"2023-05-01T12:00:00Z\",\"modified_date\":\"2023-05-02T12:00:00Z\",\"data\":{\"PublicKeyToken\":\"b77a5c561934e089\"}}],\"message\":\"Test error message\",\"type\":\"System.Exception\",\"stack_trace\":\"at TestClass.TestMethod()\",\"data\":{\"@ext\":{\"OrderNumber\":10}},\"inner\":{\"message\":\"Inner error message\",\"type\":\"System.NullReferenceException\",\"stack_trace\":\"at InnerTestClass.InnerTestMethod()\",\"data\":{},\"inner\":null}}", json); + } + + [Fact] + public void Serialize_UserDescription_IsValidJSON() { + // Arrange + var userDescription = new UserDescription { + EmailAddress = "test@example.com", + Description = "Test user description" + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(userDescription); + + // Assert + Assert.Equal("{\"email_address\":\"test@example.com\",\"description\":\"Test user description\",\"data\":{}}", json); + } + + [Fact] + public void Serialize_UserInfo_IsValidJSON() { + // Arrange + var userInfo = new UserInfo("123", "John Doe") { + Data = { + { "Age", 30 }, + { "City", "New York" } + } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(userInfo); + + // Assert + Assert.Equal("{\"identity\":\"123\",\"name\":\"John Doe\",\"data\":{\"Age\":30,\"City\":\"New York\"}}", json); + } + + + [Fact] + public void Serialize_ClientConfiguration_IsValidJSON() { + // Arrange + var clientConfiguration = new ClientConfiguration { + Version = 1, + Settings = + { + { "@@log:*", "Off" } + } + }; + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(clientConfiguration); + + // Assert + Assert.Equal("{\"version\":1,\"settings\":{\"@@log:*\":\"Off\"}}", json); + } + + + [Fact] + public void Serialize_ModelWithExclusions_ShouldExcludeProperties() { + // Arrange var data = new SampleModel { Date = DateTime.Now, Message = "Testing" }; var serializer = GetSerializer(); - string json = serializer.Serialize(data, new[] { nameof(SampleModel.Date), nameof(SampleModel.Number), nameof(SampleModel.Bool), nameof(SampleModel.DateOffset), nameof(SampleModel.Collection), nameof(SampleModel.Dictionary), nameof(SampleModel.Nested) }); - Assert.Equal(@"{""message"":""Testing""}", json); + + // Act + string json = serializer.Serialize(data, new[] { nameof(SampleModel.Date), nameof(SampleModel.Number), nameof(SampleModel.Rating), nameof(SampleModel.Bool), nameof(SampleModel.DateOffset), nameof(SampleModel.Direction), nameof(SampleModel.Collection), nameof(SampleModel.Dictionary), nameof(SampleModel.Nested) }); + + // Assert + Assert.Equal("{\"Message\":\"Testing\"}", json); } [Fact] - public void CanExcludeNestedProperties() { + public void Serialize_ModelWithNestedExclusions_WillExcludeNestedProperties() { + // Arrange var data = new NestedModel { Number = 1, Message = "Testing", @@ -60,20 +354,30 @@ public void CanExcludeNestedProperties() { }; var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(data, new[] { nameof(NestedModel.Number) }); - Assert.Equal(@"{""message"":""Testing"",""nested"":{""message"":""Nested"",""nested"":null}}", json); + + // Assert + Assert.Equal("{\"Message\":\"Testing\",\"Nested\":{\"Message\":\"Nested\",\"Nested\":null}}", json); } [Fact] - public void ShouldIncludeNullObjects() { + public void Serialize_ModelWithNullValues_ShouldIncludeNullObjects() { + // Arrange var data = new DefaultsModel(); var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(data); - Assert.Equal(@"{""number"":0,""bool"":false,""message"":null,""collection"":null,""dictionary"":null}", json); + + // Assert + Assert.Equal("{\"Number\":0,\"Bool\":false,\"Message\":null,\"Collection\":null,\"Dictionary\":null,\"DataDictionary\":null}", json); } [Fact] - public void CanExcludeMultiwordProperties() { + public void Serialize_ModelWithComplexPropertyNames_ShouldExcludeMultiWordProperties() { + // Arrange var user = new User { FirstName = "John", LastName = "Doe", @@ -86,18 +390,28 @@ public void CanExcludeMultiwordProperties() { } }; - var exclusions = new[] { nameof(user.PasswordHash), nameof(user.Billing.CardNumberRedacted), nameof(user.Billing.EncryptedCardNumber) }; + string[] exclusions = new[] { nameof(user.PasswordHash), nameof(user.Billing.CardNumberRedacted), nameof(user.Billing.EncryptedCardNumber) }; var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(user, exclusions, maxDepth: 2); - Assert.Equal(@"{""first_name"":""John"",""last_name"":""Doe"",""billing"":{""expiration_month"":10,""expiration_year"":2020}}", json); + + // Assert + Assert.Equal("{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Billing\":{\"ExpirationMonth\":10,\"ExpirationYear\":2020}}", json); } [Fact] - public void ShouldIncludeDefaultValues() { + public void Serialize_ModelWithDefaultValues_ShouldIncludeDefaultValues() { + // Arrange var data = new SampleModel(); var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(data, new []{ nameof(SampleModel.Date), nameof(SampleModel.DateOffset) }); - Assert.Equal(@"{""number"":0,""bool"":false,""message"":null,""dictionary"":null,""collection"":null,""nested"":null}", json); + + // Assert + Assert.Equal("{\"Number\":0,\"Rating\":0.0,\"Bool\":false,\"Direction\":\"North\",\"Message\":null,\"Dictionary\":null,\"Collection\":null,\"Nested\":null}", json); + var model = serializer.Deserialize(json); Assert.Equal(data.Number, model.Number); Assert.Equal(data.Bool, model.Bool); @@ -108,9 +422,11 @@ public void ShouldIncludeDefaultValues() { } [Fact] - public void ShouldSerializeValues() { + public void Serialize_ModelWithDataTypes_ShouldSerializeValues() { + // Arrange var data = new SampleModel { Number = 1, + Rating = 4.50m, Bool = true, Message = "test", Collection = new List { "one" }, @@ -120,8 +436,13 @@ public void ShouldSerializeValues() { }; var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(data); - Assert.Equal(@"{""number"":1,""bool"":true,""date"":""9999-12-31T23:59:59.9999999"",""message"":""test"",""date_offset"":""9999-12-31T23:59:59.9999999+00:00"",""dictionary"":{""key"":""value""},""collection"":[""one""],""nested"":null}", json); + + // Assert + Assert.Equal("{\"Number\":1,\"Rating\":4.50,\"Bool\":true,\"Direction\":\"North\",\"Date\":\"9999-12-31T23:59:59.9999999\",\"Message\":\"test\",\"DateOffset\":\"9999-12-31T23:59:59.9999999+00:00\",\"Dictionary\":{\"key\":\"value\"},\"Collection\":[\"one\"],\"Nested\":null}", json); + var model = serializer.Deserialize(json); Assert.Equal(data.Number, model.Number); Assert.Equal(data.Bool, model.Bool); @@ -132,7 +453,8 @@ public void ShouldSerializeValues() { } [Fact] - public void CanSetMaxDepth() { + public void Serialize_NestedModel_ShouldRespectSetMaxDepth() { + // Arrange var data = new NestedModel { Message = "Level 1", Nested = new NestedModel { @@ -143,48 +465,68 @@ public void CanSetMaxDepth() { } }; var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(data, new[] { nameof(NestedModel.Number) }, maxDepth: 2); - Assert.Equal(@"{""message"":""Level 1"",""nested"":{""message"":""Level 2""}}", json); + + // Assert + Assert.Equal("{\"Message\":\"Level 1\",\"Nested\":{\"Message\":\"Level 2\"}}", json); } [Fact] - public void WillIgnoreEmptyCollections() { + public void Serialize_ModelWithNullCollections_ShouldBeSerialized() { + // Arrange var data = new DefaultsModel { - Message = "Testing", - Collection = new Collection(), - Dictionary = new Dictionary() + Collection = null, + Dictionary = null, + DataDictionary = null }; var serializer = GetSerializer(); - string json = serializer.Serialize(data, new[] { nameof(DefaultsModel.Bool), nameof(DefaultsModel.Number) }); - Assert.Equal(@"{""message"":""Testing""}", json); - } - // TODO: Ability to deserialize objects without underscores - //[Fact] - private void CanDeserializeDataWithoutUnderscores() { - const string json = @"{""BlahId"":""Hello""}"; - const string jsonWithUnderScore = @"{""blah_id"":""Hello""}"; + // Act + string json = serializer.Serialize(data, new[] { nameof(DefaultsModel.Message), nameof(DefaultsModel.Bool), nameof(DefaultsModel.Number) }); + + // Assert + Assert.Equal("{\"Collection\":null,\"Dictionary\":null,\"DataDictionary\":null}", json); + } + [Fact] + public void Serialize_ModelWithEmptyCollections_ShouldBeSerialized() { + // Arrange + var data = new DefaultsModel { + Collection = new Collection(), + Dictionary = new Dictionary(), + DataDictionary = new DataDictionary() + }; var serializer = GetSerializer(); - var value = serializer.Deserialize(json); - Assert.Equal("Hello", value.BlahId); - value = serializer.Deserialize(jsonWithUnderScore); - Assert.Equal("Hello", value.BlahId); + // Act + string json = serializer.Serialize(data, new[] { nameof(DefaultsModel.Message), nameof(DefaultsModel.Bool), nameof(DefaultsModel.Number) }); - string serialized = serializer.Serialize(value); - Assert.Equal(jsonWithUnderScore, serialized); + // Assert + Assert.Equal("{\"Collection\":[],\"Dictionary\":{},\"DataDictionary\":{}}", json); } [Fact] - public void WillDeserializeReferenceIds() { + public void Serialize_ModelWithDictionaryValues_ShouldRespectDictionaryKeyNames() { + // Arrange + var data = new DefaultsModel { + Collection = new Collection() { "Collection" }, + Dictionary = new Dictionary() { { "ItEm", "Value" } }, + DataDictionary = new DataDictionary() { { "ItEm", "Value" } } + }; var serializer = GetSerializer(); - var ev = (Event)serializer.Deserialize(@"{""reference_id"": ""123"" }", typeof(Event)); - Assert.Equal("123", ev.ReferenceId); + + // Act + string json = serializer.Serialize(data, new[] { nameof(DefaultsModel.Message), nameof(DefaultsModel.Bool), nameof(DefaultsModel.Number) }); + + // Assert + Assert.Equal("{\"Collection\":[\"Collection\"],\"Dictionary\":{\"ItEm\":\"Value\"},\"DataDictionary\":{\"ItEm\":\"Value\"}}", json); } [Fact] - public void WillSerializeDeepExceptionWithStackInformation() { + public void Serialize_ExceptionWithInnerException_ShouldSerializeDeepExceptionWithStackInformation() { + // Arrange try { try { try { @@ -201,21 +543,59 @@ public void WillSerializeDeepExceptionWithStackInformation() { catch (Exception ex) { var client = CreateClient(); var error = ex.ToErrorModel(client); - var ev = new Event(); - ev.Data[Event.KnownDataKeys.Error] = error; + var ev = new Event { + Data = { + [Event.KnownDataKeys.Error] = error + } + }; var serializer = GetSerializer(); + + // Act string json = serializer.Serialize(ev); + // Assert Assert.Contains($"\"line_number\":{error.Inner.Inner.StackTrace.Single().LineNumber}", json); } } + [Fact] + public void Serialize_PostDataConverter_ShouldHandleRequestInfoConverterPostDataAsJSON() { + // Arrange + var requestInfo = new RequestInfo { + PostData = new { Age = 21 } + }; + + string[] propertiesToExclude = typeof(RequestInfo).GetProperties().Select(p => p.Name) + .Except(new []{ nameof(RequestInfo.PostData) }) + .ToArray(); + + var serializer = GetSerializer(); + + // Act + string json = serializer.Serialize(requestInfo, propertiesToExclude); + + // Assert + Assert.Equal("{\"post_data\":{\"Age\":21}}", json); + } + + [Fact] + public void Deserialize_Event_ShouldDeserializeReferenceIds() { + // Arrange + var serializer = GetSerializer(); + + // Act + var ev = (Event)serializer.Deserialize(@"{""reference_id"": ""123"" }", typeof(Event)); + + // Assert + Assert.Equal("123", ev.ReferenceId); + } + private ExceptionlessClient CreateClient() { return new ExceptionlessClient(c => { c.UseLogger(new XunitExceptionlessLog(_writer) { MinimumLogLevel = LogLevel.Trace }); c.ReadFromAttributes(); - c.UserAgent = "testclient/1.0.0.0"; + c.UserAgent = "test-client/1.0.0.0"; // Disable updating settings. c.UpdateSettingsWhenIdleInterval = TimeSpan.Zero; @@ -223,10 +603,6 @@ private ExceptionlessClient CreateClient() { } } - public class Blah { - public string BlahId { get; set; } - } - public class NestedModel { public int Number { get; set; } public string Message { get; set; } @@ -235,7 +611,9 @@ public class NestedModel { public class SampleModel { public int Number { get; set; } + public decimal Rating { get; set; } public bool Bool { get; set; } + public Direction Direction { get; set; } public DateTime Date { get; set; } public string Message { get; set; } public DateTimeOffset DateOffset { get; set; } @@ -250,6 +628,14 @@ public class DefaultsModel { public string Message { get; set; } public ICollection Collection { get; set; } public IDictionary Dictionary { get; set; } + public DataDictionary DataDictionary { get; set; } + } + + public enum Direction { + North = 0, + East = 1, + South = 2, + West = 3 } public class User { diff --git a/test/Exceptionless.Tests/Serializer/JsonStorageSerializerTests.cs b/test/Exceptionless.Tests/Serializer/JsonStorageSerializerTests.cs index 7db623f6..1aa2157f 100644 --- a/test/Exceptionless.Tests/Serializer/JsonStorageSerializerTests.cs +++ b/test/Exceptionless.Tests/Serializer/JsonStorageSerializerTests.cs @@ -1,4 +1,4 @@ -using Exceptionless.Dependency; +using Exceptionless.Dependency; using Exceptionless.Serializer; using Xunit; @@ -8,42 +8,42 @@ public JsonStorageSerializerTests() { Resolver.Register(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeEnvironmentInfo() { base.CanSerializeEnvironmentInfo(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeRequestInfo() { base.CanSerializeRequestInfo(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeTraceLogEntries() { base.CanSerializeTraceLogEntries(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeUserInfo() { base.CanSerializeUserInfo(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeUserDescription() { base.CanSerializeUserDescription(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeManualStackingInfo() { base.CanSerializeManualStackingInfo(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeSimpleError() { base.CanSerializeSimpleError(); } - [Fact(Skip = "The json serializer deserialize anonymous(object) types as dictionary.")] + [Fact(Skip = "The JSON DataDictionaryConverter converts objects into json strings")] public override void CanSerializeError() { base.CanSerializeError(); }