| | | 1 | | using System.Globalization; |
| | | 2 | | using System.Security.Cryptography; |
| | | 3 | | using Elsa.Bpmn.Interchange.Services; |
| | | 4 | | using Elsa.Extensions; |
| | | 5 | | using Elsa.Workflows.Management.Entities; |
| | | 6 | | |
| | | 7 | | namespace Elsa.Bpmn.Interchange.Endpoints.Bpmn.Document; |
| | | 8 | | |
| | | 9 | | /// <summary> |
| | | 10 | | /// The strong <c>ETag</c> the document <c>Get</c> and <c>Put</c> endpoints exchange for optimistic concurrency: a |
| | | 11 | | /// SHA-256 hash over the stored workflow definition's id, its version, its BPMN source |
| | | 12 | | /// (<see cref="BpmnInterchangeDocumentService.SourceXmlCustomPropertyKey"/>) and its serialized activity graph |
| | | 13 | | /// (<see cref="WorkflowDefinition.StringData"/>). |
| | | 14 | | /// </summary> |
| | | 15 | | /// <remarks> |
| | | 16 | | /// <para> |
| | | 17 | | /// Content-derived rather than a stored counter, because no counter survives every writer. An unpublished draft is |
| | | 18 | | /// edited in place — same row, same version — and both <see cref="Elsa.Workflows.Management.IWorkflowDefinitionImporter |
| | | 19 | | /// (behind <c>Import</c> and the document <c>Put</c>) and the workflow-definition save endpoint the designer uses |
| | | 20 | | /// replace <c>CustomProperties</c> wholesale with the caller's, so a counter kept there is wiped, or carried forward |
| | | 21 | | /// unchanged, by exactly the writes it would have to record. What those writes do change is the content: a document |
| | | 22 | | /// <c>Put</c> or an <c>Import</c> rewrites the stored source, a designer save rewrites the graph, and a save that drops |
| | | 23 | | /// the source hashes differently from one that keeps it. The id and version tie the value to one stored row, so a new |
| | | 24 | | /// draft version of identical content still gets a different one. |
| | | 25 | | /// </para> |
| | | 26 | | /// <para> |
| | | 27 | | /// Identical stored content yields an identical <c>ETag</c>, which is what a strong validator means — it names a |
| | | 28 | | /// representation — so a <c>Put</c> that writes back exactly what is stored returns the value <c>Get</c> did, having |
| | | 29 | | /// overwritten nothing. Every input is length-prefixed and an absent one is marked distinctly from an empty one, so |
| | | 30 | | /// no two different sets of inputs feed the hash the same bytes. The value is opaque to clients, which must send it |
| | | 31 | | /// back verbatim. |
| | | 32 | | /// </para> |
| | | 33 | | /// </remarks> |
| | | 34 | | internal static class BpmnDocumentETag |
| | | 35 | | { |
| | | 36 | | /// <summary>Computes the quoted strong ETag for <paramref name="definition"/> as it is stored.</summary> |
| | | 37 | | public static string From(WorkflowDefinition definition) |
| | | 38 | | { |
| | 160 | 39 | | var sourceXml = definition.CustomProperties.TryGetValue<string>(BpmnInterchangeDocumentService.SourceXmlCustomPr |
| | | 40 | | |
| | 160 | 41 | | using var hash = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); |
| | 160 | 42 | | BpmnContentHash.AppendField(hash, definition.Id); |
| | 160 | 43 | | BpmnContentHash.AppendField(hash, definition.Version.ToString(CultureInfo.InvariantCulture)); |
| | 160 | 44 | | BpmnContentHash.AppendField(hash, sourceXml); |
| | 160 | 45 | | BpmnContentHash.AppendField(hash, definition.StringData); |
| | | 46 | | |
| | 160 | 47 | | return $"\"{Convert.ToHexString(hash.GetHashAndReset())}\""; |
| | 160 | 48 | | } |
| | | 49 | | } |