Skip to content

Instantly share code, notes, and snippets.

@sfmskywalker
Last active August 2, 2021 17:00
Show Gist options
  • Save sfmskywalker/836d3cc0accfd009a47daa5a7eb0baee to your computer and use it in GitHub Desktop.
Save sfmskywalker/836d3cc0accfd009a47daa5a7eb0baee to your computer and use it in GitHub Desktop.
Building Workflow Driven .NET Applications With Elsa 2 - Part 4
using DocumentManagement.Core.Options;
using DocumentManagement.Core.Services;
using Microsoft.Extensions.DependencyInjection;
using Storage.Net;
namespace DocumentManagement.Core.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
services.Configure<DocumentStorageOptions>(options => options.BlobStorageFactory = StorageFactory.Blobs.InMemory);
return services
.AddSingleton<ISystemClock, SystemClock>()
.AddSingleton<IFileStorage, FileStorage>()
.AddScoped<IDocumentService, DocumentService>();
}
}
}
using System;
namespace DocumentManagement.Core.Models
{
public class Document
{
public string Id { get; set; } = default!;
public string DocumentTypeId { get; set; } = default!;
public string FileName { get; set; } = default!;
public DateTime CreatedAt { get; set; }
public string? Notes { get; set; }
public DocumentStatus Status { get; set; }
}
}
using DocumentManagement.Core.Models;
using Microsoft.EntityFrameworkCore;
namespace DocumentManagement.Persistence
{
public class DocumentDbContext : DbContext
{
public DocumentDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Document> Documents { get; set; } = default!;
public DbSet<DocumentType> DocumentTypes { get; set; } = default!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<DocumentType>().HasData(
CreateDocumentType("ChangeRequest", "Change Request"),
CreateDocumentType("LeaveRequest", "Leave Request"),
CreateDocumentType("IdentityVerification", "Identity Verification"));
}
private static DocumentType CreateDocumentType(string id, string name) => new DocumentType
{
Id = id,
Name = name
};
}
}
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Events;
using DocumentManagement.Core.Models;
using MediatR;
namespace DocumentManagement.Core.Services
{
public class DocumentService : IDocumentService
{
private readonly IFileStorage _fileStorage;
private readonly ISystemClock _systemClock;
private readonly IMediator _mediator;
private readonly IDocumentStore _documentStore;
public DocumentService(IFileStorage fileStorage, IDocumentStore documentStore, ISystemClock systemClock, IMediator mediator)
{
_fileStorage = fileStorage;
_documentStore = documentStore;
_systemClock = systemClock;
_mediator = mediator;
}
public async Task<Document> SaveDocumentAsync(string fileName, Stream data, string documentTypeId, CancellationToken cancellationToken = default)
{
// Persist the uploaded file.
await _fileStorage.WriteAsync(data, fileName, cancellationToken);
// Create a document record.
var document = new Document
{
Id = Guid.NewGuid().ToString("N"),
Status = DocumentStatus.New,
DocumentTypeId = documentTypeId,
CreatedAt = _systemClock.UtcNow,
FileName = fileName
};
// Save the document.
await _documentStore.SaveAsync(document, cancellationToken);
// Publish a domain event.
await _mediator.Publish(new NewDocumentReceived(document), cancellationToken);
return document;
}
}
}
namespace DocumentManagement.Core.Models
{
public enum DocumentStatus
{
New,
Archived
}
}
using System;
using Storage.Net;
using Storage.Net.Blobs;
namespace DocumentManagement.Core.Options
{
public class DocumentStorageOptions
{
public Func<IBlobStorage> BlobStorageFactory { get; set; } = StorageFactory.Blobs.InMemory;
}
}
namespace DocumentManagement.Core.Models
{
public class DocumentType
{
public string Id { get; set; } = default!;
public string Name { get; set; } = default!;
}
}
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Core.Services;
using Microsoft.EntityFrameworkCore;
namespace DocumentManagement.Persistence.Services
{
public class EFCoreDocumentStore : IDocumentStore
{
private readonly IDbContextFactory<DocumentDbContext> _dbContextFactory;
public EFCoreDocumentStore(IDbContextFactory<DocumentDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task SaveAsync(Document entity, CancellationToken cancellationToken = default)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
var existingDocument = await dbContext.Documents.FirstOrDefaultAsync(x => x.Id == entity.Id, cancellationToken);
if(existingDocument == null)
await dbContext.Documents.AddAsync(entity, cancellationToken);
else
dbContext.Entry(existingDocument).CurrentValues.SetValues(entity);
await dbContext.SaveChangesAsync(cancellationToken);
}
public async Task<Document?> GetAsync(string id, CancellationToken cancellationToken = default)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.Documents.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
}
}
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Core.Services;
using Microsoft.EntityFrameworkCore;
namespace DocumentManagement.Persistence.Services
{
public class EFCoreDocumentTypeStore : IDocumentTypeStore
{
private readonly IDbContextFactory<DocumentDbContext> _dbContextFactory;
public EFCoreDocumentTypeStore(IDbContextFactory<DocumentDbContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
public async Task<IEnumerable<DocumentType>> ListAsync(CancellationToken cancellationToken = default)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.DocumentTypes.ToListAsync(cancellationToken);
}
public async Task<DocumentType?> GetAsync(string id, CancellationToken cancellationToken = default)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
return await dbContext.DocumentTypes.FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
}
}
}
@page "/file-received/{DocumentId}"
@model FileReceivedModel
@{
ViewData["Title"] = "Document Received";
}
<div>
<h1 class="display-4">Document Received</h1>
<p>We received your file. Thank you!</p>
<p>Document ID: @Model.DocumentId</p>
</div>
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
using DocumentManagement.Core.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace DocumentManagement.Web.Pages
{
public class FileReceivedModel : PageModel
{
private readonly IDocumentStore _documentStore;
public FileReceivedModel(IDocumentStore documentStore)
{
_documentStore = documentStore;
}
[BindProperty(SupportsGet = true)] public string DocumentId { get; set; } = default!;
public Document? Document { get; set; }
public async Task<IActionResult?> OnGetAsync(CancellationToken cancellationToken)
{
Document = await _documentStore.GetAsync(DocumentId, cancellationToken);
return Document == null ? NotFound() : default(IActionResult?);
}
}
}
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Options;
using Microsoft.Extensions.Options;
using Storage.Net.Blobs;
namespace DocumentManagement.Core.Services
{
public class FileStorage : IFileStorage
{
private readonly IBlobStorage _blobStorage;
public FileStorage(IOptions<DocumentStorageOptions> options) => _blobStorage = options.Value.BlobStorageFactory();
public Task WriteAsync(Stream data, string fileName, CancellationToken cancellationToken = default) =>
_blobStorage.WriteAsync(fileName, data, false, cancellationToken);
public Task<Stream> ReadAsync(string fileName, CancellationToken cancellationToken = default) =>
_blobStorage.OpenReadAsync(fileName, cancellationToken);
}
}
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
namespace DocumentManagement.Core.Services
{
public interface IDocumentService
{
Task<Document> SaveDocumentAsync(string fileName, Stream data, string documentTypeId, CancellationToken cancellationToken = default);
}
}
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
namespace DocumentManagement.Core.Services
{
public interface IDocumentStore
{
Task SaveAsync(Document entity, CancellationToken cancellationToken = default);
Task<Document?> GetAsync(string id, CancellationToken cancellationToken = default);
}
}
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Models;
namespace DocumentManagement.Core.Services
{
public interface IDocumentTypeStore
{
Task<IEnumerable<DocumentType>> ListAsync(CancellationToken cancellationToken = default);
Task<DocumentType?> GetAsync(string id, CancellationToken cancellationToken = default);
}
}
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace DocumentManagement.Core.Services
{
public interface IFileStorage
{
Task WriteAsync(Stream data, string fileName, CancellationToken cancellationToken = default);
Task<Stream> ReadAsync(string fileName, CancellationToken cancellationToken = default);
}
}
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";
}
<div>
<h1 class="display-4">Upload Document</h1>
<p>Upload a file for processing</p>
</div>
<form method="post" enctype="multipart/form-data">
<div class="mb-3">
<label asp-for="DocumentTypeId" class="form-label">Document Type</label>
<select asp-for="DocumentTypeId" asp-items="Model.DocumentTypes" class="form-select"></select>
</div>
<div class="mb-3">
<label asp-for="FileUpload" class="form-label">File</label>
<input asp-for="FileUpload" type="file" class="form-control"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DocumentManagement.Core.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Open.Linq.AsyncExtensions;
namespace DocumentManagement.Web.Pages
{
public class IndexModel : PageModel
{
private readonly IDocumentTypeStore _documentTypeStore;
private readonly IDocumentService _documentService;
public IndexModel(IDocumentTypeStore documentTypeStore, IDocumentService documentService)
{
_documentTypeStore = documentTypeStore;
_documentService = documentService;
}
[BindProperty] public string DocumentTypeId { get; set; } = default!;
[BindProperty] public IFormFile FileUpload { get; set; } = default!;
public ICollection<SelectListItem> DocumentTypes { get; set; } = new List<SelectListItem>();
public async Task OnGetAsync(CancellationToken cancellationToken)
{
var documentTypes = await _documentTypeStore.ListAsync(cancellationToken).ToList();
DocumentTypes = documentTypes.Select(x => new SelectListItem(x.Name, x.Id)).ToList();
}
public async Task<IActionResult> OnPostAsync(CancellationToken cancellationToken)
{
var extension = Path.GetExtension(FileUpload.FileName);
var fileName = $"{Guid.NewGuid()}{extension}";
var fileStream = FileUpload.OpenReadStream();
var document = await _documentService.SaveDocumentAsync(fileName, fileStream, DocumentTypeId, cancellationToken);
return RedirectToPage("FileReceived", new {DocumentId = document.Id});
}
}
}
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}
using System;
namespace DocumentManagement.Core.Services
{
public interface ISystemClock
{
DateTime UtcNow { get; }
}
}
using DocumentManagement.Core.Models;
using MediatR;
namespace DocumentManagement.Core.Events
{
/// <summary>
/// Published when a new document was uploaded into the system.
/// </summary>
public record NewDocumentReceived(Document Document) : INotification;
}
using DocumentManagement.Core.Services;
using DocumentManagement.Persistence.HostedServices;
using DocumentManagement.Persistence.Services;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
namespace DocumentManagement.Persistence.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddDomainPersistence(this IServiceCollection services, string connectionString)
{
var migrationsAssemblyName = typeof(SqliteDocumentDbContextFactory).Assembly.GetName().Name;
return services
.AddPooledDbContextFactory<DocumentDbContext>(x => x.UseSqlite(connectionString, db => db.MigrationsAssembly(migrationsAssemblyName)))
.AddSingleton<IDocumentStore, EFCoreDocumentStore>()
.AddSingleton<IDocumentTypeStore, EFCoreDocumentTypeStore>()
.AddHostedService<RunMigrations>();
}
}
}
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace DocumentManagement.Persistence.HostedServices
{
/// <summary>
/// Executes EF Core migrations.
/// </summary>
public class RunMigrations : IHostedService
{
private readonly IDbContextFactory<DocumentDbContext> _dbContextFactory;
public RunMigrations(IDbContextFactory<DocumentDbContext> dbContextFactoryFactory)
{
_dbContextFactory = dbContextFactoryFactory;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
await using var dbContext = _dbContextFactory.CreateDbContext();
await dbContext.Database.MigrateAsync(cancellationToken);
await dbContext.DisposeAsync();
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
}
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace DocumentManagement.Persistence
{
public class SqliteDocumentDbContextFactory : IDesignTimeDbContextFactory<DocumentDbContext>
{
public DocumentDbContext CreateDbContext(string[] args)
{
var builder = new DbContextOptionsBuilder<DocumentDbContext>();
var connectionString = "Data Source=elsa.db;Cache=Shared";
builder.UseSqlite(connectionString);
return new DocumentDbContext(builder.Options);
}
}
}
using System;
namespace DocumentManagement.Core.Services
{
public class SystemClock : ISystemClock
{
public DateTime UtcNow => DateTime.UtcNow;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment