Conqueror 0.8.0-beta.3
See the version list below for details.
dotnet add package Conqueror --version 0.8.0-beta.3
NuGet\Install-Package Conqueror -Version 0.8.0-beta.3
<PackageReference Include="Conqueror" Version="0.8.0-beta.3" />
<PackageVersion Include="Conqueror" Version="0.8.0-beta.3" />
<PackageReference Include="Conqueror" />
paket add Conqueror --version 0.8.0-beta.3
#r "nuget: Conqueror, 0.8.0-beta.3"
#addin nuget:?package=Conqueror&version=0.8.0-beta.3&prerelease
#tool nuget:?package=Conqueror&version=0.8.0-beta.3&prerelease
Conqueror - a highly ergonomic library for building structured, scalable .NET apps
Conqueror is a .NET library that simplifies writing modular, scalable applications by unifying messages, signals, and more into a consistent, extensible model. It uses modern features of .NET like source generators and static abstract interface methods to reduce boilerplate, support advanced uses cases like AOT compilation, and to provide a highly ergonomic user-friendly API.
Whether you're building a monolith or distributed microservices, Conqueror provides a seamless experience with minimal ceremony. It also eases the transition from a modular monolith to a distributed system with minimal friction, giving teams the flexibility to start simple and delay the transition until the right time in a project's lifecycle.
Conqueror encourages clean architectures by decoupling your application logic from concrete transports like HTTP, and allows exposing business operations via many different transports with thin adapters.
Conqueror leverages design patterns like messaging, chain-of-responsibility (often also known as middlewares), aspect-oriented programming, builder pattern, publish-subscribe, and more.
<img src="./docs/intro.svg?raw=true" alt="Intro" style="height: 565px" height="565px" />
Conqueror only supports .NET 8 or later
Quickstart
This quickstart guide will let you jump right into the code without lengthy explanations. If you prefer more guidance, head over to our recipes. By following this quickstart guide, you'll add HTTP messages and an in-process signal to a minimal API ASP.NET Core application. You can also find the source code here in the repository.
dotnet new webapi -n Quickstart && cd Quickstart
dotnet add package Conqueror --prerelease
dotnet add package Conqueror.Middleware.Logging --prerelease
dotnet add package Conqueror.Transport.Http.Server.AspNetCore --prerelease
dotnet add package Swashbuckle.AspNetCore # to get a nice Swagger UI
Let's start by defining the contracts of our quickstart application in Contracts.cs:
using System.ComponentModel.DataAnnotations;
using Conqueror;
namespace Quickstart;
// In Conqueror, everything revolves around contracts of different kinds: messages, signals, and
// iterators (the latter is still experimental and therefore not yet included in the Quickstart).
// The contracts are simple records or classes marked by one or more attributes which determine the
// kind and transports (in-process, HTTP, gRPC, RabbitMQ, etc.) of the contract. A source generator
// is used to enhance the contracts with additional code, and therefore they must be partial
// Note that using transports is fully optional, and if you want you can use Conqueror purely
// in-process, similar to libraries like MediatR
// The `HttpMessage` attribute tells Conqueror that this record can be exposed via HTTP (using
// the corresponding transport package). The attribute allows customizing the HTTP endpoint method,
// path, path prefix, version, API group name, etc. (note that all are optional with sensible
// defaults, in this case leading to `POST /api/v1/incrementCounterByAmount`)
[HttpMessage<CounterIncrementedResponse>(Version = "v1")]
public sealed partial record IncrementCounterByAmount(string CounterName)
{
// We use simple data annotation validation as an example, but more powerful validation
// tools like FluentValidation are also supported. Note that the built-in .NET data annotation
// validation is only supported for properties, not constructor parameters
[Range(1, long.MaxValue)]
public required long IncrementBy { get; init; }
}
public sealed record CounterIncrementedResponse(long NewCounterValue);
// By default, HTTP messages are sent and received as POST, but all methods are supported.
// Parameters can be optional, and messages can have enumerable responses as well
[HttpMessage<List<CounterValue>>(HttpMethod = "GET", Version = "v1")]
public sealed partial record GetCounters(string? Prefix = null);
public sealed record CounterValue(string CounterName, long Value);
// Signals are a pub/sub mechanism, and can be handled in-process (like we do in this quickstart)
// or published via a transport like RabbitMQ (using the corresponding transport package)
[Signal]
public sealed partial record CounterIncremented(
string CounterName,
long NewValue,
long IncrementBy);
<details> <summary>Click here to see file without the comments to get a better idea how your own code will look like</summary>
using System.ComponentModel.DataAnnotations;
using Conqueror;
namespace Quickstart;
[HttpMessage<CounterIncrementedResponse>(Version = "v1")]
public sealed partial record IncrementCounterByAmount(string CounterName)
{
[Range(1, long.MaxValue)]
public required long IncrementBy { get; init; }
}
public sealed record CounterIncrementedResponse(long NewCounterValue);
[HttpMessage<List<CounterValue>>(HttpMethod = "GET", Version = "v1")]
public sealed partial record GetCounters(string? Prefix = null);
public sealed record CounterValue(string CounterName, long Value);
[Signal]
public sealed partial record CounterIncremented(
string CounterName,
long NewValue,
long IncrementBy);
</details>
In CountersRepository.cs create a simple repository to simulate talking to a database:
using System.Collections.Concurrent;
namespace Quickstart;
// simulate a database repository (which is usually async)
internal sealed class CountersRepository
{
private readonly ConcurrentDictionary<string, long> counters = new();
public async Task<long> AddOrIncrementCounterValue(string counterName, long incrementBy)
{
await Task.CompletedTask;
return counters.AddOrUpdate(counterName, incrementBy, (_, value) => value + incrementBy);
}
public async Task<long> GetCounterValue(string counterName)
{
await Task.CompletedTask;
return counters.GetValueOrDefault(counterName, 0L);
}
public async Task<IReadOnlyDictionary<string, long>> GetCounters()
{
await Task.CompletedTask;
return counters;
}
}
In IncrementCounterByAmountHandler.cs create a message handler for our IncrementCounterByAmount
message type.
using System.ComponentModel.DataAnnotations;
using Conqueror;
namespace Quickstart;
// The handler type is also enhanced by the Conqueror source generator, so it must be partial
internal sealed partial class IncrementCounterByAmountHandler(
CountersRepository repository,
ISignalPublishers publishers)
// This interface (among other things) is generated by a source generator
: IncrementCounterByAmount.IHandler
{
// Configure a pipeline of middlewares which is executed for every message
public static void ConfigurePipeline(IncrementCounterByAmount.IPipeline pipeline) =>
pipeline
// Conqueror ships with a handful of useful middleware packages
// for common cross-cutting concerns like logging and authorization
.UseLogging()
// Pipelines can have inline middlewares for ad-hoc logic (or you can
// build a full-fledged middleware; see the recipes for more details)
.Use(ctx =>
{
// Perform a simple data annotation validation (in a real application you would
// likely use a more powerful validation library like FluentValidation)
Validator.ValidateObject(ctx.Message, new(ctx.Message), true);
// Note that the middleware has access to the message with its proper type (i.e.
// the compiler knows that `ctx.Message` is of type `IncrementCounterByAmount`),
// so you could also write the validation directly like this:
if (ctx.Message.IncrementBy <= 0)
{
throw new ArgumentOutOfRangeException(nameof(ctx.Message.IncrementBy),
"increment amount must be positive");
}
return ctx.Next(ctx.Message, ctx.CancellationToken);
})
// Middlewares which have been added to a pipeline can be configured further
.ConfigureLogging(o => o.MessagePayloadLoggingStrategy =
PayloadLoggingStrategy.IndentedJson);
public async Task<CounterIncrementedResponse> Handle(
IncrementCounterByAmount message,
CancellationToken cancellationToken = default)
{
var newValue = await repository.AddOrIncrementCounter(message.CounterName,
message.IncrementBy);
// `ISignalPublishers` is a factory to get a publisher for a signal type. The
// `CounterIncremented.T` property is generated by the source generator and is used for
// type inference. The 'For' method returns a `CounterIncremented.IHandler` (which is a
// proxy for the actual signal handlers)
await publishers.For(CounterIncremented.T)
// You can customize the transport which is used to publish the signal
// (e.g. sending it via RabbitMQ), but here we configure the in-process
// transport to use parallel broadcasting for demonstration (instead of
// the default sequential broadcasting). You can also pass your own custom
// strategy if you need it
.WithTransport(b => b.UseInProcessWithParallelBroadcastingStrategy())
// The 'Handle' method is unique for each `IHandler`. This means that your
// IDE's "Go to Implementation" feature will show all signal handlers for
// this signal, making it simple to find all the places in your code where
// a signal is used
.Handle(new(message.CounterName, newValue, message.IncrementBy),
cancellationToken);
return new(await repository.GetCounterValue(message.CounterName));
}
}
<details> <summary>Click here to see a more realistic trimmed down version of the file</summary>
using System.ComponentModel.DataAnnotations;
using Conqueror;
namespace Quickstart;
internal sealed partial class IncrementCounterByAmountHandler(
CountersRepository repository,
ISignalPublishers publishers)
: IncrementCounterByAmount.IHandler
{
public static void ConfigurePipeline(IncrementCounterByAmount.IPipeline pipeline) =>
pipeline.UseLogging()
.UseDataAnnotationValidation()
.UseIndentedJsonMessageLogFormatting();
public async Task<CounterIncrementedResponse> Handle(
IncrementCounterByAmount message,
CancellationToken cancellationToken = default)
{
var newValue = await repository.AddOrIncrementCounter(message.CounterName,
message.IncrementBy);
await publishers.For(CounterIncremented.T)
.Handle(new(message.CounterName, newValue, message.IncrementBy),
cancellationToken);
return new(await repository.GetCounterValue(message.CounterName));
}
}
</details>
In DoublingCounterIncrementedHandler.cs create a signal handler that doubles increment operations on specific counters.
using Conqueror;
namespace Quickstart;
internal sealed partial class DoublingCounterIncrementedHandler(
IMessageSenders senders)
: CounterIncremented.IHandler
{
// Signal handlers support handling multiple signal types (by adding more `IHandler`
// interfaces), so the pipeline configuration is generic and is reused for all signal types
// (`typeof(T)` can be checked to customize the pipeline for a specific signal type)
static void ISignalHandler.ConfigurePipeline<T>(ISignalPipeline<T> pipeline) =>
pipeline.Use(ctx =>
{
// we are only interested in specific signals, so we skip the handler (and the
// rest of the pipeline) for all others
if (ctx.Signal is CounterIncremented { CounterName: "doubler" })
{
return ctx.Next(ctx.Signal, ctx.CancellationToken);
}
return Task.CompletedTask;
})
.Use(ctx =>
{
// Below in the 'Handle' method we call 'IncrementCounterByAmount' again,
// which could lead to an infinite loop. Conqueror "flows" context data
// across different executions, which is useful here to handle a signal
// only once per HTTP request
if (ctx.ConquerorContext.ContextData.Get<bool>("doubled"))
{
return Task.CompletedTask;
}
ctx.ConquerorContext.ContextData.Set("doubled", true);
return ctx.Next(ctx.Signal, ctx.CancellationToken);
})
// Middlewares in the pipeline are executed in the order that they are added in.
// We add the logging middleware to the pipeline only after the prior two
// middlewares to ensure that only signals which are not skipped get logged.
// The `Configure...` extension methods for middlewares can be used to modify the
// behavior of middlewares that were added earlier to a pipeline. A common pattern
// is to define reusable pipelines that define the order of middlewares and then
// use `Configure...` for a particular handler to modify the pipeline as necessary
.UseLogging(o => o.PayloadLoggingStrategy = PayloadLoggingStrategy.IndentedJson);
public async Task Handle(
CounterIncremented signal,
CancellationToken cancellationToken = default)
{
await senders
.For(IncrementCounterByAmount.T)
// When calling a message (or signal, etc.) handler, you can specify a sender
// pipeline, which is executed before the message is sent via the configured
// transport (and on the receiver the handler's own pipeline is then also executed)
.WithPipeline(p => p.UseLogging(o =>
{
o.PreExecutionHook = ctx =>
{
// Let's log a custom log message instead of Conqueror's default
ctx.Logger.LogInformation(
"doubling increment of counter '{CounterName}'",
ctx.Message.CounterName);
return false;
};
o.PostExecutionHook = ctx =>
{
ctx.Logger.LogInformation(
"doubled increment of counter '{CounterName}', it is now {NewValue}",
ctx.Message.CounterName,
ctx.Response.NewCounterValue);
return false;
};
}))
// You can customize the transport which is used to send the message (e.g. sending it
// via HTTP), but for demonstration we use the in-process transport (which already
// happens by default)
.WithTransport(b => b.UseInProcess())
// The 'Handle' method is unique for each `IHandler`, so "Go to Implementation" in
// your IDE will jump directly to your handler, enabling smooth code base navigation,
// even across different projects and transports
.Handle(new(signal.CounterName) { IncrementBy = signal.IncrementBy },
cancellationToken);
}
}
<details> <summary>Click here to see a more realistic trimmed down version of the file</summary>
using Conqueror;
namespace Quickstart;
internal sealed partial class DoublingCounterIncrementedHandler(
IMessageSenders senders)
: CounterIncremented.IHandler
{
static void ISignalHandler.ConfigurePipeline<T>(ISignalPipeline<T> pipeline) =>
pipeline.SkipSignalMatching<CounterIncremented>(s => s.CounterName != "doubler")
.EnsureSingleExecutionPerOperation()
.UseLoggingWithIndentedJson();
public async Task Handle(
CounterIncremented signal,
CancellationToken cancellationToken = default)
{
await senders.For(IncrementCounterByAmount.T)
.Handle(new(signal.CounterName) { IncrementBy = signal.IncrementBy },
cancellationToken);
}
}
</details>
In GetCountersHandler.cs create a message handler that returns a filtered list of counters.
using Conqueror;
namespace Quickstart;
internal sealed partial class GetCountersHandler(
CountersRepository repository)
: GetCounters.IHandler
{
public static void ConfigurePipeline(GetCounters.IPipeline pipeline) =>
pipeline.UseLogging(o =>
{
// The pipeline has access to the service provider from the scope of the call to the
// handler in case you need it to resolve some services
var isDevelopment = pipeline.ServiceProvider
.GetRequiredService<IHostEnvironment>()
.IsDevelopment();
// The logging middleware supports detailed configuration options. For example, like
// here we can omit verbose output from the logs in production. Note that in a real
// application you would wrap such logic into an extension method (leveraging
// `ConfigureLogging`) to make it reusable across message types. And thanks to the
// builder pattern, you could then call it simply like this:
// `pipeline.UseLogging().OmitResponseFromLogsInProduction()`
o.ResponsePayloadLoggingStrategy = isDevelopment
? PayloadLoggingStrategy.IndentedJson
: PayloadLoggingStrategy.Omit;
// You can also make the logging strategy dependent on the message or response
// payloads, e.g. to omit confidential data from the logs
o.ResponsePayloadLoggingStrategyFactory = (_, resp)
=> resp.Any(c => c.CounterName == "confidential")
? PayloadLoggingStrategy.Omit
: o.ResponsePayloadLoggingStrategy;
});
public async Task<List<CounterValue>> Handle(
GetCounters message,
CancellationToken cancellationToken = default)
{
var allCounters = await repository.GetCounters();
return allCounters.Where(p => message.Prefix is null || p.Key.StartsWith(message.Prefix))
.Select(p => new CounterValue(p.Key, p.Value))
.ToList();
}
}
<details> <summary>Click here to see a more realistic trimmed down version of the file</summary>
using Conqueror;
namespace Quickstart;
internal sealed partial class GetCountersHandler(
CountersRepository repository)
: GetCounters.IHandler
{
public static void ConfigurePipeline(GetCounters.IPipeline pipeline) =>
pipeline.UseLogging()
.OmitResponseFromLogsInProduction()
.OmitResponseFromLogsForMessageMatching(m => m.CounterName == "confidential");
public async Task<List<CounterValue>> Handle(
GetCounters message,
CancellationToken cancellationToken = default)
{
var allCounters = await repository.GetCounters();
return allCounters.Where(p => message.Prefix is null || p.Key.StartsWith(message.Prefix))
.Select(p => new CounterValue(p.Key, p.Value))
.ToList();
}
}
</details>
Finally, set up the app in Program.cs:
using Quickstart;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<CountersRepository>()
// This registers all the handlers in the project; alternatively, you can register
// individual handlers as well
.AddMessageHandlersFromAssembly(typeof(Program).Assembly)
.AddSignalHandlersFromAssembly(typeof(Program).Assembly)
// Add some services that Conqueror needs to properly expose messages via HTTP
.AddMessageEndpoints()
// Let's enable Swashbuckle to get a nice Swagger UI
.AddSwaggerGen();
var app = builder.Build();
app.UseSwagger()
.UseSwaggerUI();
// This enables message handlers as minimal HTTP API endpoints (including in AOT mode
// if you need that, although please check the corresponding recipe for more details)
app.MapMessageEndpoints();
app.Run();
Now launch your app and you can call the message handlers via HTTP.
# in background shell
dotnet run
curl http://localhost:5000/api/v1/incrementCounterByAmount \
--data '{"counterName":"test","incrementBy":2}' \
-H 'Content-Type: application/json'
# prints {"newCounterValue":2}
curl http://localhost:5000/api/v1/getCounters?prefix=tes
# prints [{"counterName":"test","value":2}]
# this doubles the increment operation through a signal handler
curl http://localhost:5000/api/v1/incrementCounterByAmount \
--data '{"counterName":"doubler","incrementBy":2}' \
-H 'Content-Type: application/json'
# prints {"newCounterValue":4}
curl http://localhost:5000/api/v1/getCounters
# prints [{"counterName":"test","value":2},{"counterName":"doubler","value":4}]
# add a confidential counter
curl http://localhost:5000/api/v1/incrementCounterByAmount \
--data '{"counterName":"confidential","incrementBy":1000}' \
-H 'Content-Type: application/json'
# prints {"newCounterValue":1000}
curl http://localhost:5000/api/v1/getCounters
# prints [{"counterName":"test","value":2},{"counterName":"doubler","value":4},{"counterName":"confidential","value":1000}]
Thanks to the logging middleware we added to the pipelines, you will see output similar to this in the server console.
Are you able to spot a bug in our logging configuration for confidential counters?
info: Quickstart.IncrementCounterByAmount[711195907]
Handling message on http receiver with payload
{
"CounterName": "test",
"IncrementBy": 2
}
(Message ID: fb074b19f88c65e6, Trace ID: 9b9fe7ce6e603247b13eb4cc781de6cf)
info: Quickstart.IncrementCounterByAmount[412531951]
Handled message on http receiver and got response {"NewCounterValue":2} in 29.9088ms (Message ID: fb074b19f88c65e6, Trace ID: 9b9fe7ce6e603247b13eb4cc781de6cf)
info: Quickstart.GetCounters[711195907]
Handling message on http receiver with payload {"Prefix":"tes"} (Message ID: 89c34aa7ea997929, Trace ID: 485643428bfa70b2832992ce2301386c)
info: Quickstart.GetCounters[412531951]
Handled message on http receiver and got response
[
{
"CounterName": "test",
"Value": 2
}
]
in 6.3682ms (Message ID: 89c34aa7ea997929, Trace ID: 485643428bfa70b2832992ce2301386c)
info: Quickstart.IncrementCounterByAmount[711195907]
Handling message on http receiver with payload
{
"CounterName": "doubler",
"IncrementBy": 2
}
(Message ID: 3c95064c69bad17b, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.CounterIncremented[441733974]
Handling signal with payload
{
"CounterName": "doubler",
"NewValue": 2,
"IncrementBy": 2
}
(Signal ID: 014a95afac5712ee, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.IncrementCounterByAmount[0]
doubling increment of counter 'doubler'
info: Quickstart.IncrementCounterByAmount[711195907]
Handling message with payload
{
"CounterName": "doubler",
"IncrementBy": 2
}
(Message ID: ecfef635f91abbb9, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.IncrementCounterByAmount[412531951]
Handled message and got response {"NewCounterValue":4} in 3.0264ms (Message ID: ecfef635f91abbb9, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.IncrementCounterByAmount[0]
doubled increment of counter 'doubler', it is now 4
info: Quickstart.CounterIncremented[1977864143]
Handled signal in 14.9293ms (Signal ID: 014a95afac5712ee, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.IncrementCounterByAmount[412531951]
Handled message on http receiver and got response {"NewCounterValue":4} in 18.6752ms (Message ID: 3c95064c69bad17b, Trace ID: bd7ae2a544cfa565876e8e5edf58faeb)
info: Quickstart.GetCounters[711195907]
Handling message on http receiver with payload {"Prefix":null} (Message ID: a516aabf628b1ff4, Trace ID: f1b784dbda5f2809fb8a6b0a093b0421)
info: Quickstart.GetCounters[412531951]
Handled message on http receiver and got response
[
{
"CounterName": "test",
"Value": 2
},
{
"CounterName": "doubler",
"Value": 4
}
]
in 0.5010ms (Message ID: a516aabf628b1ff4, Trace ID: f1b784dbda5f2809fb8a6b0a093b0421)
info: Quickstart.IncrementCounterByAmount[711195907]
Handling message on http receiver with payload
{
"CounterName": "confidential",
"IncrementBy": 1000
}
(Message ID: 6c45a52a82cc5384, Trace ID: 0d85b67886f6b489a6784e27abef7bcb)
info: Quickstart.IncrementCounterByAmount[412531951]
Handled message on http receiver and got response {"NewCounterValue":1000} in 0.5142ms (Message ID: 6c45a52a82cc5384, Trace ID: 0d85b67886f6b489a6784e27abef7bcb)
info: Quickstart.GetCounters[711195907]
Handling message on http receiver with payload {"Prefix":null} (Message ID: 4fc7ba52508e19ef, Trace ID: 42dc5a8f32afec50fc8709a271d7874b)
info: Quickstart.GetCounters[412531951]
Handled message on http receiver in 0.4799ms (Message ID: 4fc7ba52508e19ef, Trace ID: 42dc5a8f32afec50fc8709a271d7874b)
Libraries
Middlewares
Transports
Functionalities
<details> <summary>Click here to see documentation still under construction</summary>
Messaging
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 recipes for more guidance on how to use this library.
Signalling
Decouple your application logic by using in-process signal publishing using the publish-subscribe pattern. Handle cross-cutting concerns like logging, tracing, filtering etc. using configurable middlewares. Keep your applications scalable by moving signals from a modular monolith to a distributed application with minimal friction.
Head over to our signalling recipes for more guidance on how to use this library.
Experimental Functionalities
The functionalities below are still experimental. This means they do not have a stable API and are missing code documentation and recipes. They are therefore not suited for use in production applications, but can be used in proofs-of-concept or toy apps. If you use any of the experimental libraries and find bugs or have ideas for improving them, please don't hesitate to create an issue.
Iterating
Keep your applications in control by allowing them to consume data streams at their own pace using a pull-based 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 iterating recipes for more guidance on how to use this library.
</details>
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.
Messaging Introduction
<details> <summary>Click here to see documentation still under construction</summary>
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 so 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).
Messaging Basics
- getting started
- testing command and query handlers
- solving cross-cutting concerns with middlewares (e.g. validation or retrying on failure)
- testing command and query handlers that have middleware pipelines
- testing middlewares and reusable pipelines
Messaging Advanced
- exposing commands and queries via HTTP
- testing HTTP commands and queries
- calling HTTP commands and queries from another application
- testing code which calls HTTP commands and queries
- creating a clean architecture and modular monolith with commands and queries
- moving from a modular monolith to a distributed system
- using a different dependency injection container (e.g. Autofac or Ninject) (to-be-written)
- customizing OpenAPI specification for HTTP commands and queries (to-be-written)
- re-use middleware pipelines to solve cross-cutting concerns when calling external systems (e.g. logging or retrying failed calls) (to-be-written)
Messaging 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)
Messaging Cross-Cutting Concerns
- authenticating and authorizing commands and queries (to-be-written)
- logging commands and queries (to-be-written)
- validating commands and queries (to-be-written)
- caching query results for improved performance (to-be-written)
- making commands and queries more resilient (e.g. through retries, circuit breakers, fallbacks etc.) (to-be-written)
- executing commands and queries in a database transaction (to-be-written)
- timeouts for commands and queries (to-be-written)
- metrics for commands and queries (to-be-written)
- tracing commands and queries (to-be-written)
</details>
Recipes for experimental functionalities
<details> <summary>Click here to see recipes for experimental functionalities</summary>
Signalling Introduction
Signalling is a way to refer to the publishing and observing of signals via the publish-subscribe pattern. Signalling is a good way to decouple or loosely couple different parts of your application by making an event publisher agnostic to the observers of signals it publishes. In addition to this basic idea, Conqueror allows solving cross-cutting concerns on both the publisher as well as the observer side.
Signalling 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 or retrying on failure) (to-be-written)
- testing event observers with pipelines (to-be-written)
- testing event publisher pipeline (to-be-written)
- testing middlewares (to-be-written)
Signalling 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)
- enforce that all event observers declare a pipeline (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)
Signalling 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)
Signalling Cross-Cutting Concerns
- logging events (to-be-written)
- retrying failed event observers (to-be-written)
- executing event observers in a database transaction (to-be-written)
- metrics for events (to-be-written)
- tracing events (to-be-written)
Iterating Introduction
For data streaming Conqueror uses a pull-based approach where the consumer controls the pace (using IAsyncEnumerable
), which is a good approach for use cases like paging and event sourcing.
Iterating Basics
- getting started (to-be-written)
- testing streaming request handlers (to-be-written)
- solving cross-cutting concerns with middlewares (e.g. validation or retrying on failure) (to-be-written)
- testing streaming request handlers that have middleware pipelines (to-be-written)
- testing middlewares (to-be-written)
Iterating Advanced
- using a different dependency injection container (e.g. Autofac or Ninject) (to-be-written)
- reading 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 streaming HTTP clients (to-be-written)
- optimize HTTP streaming performance with pre-fetching (to-be-written)
- enforce that all streaming request handlers declare a pipeline (to-be-written)
- re-use middleware pipelines to solve cross-cutting concerns when consuming streams from external systems (e.g. logging or retrying failed calls) (to-be-written)
- authenticating and authorizing streaming requests (to-be-written)
- moving from a modular monolith to a distributed system (to-be-written)
Iterating 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 streams via other transports (e.g. SignalR) (to-be-written)
- building test assertions that work for HTTP and non-HTTP streams (to-be-written)
Iterating Cross-Cutting Concerns
- authenticating and authorizing streaming requests (to-be-written)
- logging streaming requests and items (to-be-written)
- validating streaming requests (to-be-written)
- retrying failed streaming requests (to-be-written)
- timeouts for streaming requests and items (to-be-written)
- metrics for streaming requests and items (to-be-written)
- tracing streaming requests and items (to-be-written)
</details>
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 cross-cutting concerns in an transport-agnostic fashion, and provides extension packages that allow exposing the business functionality via different transports (e.g. HTTP).
A useful side-effect of moving the handling of cross-cutting concerns away from the concrete transport, is that it allows solving cross-cutting concerns for both incoming and outgoing operations. For example, with Conqueror the exact same code can be used for adding retry capabilities for your own command and query handlers as well as when calling an external HTTP API.
On an architectural level, 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).
Out-of-the-box solutions for many common yet often complex cross-cutting concerns: Many development teams spend valuable time on solving common cross-cutting concerns like validation, logging, error handling etc. over and over again. Conqueror provides a variety of pre-built middlewares that help you address those concerns with minimal effort.
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 recommended 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 | 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. |
-
net8.0
- Conqueror.Abstractions (>= 0.8.0-beta.3)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
NuGet packages (7)
Showing the top 5 NuGet packages that depend on Conqueror:
Package | Downloads |
---|---|
Conqueror.CQS
Part of the Conqueror library set. Powercharge your application by using command-query-separation (CQS). |
|
Conqueror.Eventing
Part of the Conqueror library set. Make your application reactive by publishing and observing events. |
|
Conqueror.Transport.Http.Client
Part of the Conqueror library set. Create HTTP clients for servers using the Conqueror package. |
|
Conqueror.Transport.Http.Server.AspNetCore
Part of the Conqueror library set. Automatically generate HTTP endpoints for apps using ASP Core and Conqueror. |
|
Conqueror.Common.Transport.Http.Server.AspNetCore
Part of the Conqueror library set. Common ASP.NET Core integrations. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
0.8.0-experimental.2 | 93 | 4/27/2025 |
0.8.0-experimental.1 | 96 | 4/27/2025 |
0.8.0-beta.3 | 111 | 4/28/2025 |
0.7.0-experimental.10 | 119 | 4/23/2025 |
0.7.0-experimental.9 | 120 | 4/21/2025 |
0.7.0-experimental.8 | 120 | 4/21/2025 |
0.7.0-experimental.7 | 127 | 4/21/2025 |
0.7.0-experimental.6 | 118 | 4/20/2025 |
0.7.0-experimental.5 | 124 | 4/18/2025 |
0.7.0-experimental.4 | 255 | 4/17/2025 |
0.7.0-experimental.3 | 141 | 4/17/2025 |
0.7.0-experimental.2 | 173 | 4/16/2025 |
0.7.0-beta.1 | 147 | 3/10/2025 |
0.6.0-beta.2 | 86 | 8/25/2024 |
0.6.0-beta.1 | 83 | 8/13/2024 |
0.5.0-beta.4 | 192 | 11/19/2023 |
0.5.0-beta.3 | 119 | 7/18/2023 |
0.5.0-beta.2 | 105 | 7/15/2023 |
0.5.0-beta.1 | 103 | 4/22/2023 |
0.4.0-beta.2 | 123 | 2/26/2023 |
0.4.0-beta.1 | 107 | 2/25/2023 |
0.3.0-beta.3 | 116 | 2/12/2023 |
0.3.0-beta.2 | 128 | 1/9/2023 |
0.3.0-beta.1 | 129 | 1/7/2023 |
0.2.0-beta.3 | 129 | 1/1/2023 |
0.2.0-beta.2 | 129 | 11/9/2022 |
0.2.0-beta.1 | 126 | 11/6/2022 |
0.1.0-beta.21 | 120 | 10/29/2022 |
0.1.0-beta.20 | 119 | 10/28/2022 |
0.1.0-beta.19 | 159 | 10/20/2022 |
0.1.0-beta.18 | 124 | 10/16/2022 |
0.1.0-beta.17 | 126 | 10/16/2022 |
0.1.0-beta.16 | 117 | 10/16/2022 |
0.1.0-beta.15 | 119 | 10/15/2022 |
0.1.0-beta.14 | 136 | 8/20/2022 |