< Summary

Information
Class: ProjectTemplate.Infrastructure.Data.Auditing.CanonicalApplicationMutationManifestBuilder
Assembly: ProjectTemplate.Infrastructure
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/CanonicalApplicationMutationManifestBuilder.cs
Line coverage
90%
Covered lines: 104
Uncovered lines: 11
Coverable lines: 115
Total lines: 189
Line coverage: 90.4%
Branch coverage
72%
Covered branches: 31
Total branches: 43
Branch coverage: 72%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)62.5%8892.85%
CreateCanonicalRecord(...)83.33%1818100%
CanonicalizeValueObject(...)75%4490.9%
WriteCanonicalElement(...)61.53%271356.25%
WriteRecord(...)100%11100%
WriteCanonicalObjectProperty(...)100%11100%
.ctor(...)100%11100%
get_SchemaVersion()100%11100%
get_Entity()100%11100%
get_State()100%11100%
get_KeyValues()100%11100%
get_OriginalValues()100%11100%
get_CurrentValues()100%11100%
get_OperationExecutionId()100%11100%
get_ExecutionAttemptId()100%11100%
get_DecisionAuditRecordId()100%11100%
get_CorrelationId()100%11100%
get_TraceId()100%11100%
get_SpanId()100%11100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Infrastructure/Data/Auditing/CanonicalApplicationMutationManifestBuilder.cs

