Conqueror.Eventing.Abstractions 0.3.0-experimental.2

This is a prerelease version of Conqueror.Eventing.Abstractions.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Conqueror.Eventing.Abstractions --version 0.3.0-experimental.2
NuGet\Install-Package Conqueror.Eventing.Abstractions -Version 0.3.0-experimental.2
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="Conqueror.Eventing.Abstractions" Version="0.3.0-experimental.2" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Conqueror.Eventing.Abstractions --version 0.3.0-experimental.2
#r "nuget: Conqueror.Eventing.Abstractions, 0.3.0-experimental.2"
#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.
// Install Conqueror.Eventing.Abstractions as a Cake Addin
#addin nuget:?package=Conqueror.Eventing.Abstractions&version=0.3.0-experimental.2&prerelease

// Install Conqueror.Eventing.Abstractions as a Cake Tool
#tool nuget:?package=Conqueror.Eventing.Abstractions&version=0.3.0-experimental.2&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.

Build Status license

Conqueror only supports .NET 6+

Libraries

Conqueror.CQS

status-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.

NuGet version (Conqueror.CQS) NuGet version (Conqueror.CQS.Abstractions) NuGet version (Conqueror.CQS.Analyzers)

Middlewares:

NuGet version (Conqueror.CQS.Middleware.Logging)

Transports:

NuGet version (Conqueror.CQS.Transport.Http.Server.AspNetCore) NuGet version (Conqueror.CQS.Transport.Http.Client)

Conqueror.Eventing

status-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. Keep your applications scalable by moving events from a modular monolith to a distributed application with minimal friction.

Head over to our eventing recipes for more guidance on how to use this library.

NuGet version (Conqueror.Eventing) NuGet version (Conqueror.Eventing.Abstractions)

Conqueror.Streaming.Interactive

status-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.

NuGet version (Conqueror.Streaming.Interactive) NuGet version (Conqueror.Streaming.Interactive.Abstractions)

Transports:

NuGet version (Conqueror.Streaming.Interactive.Transport.Http.Server.AspNetCore) NuGet version (Conqueror.Streaming.Interactive.Transport.Http.Client)

Conqueror.Streaming.Reactive

status-only-concept

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.

NuGet version (Conqueror.Streaming.Reactive)

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 relevant CQS packages
dotnet add package Conqueror.CQS
dotnet add package Conqueror.CQS.Analyzers
dotnet add package Conqueror.CQS.Middleware.Logging
dotnet add package Conqueror.CQS.Transport.Http.Server.AspNetCore
// add Conqueror CQS to your services
builder.Services
       .AddConquerorCQS()
       .AddConquerorCQSTypesFromExecutingAssembly()
       .AddConquerorCQSLoggingMiddlewares();

builder.Services.AddControllers().AddConquerorCQSHttpControllers();
builder.Services.FinalizeConquerorRegistrations();

In IncrementCounterByCommand.cs create a command that increments a named counter by a given amount (for demonstration purposes the counter is stored in an environment variable instead of a database).

using Conqueror;

namespace Quickstart;

[HttpCommand(Version = "v1")]
public sealed record IncrementCounterByCommand(string CounterName, int IncrementBy);

public sealed record IncrementCounterByCommandResponse(int NewCounterValue);

public interface IIncrementCounterByCommandHandler
    : ICommandHandler<IncrementCounterByCommand, IncrementCounterByCommandResponse>
{
}

internal sealed class IncrementCounterByCommandHandler
    : IIncrementCounterByCommandHandler, IConfigureCommandPipeline
{
    // add logging to the command pipeline and configure the pre-execution log
    // level (only for demonstration purposes since the default is the same)
    public static void ConfigurePipeline(ICommandPipelineBuilder pipeline) =>
        pipeline.UseLogging(o => o.PreExecutionLogLevel = LogLevel.Information);

    public async Task<IncrementCounterByCommandResponse> ExecuteCommand(IncrementCounterByCommand command,
                                                                        CancellationToken cancellationToken = default)
    {
        // simulate an asynchronous operation
        await Task.CompletedTask;

        var envVariableName = $"QUICKSTART_COUNTERS_{command.CounterName}";
        var counterValue = int.Parse(Environment.GetEnvironmentVariable(envVariableName) ?? "0");
        var newCounterValue = counterValue + command.IncrementBy;
        Environment.SetEnvironmentVariable(envVariableName, newCounterValue.ToString());
        return new(newCounterValue);
    }
}

In GetCounterValueQuery.cs create a query that returns the value of a counter with the given name.

using Conqueror;

namespace Quickstart;

[HttpQuery(Version = "v1")]
public sealed record GetCounterValueQuery(string CounterName);

public sealed record GetCounterValueQueryResponse(int CounterValue);

public interface IGetCounterValueQueryHandler
    : IQueryHandler<GetCounterValueQuery, GetCounterValueQueryResponse>
{
}

