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