This document describes the functional programming refactoring of the CryptAByte codebase. The refactoring transforms imperative, mutation-heavy code into pure, composable functions with explicit data flow and isolated I/O operations.
- Functions always return the same output for the same input
- No hidden dependencies (like
DateTime.Now, static RNG calls) - No side effects (no mutations, no I/O)
- Deterministic and easily testable
- All domain models are immutable after construction
- Changes create new instances rather than mutating existing ones
- Thread-safe by default
- Enables safe sharing across contexts
- All dependencies are injected (time provider, random generator, crypto functions)
- No output parameters - use return values or Result types
- Function signatures clearly show all inputs and outputs
- Pure business logic separated from I/O operations
- Database, file system, network, and RNG operations pushed to boundaries
- Easy to test business logic without I/O infrastructure
Result<TValue, TError>type for operations that can failOption<T>type for optional values (eliminates null reference exceptions)- No silent exception handling - all errors are explicit in function signatures
A discriminated union representing success or failure:
// Creating results
var success = Result.Success<int, string>(42);
var failure = Result.Failure<int, string>("Something went wrong");
// Transforming results
var doubled = success.Map(x => x * 2); // Success(84)
// Chaining operations
var result = GetUser(id)
.Bind(user => GetUserPreferences(user.Id))
.Map(prefs => prefs.Theme);
// Pattern matching
result.Match(
onSuccess: theme => Console.WriteLine($"Theme: {theme}"),
onFailure: error => Console.WriteLine($"Error: {error}")
);Key Methods:
Map<TResult>()- Transform success valueBind<TResult>()- Chain operations that return ResultMatch<TResult>()- Pattern match on success/failureSequence()- ConvertIEnumerable<Result<T>>toResult<IEnumerable<T>>
Represents an optional value (explicit null handling):
// Creating options
var some = Option.Some("value");
var none = Option.None<string>();
// From nullable
var option = Option.FromNullable(nullableString);
// Transforming
var upper = some.Map(s => s.ToUpper()); // Some("VALUE")
// Pattern matching
option.Match(
onSome: value => Console.WriteLine(value),
onNone: () => Console.WriteLine("No value")
);
// Get value or default
var value = option.GetValueOrDefault("default");Key Methods:
Map<TResult>()- Transform the value if presentBind<TResult>()- Chain operations that return OptionWhere()- Filter based on predicateMatch<TResult>()- Pattern match on Some/None
Abstracts current time to eliminate temporal coupling:
public interface ITimeProvider
{
DateTime Now { get; }
DateTime UtcNow { get; }
}
// Production use
var timeProvider = new SystemTimeProvider();
// Testing use
var timeProvider = new FixedTimeProvider(new DateTime(2025, 1, 1));Abstracts random number generation:
public interface IRandomGenerator
{
byte[] GenerateBytes(int length);
string GenerateBase64String(int sizeInBytes);
}
// Production use
var rng = new CryptoRandomGenerator();
// Testing use
var rng = new DeterministicRandomGenerator(0x42);Before:
// Output parameters, hidden dependencies
string EncryptMessageWithKey(string message, string publicKey,
out string encryptedPassword, out string hashOfMessage)
{
string encryptionKey = SymmetricCryptoProvider.GenerateKeyPhrase(); // Static call!
// ...
}After:
// Explicit dependencies, immutable result
public AsymmetricCryptoProvider(
SymmetricCryptoProvider symmetricProvider,
IRandomGenerator randomGenerator)
public AsymmetricEncryptionResult EncryptMessageWithKey(
string message, string publicKey)
{
var encryptionKey = _randomGenerator.GenerateBase64String(128);
// Returns immutable AsymmetricEncryptionResult
}
// Explicit error handling
public Result<DecryptedData, string> DecryptMessageWithKey(
string privateKey, string messageData,
string encryptedDecryptionKey, string hashOfMessage)
{
// Returns Result instead of throwing exceptions
}Improvements:
- ✅ Eliminated output parameters
- ✅ Injected dependencies (SymmetricCryptoProvider, IRandomGenerator)
- ✅ Returns immutable value objects
- ✅ Explicit error handling with Result type
- ✅ Legacy methods preserved with
[Obsolete]attribute
Improvements:
- ✅ Marked
GenerateKeyPhrase()as obsolete (use IRandomGenerator instead) - ✅ Added pure
CryptoFunctionsstatic class for hash operations - ✅ Input validation on all methods
- ✅ Documented purity guarantees
Immutable data structures for cryptographic operations:
EncryptedData- Cipher text + IVAsymmetricEncryptionResult- Encrypted message, key, hash, IVDecryptedData- Plain text, decryption key, hashKeyPair- RSA public/private key pairProtectedKeyPair- Passphrase-encrypted key pairFileAttachment- File name, data, Base64 ZIP
Before:
byte[] DecodeAndDecompressFile(string base64ZippedData, out string fileName)
{
// Output parameter, exceptions for errors
}After:
Result<DecompressedFile, string> DecodeAndDecompressFile(string base64ZippedData)
{
// Returns immutable DecompressedFile or error
}Improvements:
- ✅ Eliminated output parameters
- ✅ Returns
Result<T, string>for explicit error handling - ✅ Immutable
DecompressedFilevalue object - ✅ All methods are pure transformations
- ✅ Legacy methods preserved with
[Obsolete]attribute
Immutable versions of EF entities for business logic:
ImmutableCryptoKey- Immutable crypto key with pure methodsImmutableMessage- Immutable messageImmutableNotification- Immutable notification
Key Features:
// Pure transformation methods
public ImmutableCryptoKey WithMessages(IEnumerable<ImmutableMessage> newMessages);
public ImmutableCryptoKey WithoutPrivateKey();
// Pure validation
public bool IsReleased(DateTime currentTime);
public Option<string> GetPrivateKeyIfReleased(DateTime currentTime);
// Conversions at I/O boundary
public static ImmutableCryptoKey FromEntity(CryptoKey entity);
public CryptoKey ToEntity();Immutable self-destructing messages:
ImmutableSelfDestructingMessageImmutableSelfDestructingMessageAttachment
Pure functions for message operations:
// Decrypt and decompress without mutations
Result<ImmutableMessage, string> DecryptAndDecompress(
ImmutableMessage encryptedMessage,
string privateKey,
Func<...> decryptFunction);
// Validate key for reading
Result<ImmutableCryptoKey, string> ValidateKeyForReading(
ImmutableCryptoKey cryptoKey,
DateTime currentTime);
// Decrypt private key with passphrase
Result<string, string> DecryptPrivateKey(
ImmutableCryptoKey cryptoKey,
string passphrase,
Func<...> decryptFunction,
Func<...> hashFunction);Pure functions for crypto key operations:
// Create keys with explicit dependencies
ImmutableCryptoKey CreateWithGeneratedKeys(
string keyToken,
DateTime requestDate,
DateTime releaseDate,
Func<KeyPair> generateKeysFunction);
// Pure transformation
ImmutableCryptoKey MakePublicKeyOnly(ImmutableCryptoKey cryptoKey);The repositories need the most significant refactoring. Follow this pattern:
// BEFORE: Mixed concerns
public class RequestRepository
{
public CryptoKey GetByToken(string token) { } // Query
public void AttachMessage(string token, ...) { } // Command
}
// AFTER: Separate interfaces
public interface IRequestQueries
{
Result<ImmutableCryptoKey, string> GetByToken(string token);
Result<IReadOnlyList<ImmutableMessage>, string> GetMessages(string token);
}
public interface IRequestCommands
{
Result<Unit, string> SaveCryptoKey(ImmutableCryptoKey key);
Result<Unit, string> AttachMessage(string token, ImmutableMessage message);
Result<Unit, string> DeleteMessages(IEnumerable<int> messageIds);
}// BEFORE: Business logic mixed with I/O
public void AttachMessageToRequest(string token, string message, string publicKey)
{
var request = _context.Keys.Find(token); // I/O
var hash = SymmetricCryptoProvider.GetSecureHashForString(message); // Logic
var encrypted = crypto.EncryptMessageWithKey(message, publicKey,
out var key, out var hash); // Logic
request.Messages.Add(new Message { ... }); // Mutation + I/O
_context.SaveChanges(); // I/O
}
// AFTER: Separate pure logic from I/O
// Pure function (in BusinessLogic/)
public static Result<EncryptedMessageData, string> EncryptMessage(
string plaintext,
string publicKey,
IRandomGenerator randomGenerator,
AsymmetricCryptoProvider crypto)
{
// Pure transformation, returns immutable result
}
// Repository (I/O boundary)
public Result<Unit, string> AttachMessage(
string token,
EncryptedMessageData encryptedData)
{
// Just persist the data, no business logic
}// BEFORE: ForEach with side effects
request.Messages.ToList().ForEach(msg => {
msg.MessageData = DecryptMessage(msg); // MUTATION
msg.EncryptionKey = key; // MUTATION
});
// AFTER: Map to new collection
var decryptedMessages = request.Messages
.Select(msg => ImmutableMessage.FromEntity(msg))
.Select(msg => MessageOperations.DecryptAndDecompress(
msg, privateKey, decryptFunction))
.Sequence() // Convert List<Result<T>> to Result<List<T>>
.Map(messages => messages.ToList().AsReadOnly());// BEFORE: Hidden dependency
public bool IsReleased() => ReleaseDate < DateTime.Now; // Not testable!
// AFTER: Explicit dependency
public RequestRepository(
CryptAByteContext context,
ITimeProvider timeProvider,
IEmailService emailService)
{
_timeProvider = timeProvider;
}
public Result<ImmutableCryptoKey, string> GetReleasedKey(string token)
{
return GetByToken(token)
.Bind(key => MessageOperations.ValidateKeyForReading(
key, _timeProvider.UtcNow));
}Controllers should be thin adapters between HTTP and business logic:
// BEFORE: Business logic in controller
public ActionResult GetMessages(string token, string passphrase)
{
try
{
var request = _repo.GetByToken(token);
if (request.ReleaseDate > DateTime.Now)
return new HttpStatusCodeResult(403);
var privateKey = new SymmetricCryptoProvider()
.DecryptWithKey(request.PrivateKey, passphrase);
request.Messages.ToList().ForEach(msg => {
msg.MessageData = DecryptMessage(msg, privateKey);
});
return View(request);
}
catch (Exception ex)
{
return new HttpStatusCodeResult(500);
}
}
// AFTER: Thin adapter with pure business logic
public ActionResult GetMessages(string token, string passphrase)
{
var result = _queries.GetByToken(token)
.Bind(key => MessageOperations.ValidateKeyForReading(
key, _timeProvider.UtcNow))
.Bind(key => MessageOperations.DecryptPrivateKey(
key, passphrase, _crypto.DecryptWithKey,
SymmetricCryptoProvider.GetSecureHashForString))
.Bind(privateKey => _queries.GetMessages(token)
.Bind(messages => MessageOperations.DecryptAndDecompressAll(
messages, privateKey, _crypto.DecryptMessageWithKey)));
return result.Match(
onSuccess: messages => View(messages),
onFailure: error => BadRequest(error)
);
}// BEFORE: Global mutable state
internal static Dictionary<string, TemporaryDownloadKey> FilePasswords
{
get
{
var cache = HttpRuntime.Cache;
if (cache.Get(keyName) == null)
cache[keyName] = new Dictionary<...>(); // SIDE EFFECT
return cache[keyName] as Dictionary<...>;
}
}
// AFTER: Explicit state service
public interface IDownloadTokenService
{
Result<DownloadToken, string> CreateToken(
string messageId,
byte[] fileData,
DateTime expiresAt);
Result<DownloadToken, string> GetToken(string tokenId);
}
public class MemoryCachedDownloadTokenService : IDownloadTokenService
{
private readonly ITimeProvider _timeProvider;
public Result<DownloadToken, string> CreateToken(...)
{
// Immutable DownloadToken objects
// Explicit expiration using ITimeProvider
}
}The functional refactoring makes testing much easier:
[Test]
public void DecryptMessage_WithValidData_ReturnsDecryptedMessage()
{
// Arrange
var message = new ImmutableMessage(...);
var privateKey = "test-key";
var mockDecrypt = (string pk, string data, string key, string hash) =>
Result.Success<DecryptedData, string>(new DecryptedData(...));
// Act
var result = MessageOperations.DecryptAndDecompress(
message, privateKey, mockDecrypt);
// Assert
Assert.That(result.IsSuccess, Is.True);
result.Match(
onSuccess: msg => Assert.That(msg.MessageData, Is.EqualTo("expected")),
onFailure: _ => Assert.Fail()
);
}[Test]
public void ValidateKeyForReading_BeforeReleaseDate_ReturnsFailure()
{
// Arrange
var releaseDate = new DateTime(2025, 12, 31);
var currentTime = new DateTime(2025, 1, 1); // Before release
var key = new ImmutableCryptoKey(..., releaseDate, ...);
// Act
var result = MessageOperations.ValidateKeyForReading(key, currentTime);
// Assert
Assert.That(result.IsFailure, Is.True);
}[Test]
public void EncryptMessage_WithDeterministicRng_ProducesPredictableOutput()
{
// Arrange
var rng = new DeterministicRandomGenerator(0x42);
var crypto = new AsymmetricCryptoProvider(symmetric, rng);
// Act
var result1 = crypto.EncryptMessageWithKey(message, publicKey);
var result2 = crypto.EncryptMessageWithKey(message, publicKey);
// Assert - same inputs = same outputs with deterministic RNG
Assert.That(result1.EncryptedKey, Is.EqualTo(result2.EncryptedKey));
}All refactored code maintains backward compatibility through:
- Legacy method overloads marked with
[Obsolete] - Adapter methods that wrap new functional code
- Gradual migration - old and new code coexist
Example:
// New functional API
public Result<DecryptedData, string> DecryptMessageWithKey(...);
// Legacy API (preserved)
[Obsolete("Use DecryptMessageWithKey returning Result<T> for explicit error handling")]
public string DecryptMessageWithKey(..., out string encryptionKey)
{
var result = DecryptMessageWithKey(...);
return result.Match(
onSuccess: data => { encryptionKey = data.DecryptionKey; return data.PlainText; },
onFailure: error => throw new CryptographicException(error)
);
}- Pure functions can be tested in isolation
- No mocking required for business logic
- Deterministic tests with fixed time and random generators
- Functions have clear, explicit contracts
- No hidden dependencies or side effects
- Easy to understand data flow
- Explicit error handling eliminates silent failures
- Immutability prevents accidental state corruption
- Type safety catches errors at compile time
- Pure functions can be freely composed
- Reusable building blocks
- Higher-order functions enable powerful abstractions
- Immutable data structures are safe to share
- No race conditions from shared mutable state
- Easier concurrent programming
-
Refactor RequestRepository
- Split into IRequestQueries and IRequestCommands
- Extract business logic to MessageOperations
- Eliminate all ForEach mutations
- Use immutable domain models
-
Refactor SelfDestructingMessageRepository
- Apply same patterns as RequestRepository
- Use ImmutableSelfDestructingMessage models
- Explicit error handling with Result types
-
Update Controllers
- Inject ITimeProvider, IRandomGenerator
- Use business logic functions from BusinessLogic/
- Transform to/from immutable models at boundaries
- Return Result types, pattern match for HTTP responses
-
Refactor Application Cache
- Create IDownloadTokenService interface
- Implement with immutable DownloadToken objects
- Inject ITimeProvider for expiration checks
-
Update Dependency Injection
- Register ITimeProvider, IRandomGenerator
- Configure AsymmetricCryptoProvider with dependencies
- Wire up new service interfaces
-
Refactor View Models
- Make view models immutable
- Create pure transformation functions
-
Update Tests
- Use new functional APIs
- Test with fixed time and deterministic RNG
- Add tests for Result type error paths
// Functional approach with explicit dependencies
var keyToken = UniqueIdGenerator.GetUniqueId();
var currentTime = timeProvider.UtcNow;
var cryptoKey = CryptoKeyOperations.CreateWithGeneratedKeys(
keyToken: keyToken,
requestDate: currentTime,
releaseDate: currentTime.AddDays(7),
generateKeysFunction: () => AsymmetricCryptoProvider.GenerateKeys()
);
// Save to database
var saveResult = commands.SaveCryptoKey(cryptoKey);// Get crypto key
var keyResult = queries.GetByToken(token);
// Validate and decrypt
var messagesResult = keyResult
.Bind(key => MessageOperations.ValidateKeyForReading(key, timeProvider.UtcNow))
.Bind(key => MessageOperations.DecryptPrivateKey(
key, passphrase, crypto.DecryptWithKey, CryptoFunctions.ComputeHash))
.Bind(privateKey => queries.GetMessages(token))
.Bind(messages => MessageOperations.DecryptAndDecompressAll(
messages, privateKey, crypto.DecryptMessageWithKey));
// Handle result
messagesResult.Match(
onSuccess: messages => DisplayMessages(messages),
onFailure: error => ShowError(error)
);This refactoring establishes a strong functional programming foundation:
- ✅ Core functional types (Result, Option)
- ✅ Time and randomness abstraction
- ✅ Pure cryptographic providers
- ✅ Immutable domain models
- ✅ Pure business logic functions
- ✅ Explicit error handling
- ✅ I/O isolation patterns
- ✅ Backward compatibility
The remaining work involves applying these patterns throughout the controllers, repositories, and services to complete the transformation to a fully functional architecture.