Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 5 - Component Implementation
[.NET, C#, OpenSource, Design]
This is Part 5 of a series on Designing, Building & Packaging A Scalable, Testable .NET Open Source Component.
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 1 - Introduction
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 2 - Basic Requirements
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 3 - Project Setup
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 4 - Types & Contracts
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 5 - Component Implementation (This Post)
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 6 - Mocking & Behaviour Tests
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 7 - Sequence Verification With Moq
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 8 - Compressor Implementation
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 9 - Encryptor Implementation
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 10 - In Memory Storage
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 11 - SQL Server Storage
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 12 - PostgreSQL Storage
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 13 - Database Configuration
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 14 - Virtualizing Infrastructure
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 15 - Test Organization
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 16 - Large File Consideration
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 17 - Large File Consideration On PostgreSQL
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 18 - Azure Blob Storage
- Designing, Building & Packaging A Scalable, Testable .NET Open Source Component - Part 19 - Testing Azure Blob Storage Locally
In our last post, we set up the contracts and types we will use.
In this post, we will start implementing our component.
We previously discussed how the component would have services injected to perform the actual work.
We shall have injected these via constructor injection, which we have discussed earlier.
The initial skeleton will look like this:
namespace UploadFileManager;
public sealed class UploadFileManager : IUploadFileManager
{
private readonly IFileCompressor _fileCompressor;
private readonly IFileEncryptor _fileEncryptor;
private readonly IFilePersistor _filePersistor;
private readonly TimeProvider _timeProvider;
public UploadFileManager(IFilePersistor filePersistor, IFileEncryptor fileEncryptor, IFileCompressor fileCompressor,
TimeProvider timeProvider)
{
// Check that the injected services are valid
ArgumentNullException.ThrowIfNull(filePersistor);
ArgumentNullException.ThrowIfNull(fileEncryptor);
ArgumentNullException.ThrowIfNull(fileCompressor);
ArgumentNullException.ThrowIfNull(timeProvider);
_filePersistor = filePersistor;
_fileEncryptor = fileEncryptor;
_fileCompressor = fileCompressor;
_timeProvider = timeProvider;
}
/// <summary>
/// Upload the file
/// </summary>
/// <param name="fileName"></param>
/// <param name="extension"></param>
/// <param name="data"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<FileMetadata> UploadFileAsync(string fileName, string extension, Stream data,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// Fetch the file metadata
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<FileMetadata> FetchMetadataAsync(Guid fileId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// Get the file
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<Stream> DownloadFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// Delete the file
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<Stream> DeleteFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
/// <summary>
/// Check if the file exists by ID
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public Task<bool> FileExistsAsync(Guid fileId, CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
}
}
Note that there aren’t any concrete implementations of the services; we are just injecting the interfaces. Note also that we haven’t written any implementations for the methods.
We shall proceed to stitch together the injected services to do the actual work.
First, we implement the UploadFileAsync
method.
/// <summary>
/// Upload the file
/// </summary>
/// <param name="fileName"></param>
/// <param name="extension"></param>
/// <param name="data"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<FileMetadata> UploadFileAsync(string fileName, string extension, Stream data,
CancellationToken cancellationToken = default)
{
//Verify the passed in parameters are not null
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
ArgumentException.ThrowIfNullOrWhiteSpace(extension);
ArgumentNullException.ThrowIfNull(data);
// Verify the fileName has valid characters
var invalidCharacters = Path.GetInvalidFileNameChars();
if (invalidCharacters.Any(fileName.Contains))
throw new ArgumentException($"The file name '{fileName}' contains invalid characters");
// Verify the extension has valid characters
if (invalidCharacters.Any(extension.Contains))
throw new ArgumentException($"The extension '{extension}' contains invalid characters");
// Validate the regex for the extension
if (!Regex.IsMatch(extension, @"^\.\w+$"))
throw new ArgumentException($"The extension {extension}' does not conform to the expected format: .xxx");
//
// Now carry out the work
//
// Compress the data
var compressed = _fileCompressor.Compress(data);
// Encrypt the data
var encrypted = _fileEncryptor.Encrypt(compressed);
// Build the metadata
var fileID = Guid.CreateVersion7();
byte[] hash;
// Get a SHA256 hash of the original contents
using (var sha = SHA256.Create())
hash = await sha.ComputeHashAsync(data, cancellationToken);
// Construct the metadata object
var metadata = new FileMetadata
{
FileId = fileID,
Name = fileName,
Extension = extension,
DateUploaded = _timeProvider.GetLocalNow().DateTime,
OriginalSize = data.Length,
PersistedSize = encrypted.Length,
CompressionAlgorithm = _fileCompressor.CompressionAlgorithm,
EncryptionAlgorithm = _fileEncryptor.EncryptionAlgorithm,
Hash = hash
};
// Persist the file data
await _filePersistor.StoreFileAsync(fileName, extension, encrypted, cancellationToken);
return metadata;
}
A couple of things here:
- For clarity, we have modified the
FileMetadata
type and renamedCompressedSize
toPersistedSize
. - We are using the CreateVersion7 of the Guid to generate sequential
Guids
, to reduce complications of storage if a database store is used. - We are injecting a TimeProvider to make date-based testing easier.
Next, we will implement the FetchMetadataAsync
method:
/// <summary>
/// Get the file metadata
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<FileMetadata> FetchMetadataAsync(Guid fileId, CancellationToken cancellationToken = default)
{
// Verify that the file exists first
if (await _filePersistor.FileExistsAsync(fileId, cancellationToken))
return await _filePersistor.GetMetadataAsync(fileId, cancellationToken);
throw new FileNotFoundException($"The file '{fileId}' was not found");
}
Next, the DownloadFileAsync
method:
/// <summary>
/// Get the file by ID
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<Stream> DownloadFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
// Verify that the file exists first
if (await _filePersistor.FileExistsAsync(fileId, cancellationToken))
{
// Get the persisted file contents
var persistedData = await _filePersistor.GetFileAsync(fileId, cancellationToken);
// Decrypt the data
var decryptedData = _fileEncryptor.Decrypt(persistedData);
// Decompress the decrypted ata
var uncompressedData = _fileCompressor.DeCompress(decryptedData);
return uncompressedData;
}
throw new FileNotFoundException($"The file '{fileId}' was not found");
}
Next, the DeleteFileAsync
method:
/// <summary>
/// Delete the file by ID
/// </summary>
/// <param name="fileId"></param>
/// <param name="cancellationToken"></param>
public async Task DeleteFileAsync(Guid fileId, CancellationToken cancellationToken = default)
{
// Verify that the file exists first
if (await _filePersistor.FileExistsAsync(fileId, cancellationToken))
await _filePersistor.DeleteFileAsync(fileId, cancellationToken);
throw new FileNotFoundException($"The file '{fileId}' was not found");
}
At this point, our implementation is complete, insofar as we are using contracts internally and no concrete types. We shall implement those later.
In our next post, we will see how to test our component design and contracts, even though we haven’t implemented any of its services.
TLDR
This post implemented the functionality of the UploadFileManager
component
The code is in my GitHub.
Happy hacking!