| | | 1 | | using System.Text.Json; |
| | | 2 | | using System.Text.Json.Serialization; |
| | | 3 | | using System.Text.Json.Serialization.Metadata; |
| | | 4 | | using Elsa.Expressions.Services; |
| | | 5 | | using Elsa.Workflows.Runtime.Entities; |
| | | 6 | | using Elsa.Workflows.Serialization.Converters; |
| | | 7 | | |
| | | 8 | | namespace Elsa.Workflows.Runtime.Comparers; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Compares two <see cref="StoredTrigger"/> instances for equality. |
| | | 12 | | /// </summary> |
| | | 13 | | public class WorkflowTriggerEqualityComparer : IEqualityComparer<StoredTrigger> |
| | | 14 | | { |
| | | 15 | | private readonly JsonSerializerOptions _settings; |
| | | 16 | | |
| | | 17 | | /// <summary> |
| | | 18 | | /// Initializes a new instance of the <see cref="WorkflowTriggerEqualityComparer"/> class. |
| | | 19 | | /// </summary> |
| | 1487 | 20 | | public WorkflowTriggerEqualityComparer() |
| | | 21 | | { |
| | 1487 | 22 | | _settings = new() |
| | 1487 | 23 | | { |
| | 1487 | 24 | | // Enables serialization of ValueTuples, which use fields instead of properties. |
| | 1487 | 25 | | IncludeFields = true, |
| | 1487 | 26 | | PropertyNameCaseInsensitive = true, |
| | 1487 | 27 | | // Use camelCase to match IPayloadSerializer, which stores payloads using camelCase. |
| | 1487 | 28 | | // Without this, fresh payloads (typed CLR objects) serialize with PascalCase while |
| | 1487 | 29 | | // DB-loaded payloads (JsonElement) preserve their stored camelCase keys, causing |
| | 1487 | 30 | | // the diff to always report all triggers as changed. |
| | 1487 | 31 | | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, |
| | 1487 | 32 | | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, |
| | 1487 | 33 | | }; |
| | | 34 | | |
| | | 35 | | // Mirror the converters used by IPayloadSerializer so that enum, TimeSpan, and |
| | | 36 | | // polymorphic object properties serialize identically to their stored representation. |
| | 1487 | 37 | | _settings.Converters.Add(new JsonStringEnumConverter()); |
| | 1487 | 38 | | _settings.Converters.Add(JsonMetadataServices.TimeSpanConverter); |
| | 1487 | 39 | | _settings.Converters.Add(new PolymorphicObjectConverterFactory()); |
| | 1487 | 40 | | _settings.Converters.Add(new TypeJsonConverter(WellKnownTypeRegistry.CreateDefault())); |
| | 1487 | 41 | | } |
| | | 42 | | |
| | | 43 | | /// <inheritdoc /> |
| | | 44 | | public bool Equals(StoredTrigger? x, StoredTrigger? y) |
| | | 45 | | { |
| | 754 | 46 | | var xJson = x != null ? Serialize(x) : null; |
| | 754 | 47 | | var yJson = y != null ? Serialize(y) : null; |
| | 754 | 48 | | return xJson == yJson; |
| | | 49 | | } |
| | | 50 | | |
| | | 51 | | /// <inheritdoc /> |
| | | 52 | | public int GetHashCode(StoredTrigger obj) |
| | | 53 | | { |
| | 1860 | 54 | | var json = Serialize(obj); |
| | 1860 | 55 | | return json.GetHashCode(); |
| | | 56 | | } |
| | | 57 | | |
| | | 58 | | private string Serialize(StoredTrigger storedTrigger) |
| | | 59 | | { |
| | | 60 | | // Normalize the payload to a canonical JSON string so that both typed CLR objects |
| | | 61 | | // and JsonElement instances (from DB round-trips) produce identical output. |
| | 3368 | 62 | | var normalizedPayload = NormalizePayload(storedTrigger.Payload); |
| | | 63 | | |
| | 3368 | 64 | | var input = new |
| | 3368 | 65 | | { |
| | 3368 | 66 | | Payload = normalizedPayload, |
| | 3368 | 67 | | storedTrigger.Name, |
| | 3368 | 68 | | storedTrigger.ActivityId, |
| | 3368 | 69 | | storedTrigger.WorkflowDefinitionId, |
| | 3368 | 70 | | storedTrigger.WorkflowDefinitionVersionId, |
| | 3368 | 71 | | storedTrigger.Hash |
| | 3368 | 72 | | }; |
| | 3368 | 73 | | return JsonSerializer.Serialize(input, _settings); |
| | | 74 | | } |
| | | 75 | | |
| | | 76 | | /// <summary> |
| | | 77 | | /// Normalizes a payload to a canonical JSON string representation. |
| | | 78 | | /// This ensures that typed CLR objects and JsonElements (which preserve their original |
| | | 79 | | /// property name casing from DB storage) produce identical output when compared. |
| | | 80 | | /// </summary> |
| | | 81 | | private string? NormalizePayload(object? payload) |
| | | 82 | | { |
| | 3368 | 83 | | if (payload == null) |
| | 4 | 84 | | return null; |
| | | 85 | | |
| | | 86 | | // Serialize using the concrete runtime type rather than object, so that |
| | | 87 | | // PolymorphicObjectConverterFactory (which only handles typeof(object)) does not |
| | | 88 | | // inject a "_type" discriminator field into CLR payloads. Without this, |
| | | 89 | | // fresh CLR payloads produce {"path":"...","_type":"..."} while DB-loaded |
| | | 90 | | // JsonElement payloads produce {"path":"..."}, and the two never compare equal. |
| | 3364 | 91 | | return JsonSerializer.Serialize(payload, payload.GetType(), _settings); |
| | | 92 | | } |
| | | 93 | | } |
| | | 94 | | |