Conqueror.CQS.Middleware.Logging
0.3.0-beta.1
See the version list below for details.
dotnet add package Conqueror.CQS.Middleware.Logging --version 0.3.0-beta.1
NuGet\Install-Package Conqueror.CQS.Middleware.Logging -Version 0.3.0-beta.1
<PackageReference Include="Conqueror.CQS.Middleware.Logging" Version="0.3.0-beta.1" />
paket add Conqueror.CQS.Middleware.Logging --version 0.3.0-beta.1
#r "nuget: Conqueror.CQS.Middleware.Logging, 0.3.0-beta.1"
// Install Conqueror.CQS.Middleware.Logging as a Cake Addin #addin nuget:?package=Conqueror.CQS.Middleware.Logging&version=0.3.0-beta.1&prerelease // Install Conqueror.CQS.Middleware.Logging as a Cake Tool #tool nuget:?package=Conqueror.CQS.Middleware.Logging&version=0.3.0-beta.1&prerelease
Conqueror - for building scalable & maintainable .NET applications
ATTENTION: This project is currently still undergoing active development and contrary to what some of this README says, everything in here is still subject to change. Therefore please do not yet use this project for any production application.
Conqueror is a set of libraries that helps you build .NET applications in a structured way (using patterns like command-query separation, chain-of-responsibility (often also known as middlewares), publish-subscribe, data streams, etc.), while keeping them scalable (both from the development perspective as well as at runtime).
See our quickstart or example projects if you want to jump right into code examples for using Conqueror. Or head over to our recipes for more detailed guidance on how you can utilize Conqueror to its maximum. Finally, if you want to learn more about the motivation behind this project (including comparisons to similar projects like MediatR), head over to the motivation section.
Conqueror only supports .NET 6+
Libraries
Conqueror.CQS (stable): Split your business processes into simple-to-maintain and easy-to-test pieces of code using the command-query separation pattern. Handle cross-cutting concerns like logging, validation, authorization etc. using configurable middlewares. Keep your applications scalable by moving commands and queries from a modular monolith to a distributed application with minimal friction.
Head over to our CQS recipes for more guidance on how to use this library.
Conqueror.Eventing (experimental): Decouple your application logic by using in-process event publishing using the publish-subscribe pattern. Handle cross-cutting concerns like logging, tracing, filtering etc. using configurable middlewares.
Head over to our eventing recipes for more guidance on how to use this library.
Conqueror.Streaming.Interactive (experimental): Keep your applications in control by allowing them to consume data streams at their own pace using a pull-based interactive approach. Handle cross-cutting concerns like logging, error handling, authorization etc. using configurable middlewares. Keep your applications scalable by moving stream consumers from a modular monolith to a distributed application with minimal friction.
Head over to our interactive streaming recipes for more guidance on how to use this library.
Conqueror.Streaming.Reactive (early prototype): Allow your applications to consume data streams for which they cannot control the frequency using a push-based reactive approach. Handle cross-cutting concerns like logging, throttling, filtering etc. using configurable middlewares. Keep your applications scalable by moving stream consumers from a modular monolith to a distributed application with minimal friction.
Head over to our reactive streaming recipes for more guidance on how to use this library.
Quickstart
This quickstart guide will let you jump right into the code without lengthy explanations (for more guidance head over to our recipes). By following this guide you'll add HTTP commands and queries to your ASP.NET Core application. You can also find the source code here in the repository.
# add server-side CQS packages
dotnet add package Conqueror.CQS
dotnet add package Conqueror.CQS.Transport.Http.Server.AspNetCore
// add Conqueror CQS to your services
builder.Services.AddConquerorCQS().AddConquerorCQSTypesFromExecutingAssembly();
builder.Services.AddControllers().AddConquerorCQSHttpControllers();
builder.Services.FinalizeConquerorRegistrations();
In PrintIntegerCommand.cs
create a command that prints its parameter to stdout and echos it back to the client.
using Conqueror;
namespace Quickstart;
[HttpCommand]
public sealed record PrintIntegerCommand(int Parameter);
public sealed record PrintIntegerCommandResponse(int Parameter);
public interface IPrintIntegerCommandHandler : ICommandHandler<PrintIntegerCommand,
PrintIntegerCommandResponse>
{
}
public sealed class PrintIntegerCommandHandler : IPrintIntegerCommandHandler
{
public Task<PrintIntegerCommandResponse> ExecuteCommand(PrintIntegerCommand command,
CancellationToken cancellationToken = default)
{
Console.WriteLine($"Got command parameter {command.Parameter}");
return Task.FromResult(new PrintIntegerCommandResponse(command.Parameter));
}
}
In AddTwoIntegersQuery.cs
create a query that takes two integer parameters and returns their sum.
using Conqueror;
namespace Quickstart;
[HttpQuery]
public sealed record AddTwoIntegersQuery(int Parameter1, int Parameter2);
public sealed record AddTwoIntegersQueryResponse(int Sum);
public interface IAddTwoIntegersQueryHandler : IQueryHandler<AddTwoIntegersQuery,
AddTwoIntegersQueryResponse>
{
}
public sealed class AddTwoIntegersQueryHandler : IAddTwoIntegersQueryHandler
{
public Task<AddTwoIntegersQueryResponse> ExecuteQuery(AddTwoIntegersQuery query,
CancellationToken cancellationToken = default)
{
return Task.FromResult(new AddTwoIntegersQueryResponse(query.Parameter1 + query.Parameter2));
}
}
Now launch your app and you can call the command and query via HTTP.
curl http://localhost:5000/api/commands/printInteger --data '{"parameter": 10}' -H 'Content-Type: application/json'
# in your server console you will see "Got command parameter 10"
curl http://localhost:5000/api/queries/addTwoIntegers?parameter1=10\¶meter2=5
# prints {"sum":15}
If you have swagger UI enabled, it will show the new command and query and they can be called from there.
Recipes
In addition to code-level API documentation, Conqueror provides you with recipes that will guide you in how to utilize it to its maximum. Each recipe will help you solve one particular challenge that you will likely encounter while building a .NET application.
For every "How do I do X?" you can imagine for this project, you should be able to find a recipe here. If you don't see a recipe for your question, please let us know by creating an issue or even better, provide the recipe as a pull request.
CQS Introduction
CQS is an acronym for command-query separation (which is the inspiration for this project and also where the name is derived from: conquer → commands and queries). The core idea behind this pattern is that operations which only read data (i.e. queries) and operations which mutate data or cause side-effects (i.e. commands) have very different characteristics (for a start, in most applications queries are executed much more frequently than commands). In addition, business operations often map very well to commands and queries, allowing you to model your application in a way that allows technical and business stakeholders alike to understand the capabilities of the system. There are many other benefits we gain from following this separation in our application logic. For example, commands and queries represent a natural boundary for encapsulation, provide clear contracts for modularization, and allow solving cross-cutting concerns according to the nature of the operation (e.g. caching makes sense for queries, but not such much for commands). With commands and queries testing often becomes more simple as well, since they provide a clear list of the capabilities that should be tested (allowing more focus to be placed on use-case-driven testing instead of traditional unit testing).
CQS Basics
- getting started
- testing command and query handlers (to-be-written)
- solving cross-cutting concerns with middlewares (e.g. validation or logging) (to-be-written)
- testing command and query handlers that have middleware pipelines (to-be-written)
- testing middlewares (to-be-written)
CQS Advanced
- using a different dependency injection container (e.g. Autofac or Ninject) (to-be-written)
- creating a clean architecture with commands and queries (to-be-written)
- exposing commands and queries via HTTP (to-be-written)
- testing HTTP commands and queries (to-be-written)
- calling HTTP commands and queries from another application (to-be-written)
- using middlewares for command and query HTTP clients (to-be-written)
- authenticating and authorizing commands and queries (to-be-written)
- moving from a modular monolith to a distributed system (to-be-written)
- using commands and queries in a Blazor app (server-side or web-assembly) (to-be-written)
- building a CLI using commands and queries (to-be-written)
CQS Expert
- store and access background context information in the scope of a single command or query (to-be-written)
- propagate background context information (e.g. trace ID) across multiple commands, queries, events, and streams (to-be-written)
- accessing properties of commands and queries in middlewares (to-be-written)
- exposing and calling commands and queries via other transports (e.g. gRPC) (to-be-written)
- building test assertions that work for HTTP and non-HTTP commands and queries (to-be-written)
- creating generic command or query handlers (to-be-written)
Eventing Introduction
Eventing is a way to refer to the publishing and observing of events via the publish-subscribe pattern. Eventing is a good way to decouple or loosely couple different parts of your application by making an event publisher agnostic to the observers of events it publishes. In addition to this basic idea, Conqueror allows solving cross-cutting concerns on both the publisher as well as the observer side.
Eventing Basics
- getting started (to-be-written)
- testing event observers (to-be-written)
- testing code that publishes events (to-be-written)
- solving cross-cutting concerns with middlewares (e.g. logging) (to-be-written)
- testing event observers with pipelines (to-be-written)
- testing event publisher pipeline (to-be-written)
- testing middlewares (to-be-written)
Eventing Advanced
- using a different dependency injection container (e.g. Autofac or Ninject) (to-be-written)
- execute event observers with a different strategy (e.g. parallel execution) (to-be-written)
- creating a clean architecture with loose coupling via events (to-be-written)
- moving from a modular monolith to a distributed system (to-be-written)
Eventing Expert
- store and access background context information in the scope of a single event (to-be-written)
- propagate background context information (e.g. trace ID) across multiple commands, queries, events, and streams (to-be-written)
- accessing properties of events in middlewares (to-be-written)
Interactive Streaming Introduction
For data streaming there are generally two high-level approaches: interactive / pull-based (i.e. consumer is in control of the pace) and reactive / push-based (i.e. the producer is in control of the pace). Here we focus on interactive streaming, which is a good approach for use cases like paging and event sourcing.
Interactive Streaming Basics
- getting started (to-be-written)
- testing streaming request handlers (to-be-written)
- solving cross-cutting concerns with middlewares (e.g. validation or logging) (to-be-written)
- testing streaming request handlers that have middleware pipelines (to-be-written)
- testing middlewares (to-be-written)
Interactive Streaming Advanced
- using a different dependency injection container (e.g. Autofac or Ninject) (to-be-written)
- reading interactive streams from a messaging system (e.g. Kafka or RabbitMQ) (to-be-written)
- exposing streams via HTTP (to-be-written)
- testing HTTP streams (to-be-written)
- consuming HTTP streams from another application (to-be-written)
- using middlewares for interactive streaming HTTP clients (to-be-written)
- optimize HTTP streaming performance with pre-fetching (to-be-written)
- authenticating and authorizing streaming requests (to-be-written)
- moving from a modular monolith to a distributed system (to-be-written)
Interactive Streaming Expert
- store and access background context information in the scope of a single streaming request (to-be-written)
- propagate background context information (e.g. trace ID) across multiple commands, queries, events, and streams (to-be-written)
- accessing properties of streaming requests in middlewares (to-be-written)
- exposing and consuming interactive streams via other transports (e.g. SignalR) (to-be-written)
- building test assertions that work for HTTP and non-HTTP streams (to-be-written)
Reactive Streaming Introduction
For data streaming there are generally two high-level approaches: interactive / pull-based (i.e. consumer is in control of the pace) and reactive / push-based (i.e. the producer is in control of the pace). Here we focus on reactive streaming, which is a good approach when you do not control the source of the stream yourself, and therefore need to handle stream items at whatever pace the producer provides (e.g. handling sensor data from IoT devices).
Reactive Streaming Basics
- tbd (to-be-written)
Reactive Streaming Advanced
- tbd (to-be-written)
Reactive Streaming Expert
- tbd (to-be-written)
Motivation
Modern software development is often centered around building web applications that communicate via HTTP (we'll call them "web APIs"). However, many applications require different entry points or APIs as well (e.g. message queues, command line interfaces, raw TCP or UDP sockets, etc.). Each of these kinds of APIs need to address a variety of cross-cutting concerns, most of which apply to all kinds of APIs (e.g. logging, tracing, error handling, authorization, etc.). Microsoft has done an excellent job in providing out-of-the-box solutions for many of these concerns when building web APIs with ASP.NET Core using middlewares (which implement the chain-of-responsibility pattern). However, for other kinds of APIs, development teams are often forced to handle these concerns themselves, spending valuable development time.
One way many teams choose to address this issue is by forcing every operation to go through a web API (e.g. having a small adapter that reads messages from a queue and then calls a web API for processing the message). While this works well in many cases, it adds extra complexity and fragility by adding a new integration point for very little value. Optimally, there would be a way to address the cross-cutting concerns in a consistent way for all kinds of APIs. This is exactly what Conqueror does. It provides the building blocks for implementing business functionality and addressing those cruss-cutting concerns in an transport-agnostic fashion, and provides extension packages that allow exposing the business functionality via different transports (e.g. HTTP).
In addition, a popular way to build systems these days is using microservices. While microservices are a powerful approach, they can often represent a significant challenge for small or new teams, mostly for deployment and operations (challenges common to most distributed systems). A different approach that many teams choose is to start with a modular monolith and move to microservices at a later point. However, it is common for teams to struggle with such a migration, partly due to sub-optimal modularization and partly due to existing tools and libraries not providing a smooth transition journey from one approach to another (or often forcing you into the distributed approach directly, e.g. MassTransit). Conqueror addresses this by encouraging you to build modules with clearly defined contracts and by allowing you to switch from having a module be part of a monolith to be its own microservice with minimal code changes.
In summary, these are some of the strengths of Conqueror:
Providing building blocks for many different communication patterns: Many applications require the use of different communication patterns to fulfill their business requirements (e.g.
request-response
,fire-and-forget
,publish-subscribe
,streaming
etc.). Conqueror provides building blocks for implementing these communication patterns efficiently and consistently, while allowing you to address cross-cutting concerns in a transport-agnostic fashion.Excellent use-case-driven documentation: A lot of effort went into writing our recipes. While most other libraries have documentation that is centered around explaining what they do, our use-case-driven documentation is focused on showing you how Conqueror helps you to solve the concrete challenges your are likely to encounter during application development.
Strong focus on testability: Testing is a very important topic that is sadly often neglected. Conqueror takes testability very seriously and makes sure that you know how you can test the code you have written using it (you may have noticed that the Conqueror.CQS recipe immediately following getting started shows you how you can test the handlers we built in the first recipe).
Migrating from a modular monolith to a distributed system with minimal friction: Business logic built on top of Conqueror provides clear contracts to consumers, regardless of whether these consumers are located in the same process or in a different application. By abstracting away the concrete transport over which the business logic is called, it can easily be moved from a monolithic approach to a distributed approach with minimal code changes.
Modular and extensible architecture: Instead of a big single library, Conqueror consists of many small (independent or complementary) packages. This allows you to pick and choose what functionality you want to use without adding the extra complexity for anything that you don't. It also improves maintainability by allowing modifications and extensions with a lower risk of breaking any existing functionality (in addition to a high level of public-API-focused test coverage).
Comparison with similar projects
Below you can find a brief comparison with some popular projects which address similar concerns as Conqueror.
Differences to MediatR
The excellent library MediatR is a popular choice for building applications. Conqueror takes a lot of inspirations from its design, with some key differences:
- MediatR allows handling cross-cutting concerns with global behaviors, while Conqueror allows handling these concerns with composable middlewares in independent pipelines per handler type.
- MediatR uses a single message sender service which makes it tricky to navigate to a message handler in your IDE from the point where the message is sent. With Conqueror you call handlers through an explicit interface, allowing you to use the "Go to implementation" functionality of your IDE.
- MediatR is focused building single applications without any support for any transports, while Conqueror allows building both single applications as well as distributed systems that communicate via different transports implemented through adapters.
Differences to MassTransit
MassTransit is a great framework for building distributed applications. It addresses many of the same concerns as Conqueror, with some key differences:
- MassTransit is designed for building distributed systems, forcing you into this approach from the start, even if you don't need it yet (the provided in-memory transport is explicitly mentioned as not being suited for production usage). Conqueror allows building both single applications as well as distributed systems.
- MassTransit is focused on asynchronous messaging, while Conqueror provides more communication patterns (e.g. synchronous request-response over HTTP).
- MassTransit has adapters for many messaging middlewares, like RabbitMQ or Azure Service Bus, which Conqueror does not.
- MassTransit provides out-of-the-box solutions for advanced patterns like sagas, state machines, etc., which Conqueror does not.
If you require the advanced patterns or messaging middleware connectors which MassTransit provides, you can easily combine it with Conqueror by calling command and query handlers from your consumers or wrapping your producers in command handlers.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. 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. |
-
net6.0
- Conqueror.CQS.Abstractions (>= 0.3.0-beta.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 6.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 6.0.0)
-
net7.0
- Conqueror.CQS.Abstractions (>= 0.3.0-beta.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 7.0.0)
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 |
---|---|---|
0.6.0-beta.2 | 70 | 8/25/2024 |
0.6.0-beta.1 | 67 | 8/13/2024 |
0.5.0-beta.4 | 185 | 11/19/2023 |
0.5.0-beta.3 | 105 | 7/18/2023 |
0.5.0-beta.2 | 100 | 7/15/2023 |
0.5.0-beta.1 | 90 | 4/22/2023 |
0.4.0-beta.2 | 99 | 2/26/2023 |
0.4.0-beta.1 | 94 | 2/25/2023 |
0.3.0-beta.3 | 104 | 2/12/2023 |
0.3.0-beta.2 | 93 | 1/9/2023 |
0.3.0-beta.1 | 112 | 1/7/2023 |