EonaCat.Blocks 1.0.0

Prefix Reserved
dotnet add package EonaCat.Blocks --version 1.0.0
                    
NuGet\Install-Package EonaCat.Blocks -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="EonaCat.Blocks" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="EonaCat.Blocks" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="EonaCat.Blocks" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add EonaCat.Blocks --version 1.0.0
                    
#r "nuget: EonaCat.Blocks, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package EonaCat.Blocks@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=EonaCat.Blocks&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=EonaCat.Blocks&version=1.0.0
                    
Install as a Cake Tool

EonaCat.Blocks

Building blocks for a modular application using C# and dependency injection.

Example block that demonstrates how to use the provided building blocks to create a simple modular application:
using System;
using EonaCat.Blocks;

public class UserCreatedEvent
{
    public Guid UserId { get; }
    public string UserName { get; }

    public UserCreatedEvent(Guid userId, string userName)
    {
        UserId = userId;
        UserName = userName;
    }
}

public class UserComponent : IComponent
{
    private readonly IEventBus _eventBus;

    public UserComponent(IEventBus eventBus)
    {
        _eventBus = eventBus;
    }

    public void Initialize()
    {
        Console.WriteLine("UserComponent Initialized");
    }

    public void Execute()
    {
        // Simulate user creation
        var userId = Guid.NewGuid();
        var userName = "Alice";
        Console.WriteLine($"User created: {userName}");

        _eventBus.Publish(new UserCreatedEvent(userId, userName));
    }

    public void Dispose()
    {
        Console.WriteLine("UserComponent Disposed");
    }
}
Example on how to use the building blocks in a modular application:
var serviceProvider = new ServiceProvider();

// Register services
var eventBus = new EventBus();
serviceProvider.RegisterSingleton<IEventBus>(eventBus);
serviceProvider.RegisterSingleton<IConfiguration>(new InMemoryConfiguration());

// Register UserComponent with injected eventBus
var userComponent = new UserComponent(serviceProvider.GetService<IEventBus>());
serviceProvider.RegisterSingleton<IComponent>(userComponent);

// Initialize and execute
var logger = serviceProvider.GetService<ILogger>();
var component = serviceProvider.GetService<IComponent>();

component.Initialize();
logger.LogInfo("Starting component execution...");
component.Execute();
component.Dispose();
Example on how to create a validator:
public class User
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

public interface IValidator<T>
{
    IEnumerable<string> Validate(T instance);
}

public class RegisterUserCommandValidator : IValidator<RegisterUserCommand>
{
    public IEnumerable<string> Validate(RegisterUserCommand command)
    {
        if (string.IsNullOrWhiteSpace(command.Name))
            yield return "Name is required.";
        if (command.Age < 0)
            yield return "Age must be non-negative.";
    }
}
Example on how to use Commands and Handlers:
public class RegisterUserCommand
{
    public string Name { get; }
    public int Age { get; }

