diff --git a/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj b/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj
new file mode 100644
index 0000000..b0d2e8b
--- /dev/null
+++ b/JsonCanicalizer.Test/JsonCanonicalizer.Test.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/JsonCanicalizer.Test/Program.cs b/JsonCanicalizer.Test/Program.cs
new file mode 100644
index 0000000..c6dd3fb
--- /dev/null
+++ b/JsonCanicalizer.Test/Program.cs
@@ -0,0 +1,26 @@
+using JsonCanonicalizer.BL;
+using System.Text.Json;
+
+namespace JsonCanicalizer.Test;
+
+internal class Program
+{
+ static void Main(string[] args)
+ {
+ string input = File.ReadAllText(@"d:\Code\testbench\JsonCanonicalizer\JsonCanicalizer.Test\test.json");
+
+ using var doc = JsonDocument.Parse(input);
+
+ var writerOptions = new JsonWriterOptions
+ {
+ Indented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ using var outputStream = new FileStream(@"d:\Code\testbench\JsonCanonicalizer\JsonCanicalizer.Test\result.json", FileMode.Create);
+ using var writer = new Utf8JsonWriter(outputStream, writerOptions);
+
+ Canonicalizer.WriteSorted(writer, doc.RootElement);
+ writer.Flush();
+ }
+}
diff --git a/JsonCanicalizer.Test/result.json b/JsonCanicalizer.Test/result.json
new file mode 100644
index 0000000..589243f
--- /dev/null
+++ b/JsonCanicalizer.Test/result.json
@@ -0,0 +1,64 @@
+{
+ "serviceTasks": [
+ {
+ "guid": "07e607a8-7673-458f-a8f0-2850f7864327",
+ "id": "Activity_1y13pct",
+ "name": "Внесение изменений/ дополнений/ исключений сведений в реестр уведомлений ТЭД (envelopeData)"
+ },
+ {
+ "guid": "18b61142-b1e7-483a-bb17-3d42db1b8db3",
+ "id": "Activity_0lrt9vk",
+ "name": "Присвоение номера по шаблону"
+ },
+ {
+ "guid": "29203f14-7505-4994-bd84-574524b35c65",
+ "id": "Activity_086flld",
+ "name": "Получение выписки из реестра ТЭД"
+ },
+ {
+ "guid": "2a804098-fba1-4828-a139-c1449c3b2acf",
+ "id": "Activity_1etjhti",
+ "name": "Перевод дела в статус \"Положительное решение\""
+ },
+ {
+ "guid": "406c7066-3eb7-4abd-a7d9-b9f96cc9e236",
+ "id": "Activity_0ptjle4",
+ "name": "Включение в реестр уведомлений ТЭД (envelopeData)"
+ },
+ {
+ "guid": "59b09840-2b0f-47c3-9d07-941e00dc9794",
+ "id": "Activity_1wcy9l4",
+ "name": "Открытые сведения из ЕГРЮЛ (включая сведения, доступ к которым ограничен абзацами 3 и 5 пункта 1 статьи 6 Федерального закона от 08.08.2001 № 129-ФЗ) по запросам органов государственной власти (envelopeData)"
+ },
+ {
+ "guid": "805fe785-5c2b-4ee2-a2e7-c4adbc40ae5d",
+ "id": "Activity_0bwwih4",
+ "name": "Перевод в статус \"Отказ в предоставлении услуги\" (4)"
+ },
+ {
+ "guid": "80dbe463-1df6-4f9f-b76d-888786c1484f",
+ "id": "Activity_0ii8c0k",
+ "name": "Перевод дела в статус \"Отрицательное решение\""
+ },
+ {
+ "guid": "87c1dc1d-b8d6-464c-8eff-48163f5b86ab",
+ "id": "Activity_0hwnhm8",
+ "name": "Отправка на ЕПГУ \"Услуга оказана\" (3) с положительным решением, необходимые файлы получены"
+ },
+ {
+ "guid": "bee4c89c-601f-4211-a615-7ef8a3eeade7",
+ "id": "Activity_14vp8wt",
+ "name": "Получение данных из реестра уведомлений ТЭД (envelopeData)"
+ },
+ {
+ "guid": "e51e3b7c-082c-4555-bbfa-1579c81b9ffb",
+ "id": "Activity_0ux39aa",
+ "name": "Перевод статуса заявления на ЕПГУ \"Заявление принято к рассмотрению\"(6)"
+ },
+ {
+ "guid": "ee174972-ab78-4fc6-979a-37c1384f3c6d",
+ "id": "Activity_1yii4fr",
+ "name": "Сведения из ЕГРИП по запросам органов государственной власти (открытые сведения и сведения о рождении и адресе ИП) (регламентный)"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/JsonCanicalizer.Test/test.json b/JsonCanicalizer.Test/test.json
new file mode 100644
index 0000000..e0d0bd1
--- /dev/null
+++ b/JsonCanicalizer.Test/test.json
@@ -0,0 +1 @@
+{"serviceTasks":[{"guid":"18b61142-b1e7-483a-bb17-3d42db1b8db3","id":"Activity_0lrt9vk","name":"Присвоение номера по шаблону"},{"guid":"29203f14-7505-4994-bd84-574524b35c65","id":"Activity_086flld","name":"Получение выписки из реестра ТЭД"},{"guid":"80dbe463-1df6-4f9f-b76d-888786c1484f","id":"Activity_0ii8c0k","name":"Перевод дела в статус \"Отрицательное решение\""},{"guid":"805fe785-5c2b-4ee2-a2e7-c4adbc40ae5d","id":"Activity_0bwwih4","name":"Перевод в статус \"Отказ в предоставлении услуги\" (4)"},{"guid":"87c1dc1d-b8d6-464c-8eff-48163f5b86ab","id":"Activity_0hwnhm8","name":"Отправка на ЕПГУ \"Услуга оказана\" (3) с положительным решением, необходимые файлы получены"},{"guid":"2a804098-fba1-4828-a139-c1449c3b2acf","id":"Activity_1etjhti","name":"Перевод дела в статус \"Положительное решение\""},{"guid":"e51e3b7c-082c-4555-bbfa-1579c81b9ffb","id":"Activity_0ux39aa","name":"Перевод статуса заявления на ЕПГУ \"Заявление принято к рассмотрению\"(6)"},{"guid":"ee174972-ab78-4fc6-979a-37c1384f3c6d","id":"Activity_1yii4fr","name":"Сведения из ЕГРИП по запросам органов государственной власти (открытые сведения и сведения о рождении и адресе ИП) (регламентный)"},{"guid":"59b09840-2b0f-47c3-9d07-941e00dc9794","id":"Activity_1wcy9l4","name":"Открытые сведения из ЕГРЮЛ (включая сведения, доступ к которым ограничен абзацами 3 и 5 пункта 1 статьи 6 Федерального закона от 08.08.2001 № 129-ФЗ) по запросам органов государственной власти (envelopeData)"},{"guid":"406c7066-3eb7-4abd-a7d9-b9f96cc9e236","id":"Activity_0ptjle4","name":"Включение в реестр уведомлений ТЭД (envelopeData)"},{"guid":"bee4c89c-601f-4211-a615-7ef8a3eeade7","id":"Activity_14vp8wt","name":"Получение данных из реестра уведомлений ТЭД (envelopeData)"},{"guid":"07e607a8-7673-458f-a8f0-2850f7864327","id":"Activity_1y13pct","name":"Внесение изменений/ дополнений/ исключений сведений в реестр уведомлений ТЭД (envelopeData)"}]}
\ No newline at end of file
diff --git a/JsonCanonicalizer.BL/Canonicalizer.cs b/JsonCanonicalizer.BL/Canonicalizer.cs
new file mode 100644
index 0000000..14f6a69
--- /dev/null
+++ b/JsonCanonicalizer.BL/Canonicalizer.cs
@@ -0,0 +1,112 @@
+using System.Runtime.Serialization;
+using System.Text.Json;
+
+namespace JsonCanonicalizer.BL;
+
+public class Canonicalizer
+{
+ public static void WriteSorted(Utf8JsonWriter writer, JsonElement element)
+ {
+ switch (element.ValueKind)
+ {
+ case JsonValueKind.Object:
+ writer.WriteStartObject();
+ // Сортируем ключи объекта по имени
+ foreach (var property in element.EnumerateObject()
+ .OrderBy(p => p.Name, StringComparer.Ordinal))
+ {
+ writer.WritePropertyName(property.Name);
+ WriteSorted(writer, property.Value);
+ }
+ writer.WriteEndObject();
+ break;
+
+ case JsonValueKind.Array:
+ writer.WriteStartArray();
+ var items = element.EnumerateArray().ToList();
+
+ // Проверяем, нужно ли сортировать массив по guid:
+ // 1. Все элементы — объекты
+ // 2. У каждого объекта есть свойство "guid"
+ bool allAreObjects = items.All(i => i.ValueKind == JsonValueKind.Object);
+ bool allHaveGuid = allAreObjects &&
+ items.All(i => IsOrderable(i));
+
+ IEnumerable orderedItems = items;
+ if (allHaveGuid)
+ {
+ orderedItems = items.OrderBy(i => GetOrder(i),
+ StringComparer.OrdinalIgnoreCase);
+ }
+
+ // Рекурсивно обрабатываем каждый элемент (на случай вложенных массивов)
+ foreach (var item in orderedItems)
+ {
+ WriteSorted(writer, item);
+ }
+ writer.WriteEndArray();
+ break;
+
+ case JsonValueKind.String:
+ writer.WriteStringValue(element.GetString());
+ break;
+
+ case JsonValueKind.Number:
+ if (element.TryGetDecimal(out decimal dec))
+ writer.WriteNumberValue(dec);
+ else
+ writer.WriteNumberValue(element.GetDouble());
+ break;
+
+ case JsonValueKind.True:
+ writer.WriteBooleanValue(true);
+ break;
+
+ case JsonValueKind.False:
+ writer.WriteBooleanValue(false);
+ break;
+
+ case JsonValueKind.Null:
+ writer.WriteNullValue();
+ break;
+
+ default:
+ writer.WriteCommentValue(element.GetRawText());
+ break;
+ }
+ }
+
+ static bool IsOrderable(JsonElement e)
+ {
+ return e.TryGetProperty("id", out _) || e.TryGetProperty("guid", out _);
+ }
+
+ static string GetOrder(JsonElement e)
+ {
+ if (e.TryGetProperty("id", out JsonElement id))
+ {
+ return GetGuidValue(id);
+ }
+ else
+ {
+ return GetGuidValue(e.GetProperty("guid"));
+ }
+ }
+
+ ///
+ /// Универсально извлекает значение guid как строку для сравнения.
+ /// Работает даже если guid почему-то оказался числом или булевым.
+ ///
+ static string GetGuidValue(JsonElement guidElement)
+ {
+ return guidElement.ValueKind switch
+ {
+ JsonValueKind.String => guidElement.GetString() ?? "",
+ JsonValueKind.Number => guidElement.GetRawText(),
+ JsonValueKind.True => "true",
+ JsonValueKind.False => "false",
+ JsonValueKind.Null => "",
+ _ => guidElement.GetRawText()
+ };
+ }
+}
diff --git a/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj b/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj
new file mode 100644
index 0000000..fa71b7a
--- /dev/null
+++ b/JsonCanonicalizer.BL/JsonCanonicalizer.BL.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/JsonCanonicalizer.sln b/JsonCanonicalizer.sln
new file mode 100644
index 0000000..0f9e7b4
--- /dev/null
+++ b/JsonCanonicalizer.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36915.13
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer", "JsonCanonicalizer\JsonCanonicalizer.csproj", "{35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer.BL", "JsonCanonicalizer.BL\JsonCanonicalizer.BL.csproj", "{C4C72D1B-E361-49FB-A76D-6691D27CF2AB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JsonCanonicalizer.Test", "JsonCanicalizer.Test\JsonCanonicalizer.Test.csproj", "{E43C66DF-FE35-4752-8201-A08E807122DB}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {35AE0B30-4ACC-4CCF-9FC7-43A7DBF86B54}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4C72D1B-E361-49FB-A76D-6691D27CF2AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E43C66DF-FE35-4752-8201-A08E807122DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E43C66DF-FE35-4752-8201-A08E807122DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E43C66DF-FE35-4752-8201-A08E807122DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E43C66DF-FE35-4752-8201-A08E807122DB}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {ADE14663-0DC6-43E9-9161-DC0A59CE0881}
+ EndGlobalSection
+EndGlobal
diff --git a/JsonCanonicalizer/JsonCanonicalizer.csproj b/JsonCanonicalizer/JsonCanonicalizer.csproj
new file mode 100644
index 0000000..b0d2e8b
--- /dev/null
+++ b/JsonCanonicalizer/JsonCanonicalizer.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/JsonCanonicalizer/Program.cs b/JsonCanonicalizer/Program.cs
new file mode 100644
index 0000000..3273ff0
--- /dev/null
+++ b/JsonCanonicalizer/Program.cs
@@ -0,0 +1,41 @@
+using System.Text.Json;
+
+namespace JsonCanonicalizer
+{
+ class Program
+ {
+ static int Main(string[] args)
+ {
+ try
+ {
+ using var inputStream = Console.OpenStandardInput();
+ using var doc = JsonDocument.Parse(inputStream);
+
+ var writerOptions = new JsonWriterOptions
+ {
+ Indented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+ };
+
+ using var outputStream = Console.OpenStandardOutput();
+ using var writer = new Utf8JsonWriter(outputStream, writerOptions);
+
+ BL.Canonicalizer.WriteSorted(writer, doc.RootElement);
+ writer.Flush();
+
+ Console.Out.WriteLine();
+ return 0;
+ }
+ catch (JsonException ex)
+ {
+ Console.Error.WriteLine($"Invalid JSON: {ex.Message}");
+ return 1;
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Error: {ex.Message}");
+ return 1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000..6582799
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file