Skip to content

Instantly share code, notes, and snippets.

@sfmskywalker
Last active August 4, 2021 18:30
Show Gist options
  • Save sfmskywalker/cc4a4bbca1e805a5c43afc2af21b6af1 to your computer and use it in GitHub Desktop.
Save sfmskywalker/cc4a4bbca1e805a5c43afc2af21b6af1 to your computer and use it in GitHub Desktop.
Building Workflow Driven .NET Applications With Elsa 2 - Part 6
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Core.Services;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services;
using Elsa.Services.Models;
namespace DocumentManagement.Workflows.Activities
{
[Activity(Category = "Document Management", Description = "Archives the specified document.")]
public class ArchiveDocument : Activity
{
private readonly IDocumentStore _documentStore;
public ArchiveDocument(IDocumentStore documentStore, IFileStorage fileStorage)
{
_documentStore = documentStore;
}
[ActivityInput(
Label = "Document",
Hint = "The document to archive",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid},
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName
)]
public Document Document { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
Document.Status = DocumentStatus.Archived;
await _documentStore.SaveAsync(Document);
return Done();
}
}
}
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Workflows.Activities;
using Elsa.Scripting.Liquid.Messages;
using Fluid;
using MediatR;
namespace DocumentManagement.Workflows.Scripting.Liquid
{
public class ConfigureLiquidEngine : INotificationHandler<EvaluatingLiquidExpression>
{
public Task Handle(EvaluatingLiquidExpression notification, CancellationToken cancellationToken)
{
var memberAccessStrategy = notification.TemplateContext.Options.MemberAccessStrategy;
memberAccessStrategy.Register<Document>();
memberAccessStrategy.Register<DocumentFile>();
return Task.CompletedTask;
}
}
}
using System.IO;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Core.Services;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services;
using Elsa.Services.Models;
namespace DocumentManagement.Workflows.Activities
{
public record DocumentFile(Document Document, Stream FileStream);
[Action(Category = "Document Management", Description = "Gets the specified document from the database.")]
public class GetDocument : Activity
{
private readonly IDocumentStore _documentStore;
private readonly IFileStorage _fileStorage;
public GetDocument(IDocumentStore documentStore, IFileStorage fileStorage)
{
_documentStore = documentStore;
_fileStorage = fileStorage;
}
[ActivityInput(
Label = "Document ID",
Hint = "The ID of the document to load",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid}
)]
public string DocumentId { get; set; } = default!;
[ActivityOutput(
Hint = "The document + file.",
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName,
DisableWorkflowProviderSelection = true)]
public DocumentFile Output { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var document = await _documentStore.GetAsync(DocumentId, context.CancellationToken);
var fileStream = await _fileStorage.ReadAsync(document!.FileName, context.CancellationToken);
Output = new DocumentFile(document, fileStream);
return Done();
}
}
}
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Elsa;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services;
using Elsa.Services.Models;
namespace DocumentManagement.Workflows.Activities
{
[Activity(
Category = "Document Management",
Description = "Saves a hash of the specified file onto the blockchain to prevent tampering."
)]
public class UpdateBlockchain : Activity
{
[ActivityInput(
Label = "File",
Hint = "The file to store its hash of onto the blockchain. Can be byte[] or Stream.",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid},
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName
)]
public object File { get; set; } = default!;
[ActivityOutput(Hint = "The computed file signature as stored on the blockchain.")]
public string Output { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
// Determine the type of File object: is it a Stream or a byte array?
var bytes = File is Stream stream
? await stream.ReadBytesToEndAsync()
: File is byte[] buffer
? buffer
: throw new NotSupportedException();
// Compute hash.
var fileSignature = ComputeSignature(bytes);
// TODO: Schedule background work using Hangfire that will fake-update an imaginary blockchain with the file signature.
// Suspend the workflow.
return Suspend();
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
// When we resume, read the simply complete this activity.
// TODO: Receive the computed file signature and set it as Output.
return Done();
}
private static string ComputeSignature(byte[] bytes)
{
using var algorithm = SHA256.Create();
var hashValue = algorithm.ComputeHash(bytes);
return Convert.ToBase64String(hashValue);
}
}
}
using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Elsa;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Models;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services;
using Elsa.Services.Models;
using Hangfire;
namespace DocumentManagement.Workflows.Activities
{
[Activity(
Category = "Document Management",
Description = "Saves a hash of the specified file onto the blockchain to prevent tampering."
)]
public class UpdateBlockchain : Activity
{
public record UpdateBlockchainContext(string WorkflowInstanceId, string ActivityId, string FileSignature);
private readonly IBackgroundJobClient _backgroundJobClient;
private readonly IWorkflowInstanceDispatcher _workflowInstanceDispatcher;
public UpdateBlockchain(IBackgroundJobClient backgroundJobClient, IWorkflowInstanceDispatcher workflowInstanceDispatcher)
{
_backgroundJobClient = backgroundJobClient;
_workflowInstanceDispatcher = workflowInstanceDispatcher;
}
[ActivityInput(
Label = "File",
Hint = "The file to store its hash of onto the blockchain. Can be byte[] or Stream.",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript, SyntaxNames.Liquid},
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName
)]
public object File { get; set; } = default!;
[ActivityOutput(Hint = "The computed file signature as stored on the blockchain.")]
public string Output { get; set; } = default!;
/// <summary>
/// Invoked by Hangfire as a background job.
/// </summary>
public async Task SubmitToBlockChainAsync(UpdateBlockchainContext context, CancellationToken cancellationToken)
{
// Simulate storing it on an imaginary blockchain out there.
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken);
// Resume the suspended workflow.
await _workflowInstanceDispatcher.DispatchAsync(new ExecuteWorkflowInstanceRequest(context.WorkflowInstanceId, context.ActivityId, new WorkflowInput(context.FileSignature)), cancellationToken);
}
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
// Compute hash.
var bytes = File is Stream stream ? await stream.ReadBytesToEndAsync() : File is byte[] buffer ? buffer : throw new NotSupportedException();
var fileSignature = ComputeSignature(bytes);
// Schedule background work using Hangfire.
_backgroundJobClient.Enqueue(() => SubmitToBlockChainAsync(new UpdateBlockchainContext(context.WorkflowInstance.Id, context.ActivityId, fileSignature), CancellationToken.None));
// Suspend the workflow.
return Suspend();
}
protected override IActivityExecutionResult OnResume(ActivityExecutionContext context)
{
// When we resume, simply complete this activity.
var fileSignature = context.GetInput<string>();
Output = fileSignature;
return Done();
}
private static string ComputeSignature(byte[] bytes)
{
using var algorithm = SHA256.Create();
var hashValue = algorithm.ComputeHash(bytes);
return Convert.ToBase64String(hashValue);
}
}
}
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Elsa.ActivityResults;
using Elsa.Attributes;
using Elsa.Expressions;
using Elsa.Providers.WorkflowStorage;
using Elsa.Services;
using Elsa.Services.Models;
namespace DocumentManagement.Workflows.Activities
{
[Action(Category = "Document Management", Description = "Zips the specified file.")]
public class ZipFile : Activity
{
[ActivityInput(
Hint = "The file stream to zip.",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript},
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName,
DisableWorkflowProviderSelection = true
)]
public Stream Stream { get; set; } = default!;
[ActivityInput(
Hint = "The file name to use for the zip entry.",
SupportedSyntaxes = new[] {SyntaxNames.JavaScript}
)]
public string FileName { get; set; } = default!;
[ActivityOutput(
Hint = "The zipped file stream.",
DefaultWorkflowStorageProvider = TransientWorkflowStorageProvider.ProviderName,
DisableWorkflowProviderSelection = true
)]
public Stream Output { get; set; } = default!;
protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
{
var outputStream = new MemoryStream();
using (var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create, true))
{
var zipEntry = zipArchive.CreateEntry(FileName, CompressionLevel.Optimal);
await using var zipStream = zipEntry.Open();
await Stream.CopyToAsync(zipStream);
}
// Reset position.
outputStream.Seek(0, SeekOrigin.Begin);
Output = outputStream;
return Done();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment