ScopeBus 1.1.1
dotnet add package ScopeBus --version 1.1.1
NuGet\Install-Package ScopeBus -Version 1.1.1
<PackageReference Include="ScopeBus" Version="1.1.1" />
<PackageVersion Include="ScopeBus" Version="1.1.1" />
<PackageReference Include="ScopeBus" />
paket add ScopeBus --version 1.1.1
#r "nuget: ScopeBus, 1.1.1"
#:package ScopeBus@1.1.1
#addin nuget:?package=ScopeBus&version=1.1.1
#tool nuget:?package=ScopeBus&version=1.1.1
ScopeBus
ScopeBus is a clean and extensible mediator framework for .NET, supporting CQRS, domain events, pipelines, and flexible handler behaviors — with automatic handler registration, minimal ceremony, and maximum control.
Features
- Modular Design: Easily extendable and customizable to fit your needs.
- Mediator Pattern: Implements the mediator pattern to decouple components and promote clean architecture.
- CQRS Support: Built-in support for Command Query Responsibility Segregation (CQRS) to separate read and write operations.
- Domain-Driven Design (DDD): Facilitates the implementation of DDD principles for better organization and maintainability of your code.
- Minimal Setup: Quick and easy to set up, allowing you to focus on building your application rather than configuring the framework.
- Flexible Pipeline: Create custom pipelines to handle various scenarios, including validation, logging, and error handling.
- Asynchronous Support: Built-in support for asynchronous operations, making it suitable for modern applications.
- Dependency Injection: Seamless integration with popular dependency injection frameworks, such as Microsoft.Extensions.DependencyInjection.
- Cross-Cutting Concerns: Easily manage cross-cutting concerns like logging, caching, and validation through middleware components.
- Testable: Designed with testability in mind, making it easy to write unit tests for your components.
- Open Source: Free to use and modify under the MIT License.
- Documentation: Comprehensive documentation and examples to help you get started quickly.
- Community Support: Active community and support channels to help you with any questions or issues you may encounter.
- Performance: Optimized for performance, ensuring that your application runs smoothly even under heavy load.
Getting Started
To get started with ScopeBus, follow these steps:
Install the NuGet package:
dotnet add package ScopeBus
Add the necessary using directives in your code:
using ScopeBus; services.AddScopeBus();
Why ScopeBus?
- You're building a clean architecture application and need a flexible mediator
- You want CQRS without being locked into MediatR’s conventions
- You need pre/post pipeline behaviors, auditing, and domain events
- You want automatic handler registration — no manual DI
- You need to support streaming, retries, and granular tracing in one pipeline
Define Your Request Handler
ScopeBus will automatically register the handler for you
public class MyRequest : IRequest<MyResponse>
{
public string Name { get; set; }
}
public class MyRequestHandler : IRequestHandler<MyRequest, MyResponse>
{
public Task<MyResponse> Handle(MyRequest request, CancellationToken cancellationToken)
{
return Task.FromResult(new MyResponse { Message = $"Hello, {request.Name}!" });
}
}
- Send a Request
[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
private readonly IMediator _bus;
public MyController(IMediator bus)
{
_bus = bus;
}
[HttpPost]
public async Task<IActionResult> Get(MyRequest request)
{
var response = await _bus.Send(request);
return Ok(response);
}
}
Define Your Audit Handler
How Audit Handlers Work
- PreAuditAsync: This method is called before the command handler executes. You can use it to log or perform any other actions before the main logic runs.
- PostAuditAsync: This method is called after the command handler executes. You can use it to log or perform any other actions after the main logic has completed. This allows you to separate the auditing logic from the main command handler, making your code cleaner and more maintainable. NB: ScopeBus will automatically register the handler for you
public class DeleteEmployeeRequest : ICommand<ApiResponse<string>>
{
public string? EmployeeId { get; set; }
}
public class DeleteEmployeeAuditHandler : IAuditableCommandHandler<DeleteEmployeeRequest, ApiResponse<string>>
{
private readonly ILogger<DeleteEmployeeAuditHandler> _logger;
private readonly IHttpContextAccessor _context;
public DeleteEmployeeAuditHandler(ILogger<DeleteEmployeeAuditHandler> logger, IHttpContextAccessor context)
{
_logger = logger;
_context = context;
}
public Task PreAuditAsync(DeleteEmployeeRequest request, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[PRE] {user} requested to delete employee {request.EmployeeId}");
return Task.CompletedTask;
}
public Task PostAuditAsync(DeleteEmployeeRequest request, ApiResponse<string> response, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[POST] {user} deleted employee {request.EmployeeId} with result: {response.Code}");
return Task.CompletedTask;
}
}
Define Your Query Handler
ScopeBus will automatically register the handler for you Note: How Query Handlers Work QueryHandler is mostly intended for read operations. It is a simple interface that defines a single method, Handle, which takes a query object and returns a result. The query object can be any type that implements the IQuery<T> interface, where T is the type of the result.
The Query handler is used to retrieve data and cache it for later use. You can use it to implement caching strategies, such as in-memory caching or distributed caching, to improve performance and reduce database load. To Provide a custom implementation of ICachingProvider, Create a class that implements the ICachingProvider interface and register it with the DI container. Eg: builder.Services.AddScoped<ICacheProvider, RedisCacheProvider>();
if your Model or DTO extends IQuery<T>, Then caching provider will be automatically registered for you which uses immemory caching. Unless otherwise you specified your own Caching provider as stated above.
public class GetEmployeeQuery : IQuery<Employee>
{
public int EmployeeId { get; set; }
}
public class GetEmployeeQueryHandler : IQueryHandler<GetEmployeeQuery, Employee>
{
private readonly IEmployeeRepository _repository;
public GetEmployeeQueryHandler(IEmployeeRepository repository)
{
_repository = repository;
}
public async Task<Employee> Handle(GetEmployeeQuery request, CancellationToken cancellationToken)
{
return await _repository.GetByIdAsync(request.EmployeeId);
}
}
Auditing
Your request DTO should implement the ICommand<TResponse> interface, where TResponse is the type of the response you expect from the command handler. How Audit Handlers Work
- PreAuditAsync: This method is called before the command handler executes. You can use it to log or perform any other actions before the main logic runs.
- PostAuditAsync: This method is called after the command handler executes. You can use it to log or perform any other actions after the main logic has completed. This allows you to separate the auditing logic from the main command handler, making your code cleaner and more maintainable.
public class DeleteEmployeeRequest : ICommand<ApiResponse<string>>
{
public int EmployeeId { get; set; }
}
public class DeleteEmployeeAuditHandler : IAuditableCommandHandler<DeleteEmployeeRequest, ApiResponse<string>>
{
private readonly ILogger<DeleteEmployeeAuditHandler> _logger;
private readonly IHttpContextAccessor _context;
public DeleteEmployeeAuditHandler(ILogger<DeleteEmployeeAuditHandler> logger, IHttpContextAccessor context)
{
_logger = logger;
_context = context;
}
public Task PreAuditAsync(DeleteEmployeeRequest request, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[PRE] {user} requested to delete employee {request.EmployeeId}");
return Task.CompletedTask;
}
public Task PostAuditAsync(DeleteEmployeeRequest request, ApiResponse<string> response, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[POST] {user} deleted employee {request.EmployeeId} with result: {response.Code}");
return Task.CompletedTask;
}
}
Validation
How Validation Handlers Work
- ValidationHandler is a simple interface that defines a single method, ValidateAsync, which takes a request object and returns a list of validation errors.
- The request object can be any type that implements the IValidatable interface, which contains the properties to be validated.
- The validation handler is used to validate the request object before it is processed by the command handler.
- If the validation fails, the command handler will not be executed, and the validation errors will be returned to the client.
- You can use any validation library, such as FluentValidation or DataAnnotations, to implement the validation logic.
- You can also create custom validation attributes to validate specific properties of the request object.
- The validation handler is registered with the DI container, and ScopeBus will automatically resolve it when processing the request.
NB: ScopeBus will automatically register the handler for you
public class EmployeeRequestValidator : IValidator<EmployeeRequest>
{
public Task<IEnumerable<ErrorResponse>> ValidateAsync(EmployeeRequest request, CancellationToken cancellationToken)
{
var errors = new List<ErrorResponse>();
if (string.IsNullOrWhiteSpace(request.FirstName))
errors.Add(new ErrorResponse(nameof(request.FirstName), "First name is required"));
if (string.IsNullOrWhiteSpace(request.LastName))
errors.Add(new ErrorResponse(nameof(request.LastName), "Last name is required"));
return Task.FromResult<IEnumerable<ErrorResponse>>(errors);
}
}
INotification
How Notification Handlers Work
- NotificationHandler is a simple interface that defines a single method, Handle, which takes a notification object and returns a Task.
- The notification object can be any type that implements the INotification interface, which contains the properties to be notified.
- The notification handler is used to notify other components of an event that has occurred in the system.
- You can use it to implement event-driven architectures, where components communicate with each other through events.
- The notification handler is registered with the DI container, and ScopeBus will automatically resolve it when processing the notification.
In the handler for creating employee, after creating the employee, call the OnCreated method to trigger the domain event. This will send the notification to the registered handlers.
NB: ScopeBus will automatically register the handler for you
public class Employee:EventEntity
{
public string? Id { get; set; } = Guid.NewGuid().ToString("N");
public DateTime CreatedAt { get; set; }= DateTime.UtcNow;
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
public void OnCreated()
{
AddDomainEvent(new EmployeeCreatedEvent
{
Employee =this
});
}
}
public class EmployeeCreatedNotification : INotification
{
public Employee? Employee { get; set; }
}
public class EmployeeCreatedNotificationHandler : INotificationHandler<EmployeeCreatedNotification>
{
private readonly ILogger<EmployeeCreatedNotificationHandler> _logger;
public EmployeeCreatedNotificationHandler(ILogger<EmployeeCreatedNotificationHandler> logger)
{
_logger = logger;
}
public Task Handle(EmployeeCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation($"Employee created: {notification.Employee?.FirstName} {notification.Employee?.LastName}");
return Task.CompletedTask;
}
}
Roles And Permissions
How Roles And Permissions Handlers Work
- Roles and permissions handlers are used to manage user roles and permissions in the system.
- They provide methods to check if a user has a specific role or permission, and to assign roles and permissions to users.
- You can use them to implement role-based access control (RBAC) in your application.
- The roles and permissions handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request.
- You can also create custom roles and permissions to fit your application's needs.
- You can use any authentication and authorization library, such as ASP.NET Core Identity or IdentityServer, to implement the roles and permissions logic.
- Roles are extracted from the JWT token and stored in the ClaimsIdentity of the user.
- The roles are checked against the permissions defined in the system.
- The roles and permissions handlers will only work if you enable authentication and authorization in your application.
- Your request DTO should extend the
IAuthorizedRequest
interface, which contains the properties to be checked for authorization.
public class EmployeeRequest:IRequest<ApiResponse<Employee>>,IAuthorizedRequest
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Email { get; set; }
/*
This property is used to specify the required roles for the command
It is used to check if the user has the required roles before executing the command
If the user does not have the required roles, an access denied exception will be raised preventing the execution of the command
*/
public string[] RequiredRoles => ["HR.Manager", "Admin"];
}
Streams Request
How Streams Request Handlers Work
- Streams request handlers are used to process large amounts of data in a streaming fashion.
- They provide methods to read and write data in a streaming manner, allowing you to process data as it arrives.
- You can use them to implement streaming APIs, where data is sent and received in chunks rather than all at once.
- The streams request handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request. The StreamRequestHandler interface is used to process the request and return a stream of data. The StreamHandler will invoke any handler that is registered for the request type.
Example:
public class StreamEmployeesRequest : IStreamRequest<Employee>
{
public int Count { get; set; } = 5;
}
public class StreamEmployeesRequestHandler : IStreamRequestHandler<StreamEmployeesRequest, Employee>
{
public async IAsyncEnumerable<Employee> Handle(StreamEmployeesRequest request, [EnumeratorCancellation] CancellationToken cancellationToken)
{
for (int i = 0; i < request.Count; i++)
{
yield return new Employee
{
Id = Guid.NewGuid().ToString("N"),
FirstName = $"FirstName {i}",
LastName = $"LastName {i}",
Email = $"Email {i}"
};
}
}
}
//Controller Call
[ApiController]
[Route("api/[controller]")]
public class EmployeeController : ControllerBase
{
private readonly IMediator _bus;
public EmployeeController(IMediator bus)
{
_bus = bus;
}
[HttpGet("stream")]
public async Task<IActionResult> StreamEmployees([FromQuery] StreamEmployeesRequest request)
{
var stream = _bus.Stream(request);
//Do something with the stream
}
}
Retry Flaky
Requests
How Retry Handlers Work
- Retry handlers are used to automatically retry failed requests in a configurable manner.
- They provide methods to specify the number of retries, the delay between retries, and the conditions under which to retry.
- You can use them to implement retry policies for transient failures, such as network errors or timeouts.
- The retry handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request.
Example.
public class RetryRequest : IRequest<ApiResponse<string>>
{
public string? Name { get; set; }
}
public class RetryRequestHandler : IRequestHandler<RetryRequest, ApiResponse<string>>
{
private static int _attempt = 0;
public async Task<string> Handle(RetryRequest request, CancellationToken cancellationToken)
{
await Task.CompletedTask;
_attempt++;
Console.WriteLine($"Attempt #{_attempt} at handling FlakyRequest");
if (_attempt < 3)
{
throw new InvalidOperationException("Simulated transient failure");
}
return $"Recovered successfully after {_attempt} attempts. Message: {request.Message}";
}
}
PreProcessing and PostProcessing
How PreProcessing and PostProcessing Handlers Work
- PreProcessing and PostProcessing handlers are used to perform actions before and after the request is processed.
- They provide methods to specify the actions to be performed, such as logging, caching, or modifying the request or response.
- You can use them to implement cross-cutting concerns, such as logging, caching, or validation.
- The pre-processing and post-processing handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request.
- Your PreProcessing and PostProcessing handlers should implement the
IRequestPreProcessor<TRequest>
andIRequestPostProcessor<TRequest, TResponse>
interfaces, respectively. Example.
//PreProcessing
public class EmployeePreProcessHandler : IRequestPreProcessor<EmployeeRequest>
{
private readonly ILogger<EmployeePreProcessHandler> _logger;
public EmployeePreProcessHandler(ILogger<EmployeePreProcessHandler> logger)
{
_logger = logger;
}
public Task Process(EmployeeRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation($"[PRE] Processing EmployeeRequest at {DateTime.UtcNow}");
return Task.CompletedTask;
}
}
//PostProcessing
public class EmployeePostProcessHandler : IRequestPostProcessor<EmployeeRequest, ApiResponse<Employee>>
{
private readonly ILogger<EmployeePostProcessHandler> _logger;
public EmployeePostProcessHandler(ILogger<EmployeePostProcessHandler> logger)
{
_logger = logger;
}
public Task Process(EmployeeRequest request, ApiResponse<Employee> response, CancellationToken cancellationToken)
{
_logger.LogInformation($"[POST] EmployeeRequest returned {response.GetType().Name} with status: {response.Code}");
return Task.CompletedTask;
}
}
Auditing
How Audit Handlers Work
- Audit handlers are used to log and track the actions performed in the system.
- They provide methods to specify the actions to be logged, such as the request and response data, the user who performed the action, and the timestamp.
- You can use them to implement auditing and logging for compliance and security purposes.
- The audit handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request.
- The Auditing handlers provides you with two methods:
PreAuditAsync
andPostAuditAsync
.
Example: Implementing Audit for Employee Delete
Your request DTO should implement the ICommand<TResponse>
interface, where TResponse is the type of the response you expect from the command handler.
The ICommand<TResponse>
interface is used to define a command that can be executed by the command handler.
The PreAuditAsync
method is called before the handler of your request executes, and the PostAuditAsync
method is called after the command handler executes.
The PostAuditAsync
method is called after the Handler for your request is executed.
public class DeleteEmployeeRequest : ICommand<ApiResponse<string>>
{
public string? EmployeeId { get; set; }
}
public class DeleteEmployeeAuditHandler : IAuditableCommandHandler<DeleteEmployeeRequest, ApiResponse<string>>
{
private readonly ILogger<DeleteEmployeeAuditHandler> _logger;
private readonly IHttpContextAccessor _context;
public DeleteEmployeeAuditHandler(ILogger<DeleteEmployeeAuditHandler> logger, IHttpContextAccessor context)
{
_logger = logger;
_context = context;
}
public Task PreAuditAsync(DeleteEmployeeRequest request, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[PRE] {user} requested to delete employee {request.EmployeeId}");
return Task.CompletedTask;
}
public Task PostAuditAsync(DeleteEmployeeRequest request, ApiResponse<string> response, CancellationToken cancellationToken)
{
var user = _context.HttpContext?.User?.Identity?.Name ?? "anonymous";
_logger.LogInformation($"[POST] {user} deleted employee {request.EmployeeId} with result: {response.Code}");
return Task.CompletedTask;
}
}
This ensures that the auditing logic is separated from the main command handler, making your code cleaner and more maintainable.
Telemetry and Logging: CorrelationHandler
How Correlation Handlers Work
- Correlation handlers are used to track and correlate requests and responses in the system.
- They provide methods to specify the correlation ID, which is used to identify and track requests and responses across different components and services.
- You can use them to implement distributed tracing and logging for better observability and debugging.
- The correlation handlers are registered with the DI container, and ScopeBus will automatically resolve them when processing the request.
- The Correlation handlers provides you with two methods:
OnStartAsync
andOnCompleteAsync
. - The
OnStartAsync
method is called before the handler of your request executes, and theOnCompleteAsync
method is called after the command handler executes. - Your Correlation handler should implement the
ICorrelationHandler
interface, which contains the methods to be called for tracking requests and responses.
Example: Create Your CorrelationHandler to Track Requests and Responses
public class DefaultCorrelationHandler : ICorrelationHandler
{
private readonly ILogger<DefaultCorrelationHandler> _logger;
public DefaultCorrelationHandler(ILogger<DefaultCorrelationHandler> logger)
{
_logger = logger;
}
public Task OnStartAsync(string traceId, object request, CancellationToken cancellationToken)
{
_logger.LogInformation("[Global TRACE Start] {TraceId} for {RequestType}", traceId, request.GetType().Name);
return Task.CompletedTask;
}
public Task OnCompleteAsync(string traceId, object request, object response, long durationMs, CancellationToken cancellationToken)
{
_logger.LogInformation("[Global TRACE End] {TraceId} ({Duration} ms)", traceId, durationMs);
return Task.CompletedTask;
}
}
This ensures that the correlation logic is separated from the main command handler, making your code cleaner and more maintainable.
ScopeBus Logging
ScopeBus supports a flexible and extensible logging pipeline using a built-in LoggingBehavior<TRequest, TResponse>
. Logging is opt-in, meaning it only activates for requests that implement a custom hook interface, giving you full control over what and how you log.
- Hook-based logging – opt-in per request
- Full access to request, response, and exception data
- No boilerplate logging in your handlers
- Works for commands, queries, or any request type
🧩 How It Works
ScopeBus includes a default logging pipeline behavior:
To enable logging for a request, implement the ILoggingHook
interface,
public interface ILoggingHook<TRequest, TResponse>
{
Task OnLogStartAsync(TRequest request, CancellationToken cancellationToken);
Task OnLogCompleteAsync(TRequest request, TResponse response, CancellationToken cancellationToken);
Task OnLogExceptionAsync(TRequest request, Exception ex, CancellationToken cancellationToken);
}
This interface provides three methods:
OnLogStartAsync
: Called before the request is sent to the handler.OnLogCompleteAsync
: Called after the request is completed successfully.OnLogExceptionAsync
: Called if an exception occurs during request processing.
Example:
public class CreateEmployeeLoggingHook : ILoggingHook<CreateEmployeeRequest, ApiResponse<Employee>>
{
private readonly ILogger<CreateEmployeeLoggingHook> _logger;
public CreateEmployeeLoggingHook(ILogger<CreateEmployeeLoggingHook> logger)
{
_logger = logger;
}
public Task OnLogStartAsync(CreateEmployeeRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("[LOGGING] Starting CreateEmployeeRequest for: {Email}", request.Email);
return Task.CompletedTask;
}
public Task OnLogCompleteAsync(CreateEmployeeRequest request, ApiResponse<Employee> response, CancellationToken cancellationToken)
{
_logger.LogInformation("[LOGGING] Completed CreateEmployeeRequest with status {Status}", response.Code);
return Task.CompletedTask;
}
public Task OnLogExceptionAsync(CreateEmployeeRequest request, Exception ex, CancellationToken cancellationToken)
{
_logger.LogError(ex, "[LOGGING] Exception during CreateEmployeeRequest for: {Email}", request.Email);
return Task.CompletedTask;
}
}
🧪 Testing
ScopeBus is testable by design. You can mock IMediator
, inject handlers directly, or use test doubles for pipeline behaviors.
We recommend creating a custom TestScopeBus
helper for integration testing command/response flow.
License
This project is licensed under the MIT License.
Architecture
Product | Versions 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. |
-
net8.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.2.0)
- Microsoft.Extensions.Caching.Memory (>= 6.0.2)
- Microsoft.Extensions.DependencyInjection (>= 8.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Logging (>= 8.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 8.0.0)
- Microsoft.Extensions.Options (>= 8.0.0)
- Newtonsoft.Json (>= 13.0.3)
- Polly (>= 8.5.2)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ScopeBus:
Package | Downloads |
---|---|
DotnetCqrsPgTemplate.Api
Package Description |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version 1.1.1 Added PBAC to IAuthorized Request