internal sealed class GetCounterValueQueryHandler
    : IGetCounterValueQueryHandler, IConfigureQueryPipeline
{
    // add logging to the query pipeline and configure the pre-execution log
    // level (only for demonstration purposes since the default is the same)
    public static void ConfigurePipeline(IQueryPipelineBuilder pipeline) =>
        pipeline.UseLogging(o => o.PreExecutionLogLevel = LogLevel.Information);

    public async Task<GetCounterValueQueryResponse> ExecuteQuery(GetCounterValueQuery query,
                                                                 CancellationToken cancellationToken = default)
    {
        // simulate an asynchronous operation
        await Task.CompletedTask;

        var envVariableName = $"QUICKSTART_COUNTERS_{query.CounterName}";
        var counterValue = int.Parse(Environment.GetEnvironmentVariable(envVariableName) ?? "0");
        return new(counterValue);
    }
}

Now launch your app and you can call the command and query via HTTP.

curl http://localhost:5000/api/v1/commands/incrementCounterBy --data '{"counterName":"test","incrementBy":2}' -H 'Content-Type: application/json'
# prints {"newCounterValue":2}

curl http://localhost:5000/api/v1/queries/getCounterValue?counterName=test
# prints {"counterValue":2}

Thanks to the logging middleware we added to the command and query pipelines, you will see output similar to this in the server console.

info: Quickstart.IncrementCounterByCommand[0]
      Executing command with payload {"CounterName":"test","IncrementBy":2} (Command ID: cb9a2563f1fc4965acf1a5f972532e81, Trace ID: fe675fdbf9a987620af31a474bf7ae8c)
info: Quickstart.IncrementCounterByCommand[0]
      Executed command and got response {"NewCounterValue":2} in 4.2150ms (Command ID: cb9a2563f1fc4965acf1a5f972532e81, Trace ID: fe675fdbf9a987620af31a474bf7ae8c)
info: Quickstart.GetCounterValueQuery[0]
      Executing query with payload {"CounterName":"test"} (Query ID: 802ddda5d6b14a68a725a27a1c0f7a1d, Trace ID: 8fdfa04f8c45ae3174044be0001a6e96)
info: Quickstart.GetCounterValueQuery[0]
      Executed query and got response {"CounterValue":2} in 2.9833ms (Query ID: 802ddda5d6b14a68a725a27a1c0f7a1d, Trace ID: 8fdfa04f8c45ae3174044be0001a6e96)

If you have swagger UI enabled, it will show the new command and query and they can be called from there.

Quickstart Swagger

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

library-status-stable

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).

CQS Basics
CQS Advanced
CQS Expert

Eventing Introduction

library-status-experimental

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
Eventing Advanced
Eventing Expert

Interactive Streaming Introduction

library-status-experimental

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
Interactive Streaming Advanced
Interactive Streaming Expert

Reactive Streaming Introduction

library-status-only-concept

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).

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).

  • 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.
  • net7.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Conqueror.Eventing.Abstractions:

Package Downloads
Conqueror.Eventing

Part of the Conqueror library set. Make your application reactive by publishing and observing events.

Conqueror.Abstractions

Collection package that provides a simple entry point to all abstractions Conqueror packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.5.0-experimental.4 165 11/19/2023
0.5.0-experimental.3 94 7/18/2023
0.5.0-experimental.2 79 7/15/2023
0.5.0-experimental.1 79 4/22/2023
0.4.0-experimental.2 88 2/26/2023
0.4.0-experimental.1 80 2/25/2023
0.3.0-experimental.3 85 2/12/2023
0.3.0-experimental.2 91 1/9/2023
0.3.0-experimental.1 89 1/7/2023
0.2.0-experimental.3 98 1/1/2023
0.2.0-beta.2 102 11/9/2022
0.2.0-beta.1 99 11/6/2022
0.1.0-beta.21 99 10/29/2022
0.1.0-beta.20 88 10/28/2022
0.1.0-beta.19 124 10/20/2022
0.1.0-beta.18 95 10/16/2022
0.1.0-beta.17 84 10/16/2022
0.1.0-beta.16 82 10/16/2022
0.1.0-beta.15 87 10/15/2022
0.1.0-beta.14 134 7/31/2022
0.1.0-beta.13 112 7/31/2022
0.1.0-beta.12 111 7/30/2022
0.1.0-beta.11 99 7/30/2022
0.1.0-beta.10 100 7/30/2022
0.1.0-beta.9 98 7/30/2022
0.1.0-beta.8 106 7/16/2022
0.1.0-beta.7 122 7/3/2022
0.1.0-beta.6 118 4/19/2022
0.1.0-beta.5 122 4/17/2022
0.1.0-beta.4 109 4/17/2022
0.1.0-beta.3 121 4/17/2022
0.1.0-beta.2 119 4/17/2022
0.1.0-beta.1 132 4/17/2022