#LineLine coverage
 1using System.Text;
 2using System.Text.Json;
 3using ProjectTemplate.Infrastructure.Data.Entities;
 4
 5namespace ProjectTemplate.Infrastructure.Data.Auditing;
 6
 7/// <summary>
 8/// Builds deterministic version 1.0 mutation manifests using ordinal ordering rules.
 9/// </summary>
 10public sealed class CanonicalApplicationMutationManifestBuilder : IApplicationMutationManifestBuilder
 11{
 12    /// <inheritdoc />
 13    public ApplicationMutationManifest Build(IReadOnlyCollection<AuditRecord> auditRecords)
 14    {
 8415        ArgumentNullException.ThrowIfNull(auditRecords);
 16
 8417        if (auditRecords.Count == 0)
 18        {
 019            throw new ArgumentException("At least one audit record is required to build a mutation manifest.", nameof(au
 20        }
 21
 8422        string mutationBatchId = auditRecords.First().MutationBatchId;
 8423        if (string.IsNullOrWhiteSpace(mutationBatchId))
 24        {
 025            throw new InvalidOperationException("Mutation audit records require a mutation batch identifier.");
 26        }
 27
 18428        if (auditRecords.Any(record => !string.Equals(record.MutationBatchId, mutationBatchId, StringComparison.Ordinal)
 29        {
 030            throw new InvalidOperationException("A canonical mutation manifest can represent only one mutation batch.");
 31        }
 32
 8433        var canonicalRecords = auditRecords
 8434            .Select(CreateCanonicalRecord)
 3035            .OrderBy(record => record.Entity, StringComparer.Ordinal)
 3036            .ThenBy(record => record.State, StringComparer.Ordinal)
 3037            .ThenBy(record => record.KeyValues, StringComparer.Ordinal)
 3038            .ThenBy(record => record.OriginalValues, StringComparer.Ordinal)
 3039            .ThenBy(record => record.CurrentValues, StringComparer.Ordinal)
 3040            .ThenBy(record => record.OperationExecutionId, StringComparer.Ordinal)
 3041            .ThenBy(record => record.ExecutionAttemptId, StringComparer.Ordinal)
 3042            .ThenBy(record => record.DecisionAuditRecordId, StringComparer.Ordinal)
 3043            .ThenBy(record => record.CorrelationId, StringComparer.Ordinal)
 3044            .ThenBy(record => record.TraceId, StringComparer.Ordinal)
 3045            .ThenBy(record => record.SpanId, StringComparer.Ordinal)
 3046            .ThenBy(record => record.SchemaVersion, StringComparer.Ordinal)
 8447            .ToList();
 48
 8449        using var stream = new MemoryStream();
 8450        using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = false }))
 51        {
 8452            writer.WriteStartObject();
 8453            writer.WriteString("schemaVersion", ApplicationMutationManifest.CurrentSchemaVersion);
 8454            writer.WriteString("mutationBatchId", mutationBatchId);
 8455            writer.WriteNumber("auditRecordCount", canonicalRecords.Count);
 8456            writer.WritePropertyName("records");
 8457            writer.WriteStartArray();
 58
 36859            foreach (CanonicalAuditRecord record in canonicalRecords)
 60            {
 10061                WriteRecord(writer, record);
 62            }
 63
 8464            writer.WriteEndArray();
 8465            writer.WriteEndObject();
 8466        }
 67
 8468        return new ApplicationMutationManifest(
 8469            ApplicationMutationManifest.CurrentSchemaVersion,
 8470            mutationBatchId,
 8471            canonicalRecords.Count,
 8472            Encoding.UTF8.GetString(stream.ToArray()));
 8473    }
 74
 75    private static CanonicalAuditRecord CreateCanonicalRecord(AuditRecord record)
 76    {
 10077        ArgumentNullException.ThrowIfNull(record);
 78
 10079        return new CanonicalAuditRecord(
 10080            record.SchemaVersion ?? string.Empty,
 10081            record.Entity ?? string.Empty,
 10082            record.State ?? string.Empty,
 10083            CanonicalizeValueObject(record.KeyValues),
 10084            CanonicalizeValueObject(record.OriginalValues),
 10085            CanonicalizeValueObject(record.CurrentValues),
 10086            record.OperationExecutionId ?? string.Empty,
 10087            record.ExecutionAttemptId ?? string.Empty,
 10088            record.DecisionAuditRecordId ?? string.Empty,
 10089            record.CorrelationId ?? string.Empty,
 10090            record.TraceId ?? string.Empty,
 10091            record.SpanId ?? string.Empty);
 92    }
 93
 94    private static string CanonicalizeValueObject(string? json)
 95    {
 30096        if (string.IsNullOrWhiteSpace(json))
 97        {
 6698            return "{}";
 99        }
 100
 234101        using var document = JsonDocument.Parse(json);
 234102        if (document.RootElement.ValueKind != JsonValueKind.Object)
 103        {
 0104            throw new InvalidOperationException("Audit value payloads must be JSON objects.");
 105        }
 106
 234107        using var stream = new MemoryStream();
 234108        using (var writer = new Utf8JsonWriter(stream))
 109        {
 234110            WriteCanonicalElement(writer, document.RootElement);
 234111        }
 112
 234113        return Encoding.UTF8.GetString(stream.ToArray());
 234114    }
 115
 116    private static void WriteCanonicalElement(Utf8JsonWriter writer, JsonElement element)
 117    {
 996118        switch (element.ValueKind)
 119        {
 120            case JsonValueKind.Object:
 234121                writer.WriteStartObject();
 2754122                foreach (JsonProperty property in element.EnumerateObject().OrderBy(property => property.Name, StringCom
 123                {
 762124                    writer.WritePropertyName(property.Name);
 762125                    WriteCanonicalElement(writer, property.Value);
 126                }
 234127                writer.WriteEndObject();
 234128                break;
 129            case JsonValueKind.Array:
 0130                writer.WriteStartArray();
 0131                foreach (JsonElement item in element.EnumerateArray())
 132                {
 0133                    WriteCanonicalElement(writer, item);
 134                }
 0135                writer.WriteEndArray();
 0136                break;
 137            case JsonValueKind.String:
 138            case JsonValueKind.Number:
 139            case JsonValueKind.True:
 140            case JsonValueKind.False:
 141            case JsonValueKind.Null:
 762142                element.WriteTo(writer);
 762143                break;
 144            case JsonValueKind.Undefined:
 0145                throw new InvalidOperationException("Undefined JSON values cannot be represented in a canonical mutation
 146            default:
 0147                throw new InvalidOperationException($"Unsupported JSON value kind '{element.ValueKind}'.");
 148        }
 149    }
 150
 151    private static void WriteRecord(Utf8JsonWriter writer, CanonicalAuditRecord record)
 152    {
 100153        writer.WriteStartObject();
 100154        writer.WriteString("schemaVersion", record.SchemaVersion);
 100155        writer.WriteString("entity", record.Entity);
 100156        writer.WriteString("state", record.State);
 100157        WriteCanonicalObjectProperty(writer, "keyValues", record.KeyValues);
 100158        WriteCanonicalObjectProperty(writer, "originalValues", record.OriginalValues);
 100159        WriteCanonicalObjectProperty(writer, "currentValues", record.CurrentValues);
 100160        writer.WriteString("operationExecutionId", record.OperationExecutionId);
 100161        writer.WriteString("executionAttemptId", record.ExecutionAttemptId);
 100162        writer.WriteString("decisionAuditRecordId", record.DecisionAuditRecordId);
 100163        writer.WriteString("correlationId", record.CorrelationId);
 100164        writer.WriteString("traceId", record.TraceId);
 100165        writer.WriteString("spanId", record.SpanId);
 100166        writer.WriteEndObject();
 100167    }
 168
 169    private static void WriteCanonicalObjectProperty(Utf8JsonWriter writer, string propertyName, string canonicalJson)
 170    {
 300171        writer.WritePropertyName(propertyName);
 300172        using var document = JsonDocument.Parse(canonicalJson);
 300173        document.RootElement.WriteTo(writer);
 600174    }
 175
 100176    private sealed record CanonicalAuditRecord(
 130177        string SchemaVersion,
 130178        string Entity,
 130179        string State,
 130180        string KeyValues,
 130181        string OriginalValues,
 130182        string CurrentValues,
 130183        string OperationExecutionId,
 130184        string ExecutionAttemptId,
 130185        string DecisionAuditRecordId,
 130186        string CorrelationId,
 130187        string TraceId,
 230188        string SpanId);
 189}