    public RegisterUserCommand(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

public interface ICommandHandler<TCommand>
{
    Task HandleAsync(TCommand command);
}

public class RegisterUserCommandHandler : ICommandHandler<RegisterUserCommand>
{
    private readonly DomainEventDispatcher _dispatcher;
    private readonly ICache _cache;
    private readonly ILogger _logger;

    public RegisterUserCommandHandler(DomainEventDispatcher dispatcher, ICache cache, ILogger logger)
    {
        _dispatcher = dispatcher;
        _cache = cache;
        _logger = logger;
    }

    public async Task HandleAsync(RegisterUserCommand command)
    {
        _logger.LogInfo($"Registering user {command.Name}");

        var user = new User { Name = command.Name, Age = command.Age };

        // Simulate DB save with cache
        _cache.Set(user.Id.ToString(), user);

        // Raise domain event
        await _dispatcher.DispatchAsync(new UserCreatedEvent(user.Id, user.Name));

        _logger.LogInfo("User registration completed");
    }
}
Example on how to use the Event Aggegregator:
public class UserCreated
{
    public Guid Id { get; }
    public string Name { get; }
    public UserCreated(Guid id, string name)
    {
        Id = id;
        Name = name;
    }
}

public class UserNameChanged
{
    public string NewName { get; }
    public UserNameChanged(string newName) => NewName = newName;
}
public class UserAggregate : AggregateRoot
{
    public string Name { get; private set; }

    // Constructor for creating new user
    public UserAggregate(Guid id, string name)
    {
        ApplyChange(new UserCreated(id, name));
    }

    // Empty constructor for rehydration
    public UserAggregate() { }

    public void ChangeName(string newName)
    {
        if (string.IsNullOrWhiteSpace(newName)) throw new ArgumentException("Name required");
        ApplyChange(new UserNameChanged(newName));
    }

    // Event handlers to update state

    public void Apply(UserCreated e)
    {
        Id = e.Id;
        Name = e.Name;
        Version++;
    }

    public void Apply(UserNameChanged e)
    {
        Name = e.NewName;
        Version++;
    }
}
var user = new UserAggregate(Guid.NewGuid(), "Alice");

user.ChangeName("Alice Smith");

foreach (var evt in user.GetUncommittedChanges())
{
    Console.WriteLine(evt.GetType().Name);
}

Outputs:

UserCreated
UserNameChanged
Example on how to use Create a Domain Event:
public class UserCreatedEvent : IDomainEvent
{
    public Guid UserId { get; }
    public string UserName { get; }

    public UserCreatedEvent(Guid userId, string userName)
    {
        UserId = userId;
        UserName = userName;
    }
}
public class WelcomeEmailSender
{
    private readonly ILogger _logger;

    public WelcomeEmailSender(ILogger logger)
    {
        _logger = logger;
    }

    public Task OnUserCreated(UserCreatedEvent e)
    {
        _logger.LogInfo($"Sending welcome email to {e.UserName}");
        // Simulate sending email
        return Task.CompletedTask;
    }
}
Example on how to use the middleware pipeline component:
public class LoggingMiddleware : IMiddleware
{
    private readonly ILogger _logger;

    public LoggingMiddleware(ILogger logger)
    {
        _logger = logger;
    }

    public async Task InvokeAsync(RequestDelegate next)
    {
        _logger.LogInfo("Middleware: Before command");
        await next();
        _logger.LogInfo("Middleware: After command");
    }
}
public class ValidationMiddleware<TCommand> : IMiddleware
{
    private readonly IValidator<TCommand> _validator;
    private readonly TCommand _command;

    public ValidationMiddleware(IValidator<TCommand> validator, TCommand command)
    {
        _validator = validator;
        _command = command;
    }

    public Task InvokeAsync(RequestDelegate next)
    {
        var errors = _validator.Validate(_command);
        foreach (var error in errors)
        {
            throw new Exception($"Validation failed: {error}");
        }
        return next();
    }
}
Example on how to use create a background Cache Cleaner Task:
public class CacheCleanerTask : IBackgroundTask
{
    private readonly ICache _cache;
    private readonly ILogger _logger;

    public CacheCleanerTask(ICache cache, ILogger logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public Task ExecuteAsync(CancellationToken token)
    {
        _logger.LogInfo("Running cache cleaning task...");
        // Here you'd implement real cache cleanup, just simulating:
        return Task.CompletedTask;
    }
}
Example on how to use the retry policy:
var pipeline = new MiddlewarePipeline();
pipeline.Use(new LoggingMiddleware(logger));
pipeline.Use(new ValidationMiddleware<RegisterUserCommand>(validator, command));

try
{
    // Run pipeline with retry policy wrapping command execution
    await pipeline.ExecuteAsync(async () =>
    {
        await RetryPolicy.RetryAsync(() => handler.HandleAsync(command), retryCount: 3);
    });
}
catch (Exception ex)
{
    logger.LogError("Command execution failed.", ex);
}
Example on how to use the in-memory cache:
var cache = new InMemoryCache();
cache.Set("key1", "value1");
var value = cache.Get<string>("key1");
Console.WriteLine($"Cached value: {value}"); // Output: Cached value: value1
cache.Remove("key1");
Example on how to use the fluent validator:
var validator = new FluentValidator<User>()
    .Rule(u => !string.IsNullOrWhiteSpace(u.Name), "Name is required")
    .Rule(u => u.Age >= 0, "Age must be non-negative");

var errors = validator.Validate(user);
Example on how to use the AsyncEventBus and the CommandHandler:
var eventBus = new AsyncEventBus();
var commandBus = new CommandBus();

// Subscribe to UserCreatedEvent
eventBus.Subscribe<UserCreatedEvent>(async e =>
{
    Console.WriteLine($"Event handler: Welcome {e.UserName}!");
    await Task.CompletedTask;
});

// Register command handler for RegisterUserCommand
commandBus.RegisterHandler<RegisterUserCommand>(async cmd =>
{
    Console.WriteLine($"Command handler: Registering user {cmd.UserName}...");
    // After processing command, publish event
    await eventBus.PublishAsync(new UserCreatedEvent(cmd.UserName));
});

// Send command
await commandBus.SendAsync(new RegisterUserCommand("Alice"));
Example on how to use the Circuit breaker:
public class FlakyService
{
    private readonly Random _rnd = new();

    public async Task<string> CallAsync(CancellationToken token)
    {
        await Task.Delay(300, token);
        if (_rnd.NextDouble() < 0.6) throw new Exception("Service failed");
        return "βœ… Success!";
    }
}

class Program
{
    static async Task Main()
    {
        var breaker = new CircuitBreaker(
            failureThreshold: 3,
            resetTimeout: TimeSpan.FromSeconds(5)
        );

        var service = new FlakyService();

        for (int i = 1; i <= 10; i++)
        {
            try
            {
                Console.WriteLine($"\n➑ Attempt {i}");
                var result = await breaker.ExecuteAsync(
                    action: service.CallAsync,
                    timeout: TimeSpan.FromSeconds(1),
                    fallback: () => Task.FromResult("πŸ›Ÿ Fallback result"),
                    retryCount: 1
                );
                Console.WriteLine($"βœ… Got result: {result}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"❌ Final failure: {ex.Message}");
            }

            await Task.Delay(1000); // delay between attempts
        }
    }
}
Outputs:

➑ Attempt 1
⚠ Attempt 1 failed: Service failed
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 2
⚠ Attempt 1 failed: Service failed
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 3
⚠ Attempt 1 failed: Service failed
πŸ”Œ Circuit breaker OPENED
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 4
β›” Circuit breaker is OPEN
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 5
β›” Circuit breaker is OPEN
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 6
β›” Circuit breaker is OPEN
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 7
πŸ” Reset timeout expired. Trying HALF-OPEN state...
⚠ Attempt 1 failed: Service failed
πŸ”Œ Circuit breaker OPENED
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 8
β›” Circuit breaker is OPEN
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 9
β›” Circuit breaker is OPEN
➑ Executing fallback
βœ… Got result: πŸ›Ÿ Fallback result

➑ Attempt 10
πŸ” Reset timeout expired. Trying HALF-OPEN state...
βœ… Got result: πŸŽ‰ Success!
Example on how to use the Audit trail:
class BankService
{
    private readonly AuditTrail _audit;
    private decimal _balance = 1000;

    public BankService(AuditTrail audit)
    {
        _audit = audit;
    }

    public void Deposit(string user, decimal amount)
    {
        _balance += amount;
        _audit.Record(user, "Deposit", $"Deposited {amount:C}. New Balance: {_balance:C}");
    }

    public void Withdraw(string user, decimal amount)
    {
        if (amount > _balance)
        {
            _audit.Record(user, "Failed Withdraw", $"Attempted to withdraw {amount:C} with insufficient funds.");
            throw new InvalidOperationException("Insufficient funds.");
        }

        _balance -= amount;
        _audit.Record(user, "Withdraw", $"Withdrew {amount:C}. New Balance: {_balance:C}");
    }

    public decimal GetBalance() => _balance;
}
var auditTrail = new AuditTrail();
        var bank = new BankService(auditTrail);

        try
        {
            bank.Deposit("alice", 250);
            bank.Withdraw("alice", 100);
            bank.Withdraw("bob", 1500); // Will fail and audit that too
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ Error: {ex.Message}");
        }

        Console.WriteLine("\nπŸ“‹ Audit Trail:");
        foreach (var entry in auditTrail.GetAll())
        {
            Console.WriteLine($"{entry.Timestamp:u} | {entry.User} | {entry.Action} | {entry.Details}");
        }
Outputs:

πŸ“‹ Audit Trail:
2025-06-28 16:10:21Z | alice | Deposit | Deposited $250.00. New Balance: $1,250.00
2025-06-28 16:10:21Z | alice | Withdraw | Withdrew $100.00. New Balance: $1,150.00
2025-06-28 16:10:21Z | bob | Failed Withdraw | Attempted to withdraw $1,500.00 with insufficient funds.
Example on how to use the outbox pattern:
public class UserService
{
    private readonly Outbox _outbox;

    public UserService(Outbox outbox)
    {
        _outbox = outbox;
    }

    public void ChangeEmail(string userId, string newEmail)
    {
        // Imagine DB update here...
        Console.WriteLine($"πŸ“₯ User {userId} email changed to {newEmail}");

        var payload = $"{{ \"userId\": \"{userId}\", \"newEmail\": \"{newEmail}\" }}";
        _outbox.SaveEvent("UserEmailChanged", payload);
    }
}
public class OutboxDispatcher
{
    private readonly Outbox _outbox;

    public OutboxDispatcher(Outbox outbox)
    {
        _outbox = outbox;
    }

    public void Dispatch()
    {
        foreach (var msg in _outbox.GetPending())
        {
            // Simulate publishing
            Console.WriteLine($"πŸ“€ Dispatching Event: {msg.EventType} => {msg.Payload}");
            _outbox.MarkAsDispatched(msg.Id);
        }
    }
}
var outbox = new Outbox();
var userService = new UserService(outbox);
var dispatcher = new OutboxDispatcher(outbox);

// Simulate business logic
userService.ChangeEmail("user-123", "alice@example.com");
userService.ChangeEmail("user-456", "bob@example.com");

Console.WriteLine("\n⏳ Outbox before dispatch:");
foreach (var msg in outbox.GetPending())
{
    Console.WriteLine($"⏺️  {msg.EventType} - {msg.Payload}");
}

// Now dispatch
Console.WriteLine("\n🚚 Dispatching Outbox...");
dispatcher.Dispatch();

Console.WriteLine("\nβœ… Final Outbox State:");
foreach (var msg in outbox.GetAll())
{
    Console.WriteLine($"🧾 {msg.EventType} | Sent: {msg.Dispatched}");
}
Outputs:

πŸ“₯ User user-123 email changed to alice@example.com
πŸ“₯ User user-456 email changed to bob@example.com

⏳ Outbox before dispatch:
⏺️  UserEmailChanged - { "userId": "user-123", "newEmail": "alice@example.com" }
⏺️  UserEmailChanged - { "userId": "user-456", "newEmail": "bob@example.com" }

🚚 Dispatching Outbox...
πŸ“€ Dispatching Event: UserEmailChanged => { "userId": "user-123", "newEmail": "alice@example.com" }
πŸ“€ Dispatching Event: UserEmailChanged => { "userId": "user-456", "newEmail": "bob@example.com" }

βœ… Final Outbox State:
🧾 UserEmailChanged | Sent: True
🧾 UserEmailChanged | Sent: True
Example on how to use the Saga Orchestrator:
public class PaymentStep : ISagaStep
{
    public async Task<bool> ExecuteAsync()
    {
        Console.WriteLine("Processing payment...");
        await Task.Delay(500);
        Console.WriteLine("Payment processed.");
        return true; // success
    }

    public async Task CompensateAsync()
    {
        Console.WriteLine("Compensating payment...");
        await Task.Delay(300);
        Console.WriteLine("Payment refunded.");
    }
}
public class InventoryStep : ISagaStep
{
    public async Task<bool> ExecuteAsync()
    {
        Console.WriteLine("Updating inventory...");
        await Task.Delay(500);
        Console.WriteLine("Inventory updated.");
        return false; // simulate failure to trigger compensation
    }

    public async Task CompensateAsync()
    {
        Console.WriteLine("Reverting inventory update...");
        await Task.Delay(300);
        Console.WriteLine("Inventory reverted.");
    }
}
class Program
{
    static async Task Main()
    {
        var saga = new SagaOrchestrator();
        saga.AddStep(new PaymentStep());
        saga.AddStep(new InventoryStep());

        bool result = await saga.RunAsync();
        Console.WriteLine(result ? "Saga completed successfully." : "Saga failed and compensated.");
    }
}
Outputs:

Processing payment...
Payment processed.
Updating inventory...
Inventory updated.
Step failed, compensating previous steps...
Payment refunded.
Saga failed and compensated.
Example on how to use the Bulkhead:
class Program
{
    static Bulkhead bulkhead = new Bulkhead(2);

    static async Task<string> ProtectedCall(int id)
    {
        Console.WriteLine($"[{id}] Waiting for slot...");
        var result = await bulkhead.ExecuteAsync(async () =>
        {
            Console.WriteLine($"[{id}] Running...");
            await Task.Delay(1000);
            Console.WriteLine($"[{id}] Done.");
            return $"Result from {id}";
        });
        return result;
    }

    static async Task Main()
    {
        var tasks = new Task<string>[5];
        for (int i = 0; i < 5; i++)
        {
            int capture = i;
            tasks[i] = ProtectedCall(capture);
        }

        var results = await Task.WhenAll(tasks);
        foreach (var res in results)
        {
            Console.WriteLine(res);
        }
    }
}
Example on how to use the lazy initialization:
class Program
{
    static AsyncLazy<string> lazyResource = new AsyncLazy<string>(async () =>
    {
        Console.WriteLine("Starting async initialization...");
        await Task.Delay(2000);
        Console.WriteLine("Initialization complete.");
        return "Resource Ready";
    });

    static async Task UseResource(int id)
    {
        Console.WriteLine($"[{id}] Requesting resource...");
        var resource = await lazyResource.Value;
        Console.WriteLine($"[{id}] Got resource: {resource}");
    }

    static async Task Main()
    {
        var tasks = new Task[]
        {
            UseResource(1),
            UseResource(2),
            UseResource(3)
        };
        await Task.WhenAll(tasks);
    }
}
Outputs:

[1] Requesting resource...
Starting async initialization...
[2] Requesting resource...
[3] Requesting resource...
Initialization complete.
[1] Got resource: Resource Ready
[2] Got resource: Resource Ready
[3] Got resource: Resource Ready
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 88 6/28/2025