Skip to content
panahisterPublic

About

A backend framework for .NET 10: the execution model, transactional outbox, idempotency, failures over REST and gRPC, security and observability, as NuGet packages, a template and a generator.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

MP Core

The backend architecture you would have built anyway: already built, tested, and explained.

A framework for .NET 10 that makes the decisions every backend makes again,
once, in the open, with a test for each and the name of the person who first described it.

1.0.0 is a promise. Its public API, its command line tool and its template change only by the rules of Versioning: a patch fixes, a minor version adds, and a break waits for 2.0. What changed since 0.9.3.

NuGet ci license .NET

Get started · Conventions · Capabilities · Reference architecture · Ecosystem · AI agents · Storefront · Tiffin · MP Frontend · Decisions · Release notes

A backend platform: an API gateway, an identity provider, backends built on MP Core, Kafka and RabbitMQ, PostgreSQL, TimescaleDB and Redis, and OpenTelemetry

Why MP Core

A backend is mostly decisions that have nothing to do with its business. Who opens the transaction, and who commits it? Does a message leave when the change it announces was rolled back? What does a caller see when a rule is broken, and does it look the same over REST and over gRPC? What happens when the same request arrives twice?

Every team answers these, every time, under a deadline. The answers differ from service to service, and the ones that are wrong stay hidden until production finds them.

The pain What usually happens With MP Core
A message is sent, and the change it announces is lost, or the other way round Publish, then save, and hope The change, its messages, the audit record and the idempotency key are one commit. A message leaves if, and only if, the change was committed
A client retries a request after a timeout, and the customer is charged twice Each endpoint invents its own guard, or has none RequireIdempotencyKey(): a repeat receives the first answer, and nothing happens twice
A broker delivers a message twice Every consumer is written to be careful, and one is not An inbox in the consumer's own transaction stops the second delivery before the handler
Every service reports failure in its own way Status codes, strings, exceptions that leak a stack trace One failure model. REST answers Problem Details (RFC 9457), gRPC a rich status, both in the caller's language
Business rules are scattered over controllers, handlers and the database Nobody can list the rules of the system A rule is a class with a name, checked by the aggregate, reported under its own code
Security is added at the end One endpoint is forgotten Deny by default. An endpoint without a policy requires a token, and identity comes from the token only
Nobody can see what happened Logs in one place, traces nowhere OpenTelemetry logs, traces and metrics from the first request, with secrets redacted
An AI agent writes code that compiles and breaks the architecture Review catches some of it Ten skills ship with every backend and tell the agent what to establish, what to ask, and what it may not decide alone

You write the business. MP Core is everything around it, the same in every backend, so that the second service a team builds is as sound as the first.

One ecosystem, clear ownership

The MP ecosystem: MP Core and MP Frontend foundations build Storefront and Tiffin reference products, while Keycloak and APISIX provide explicit identity and edge boundaries

MP Core and MP Frontend are reusable foundations. Storefront and Tiffin are independently runnable references. Tiffin keeps identity and edge configuration in their own repositories, so each boundary has one reviewable owner. The ecosystem guide maps every repository, responsibility, and non-goal.

Everything it does

The twelve areas of MP Core's capabilities: domain model, use cases, one commit, messaging, idempotency, security, transport, business audit, language, data and cache, operations, tooling

The catalogue of capabilities has every line of this picture, with the package that carries it and how it was proved: by a test of this repository, or by a scenario of a sample that runs against live backends. It ends with what MP Core deliberately does not do, because a framework that says yes to everything guarantees nothing.

Start in two minutes

dotnet tool install --global MPCore.Cli --version 1.0.1
dotnet new install MPCore.Templates::1.0.1
mpcore new backend --organization Acme --component Orders --output ./orders --shape modular-monolith --transport both --messaging kafka --cache hybrid --business-audit postgresql

You get a backend that builds, starts, protects every endpoint, and carries its own guides and skills, written for the options you chose. Getting started takes you from there to a first use case with a rule, an endpoint and a test.

Choice Values
--shape service, modular-monolith
--transport rest, grpc, both
--messaging kafka, rabbitmq, none
--cache none, memory, redis, hybrid
--business-audit none, postgresql
--timeseries none, timescale
--ai-tooling none, codex, claude, both

The architecture

The picture at the top is the whole platform. The reference architecture describes every part of it: what it is for, how MP Core works with it, and how far that was proved.

One request, end to end

A request passes through transport, security and validation, reaches your handler, is committed together with its messages, and the messages are released to the broker

A handler is a static method. It takes a command and the ports it needs, changes an aggregate, and returns. It never saves: MP Core saves after the handler returns, and releases the messages only after the commit. Concepts explains each step and what is guaranteed when one fails.

Inside one backend

Four layers: the API host, the application, the domain and the infrastructure, with what you write in each and the MP Core packages that serve it

Works with

MP Core relies on standards, not on products: OpenID Connect for identity, X-Forwarded-* for a gateway, OTLP for telemetry. A product that speaks the standard fits. The table says which ones this project has run, and which ones it has not.

Part Product How MP Core works with it Proved
API gateway Apache APISIX TLS ends at the edge; REST and gRPC are routed to separate listeners; forwarded headers are believed from trusted proxies only; identity headers a caller could forge are removed Run by the scenarios of both samples
API gateway WSO2 API Manager, or another gateway The same: it forwards the bearer token and the X-Forwarded-* headers Fits by standard. Not run by this project
Identity Keycloak A preset for realm roles, client roles and service accounts; one audience per backend; a service's own token by client credentials. A service account is recognised by client_id, which Keycloak 25 and later write only for a client with the service_account scope Run by the scenarios of both samples; client credentials by Tiffin's
Identity WSO2 Identity Server, Microsoft Entra ID, another OpenID Connect provider A plain mapping of claims to roles, without a provider's name in the code Fits by standard. Unit tests only
Messaging Apache Kafka Integration events, partitioned by key; an outbox on the way out, an inbox on the way in; the tenant in a header Run by the scenarios of both samples
Messaging RabbitMQ Work for one reader, on durable queues Run by the scenarios of both samples; a saga's requests and answers in Tiffin
Execution Wolverine Handlers, middleware, durable local queues and the outbox Run by the framework's own tests
Data PostgreSQL with Entity Framework Core The unit of work, the outbox, the audit trail, idempotency and stored translations Run by the framework's integration tests
Data TimescaleDB Hypertables, compression and retention, created in a migration Run by the scenarios of both samples
Cache Redis A distributed cache, or the second level behind an in-process one Run by the scenarios of both samples
Telemetry OpenTelemetry with Jaeger, Prometheus and Grafana Logs, traces and metrics over OTLP, each signal to its own destination Run by Storefront's scenarios. Configured in Tiffin and not looked at

Domain-Driven Design

MP Core does not model your business, and holds none. It gives every building block of Domain-Driven Design a place to live and a guarantee to rely on, so that the model you draw on a whiteboard is the model in the code.

The building blocks of Domain-Driven Design, strategic and tactical, and the MP Core type that carries each
When you decide MP Core gives you
Where a bounded context ends A module, which is one project the compiler guards, or a service. The same code moves from one to the other: between modules a message changes its transport and nothing else
How two contexts talk A message that commits with the change, or a read-only interface in a Contracts project. A call that writes is possible, and is a recorded decision
What an aggregate protects CheckRule: a rule is checked before the state changes, so an aggregate is never invalid
What a reader may ask A query: its own message, no unit of work, a read model that returns views. It is the only thing a GET sends

An audit trail that can be trusted

A log helps an operator find a fault, and is thrown away after some weeks. An audit trail answers a question somebody will ask in a year, perhaps a regulator: who changed this, when, from what to what, and who tried and was refused? Most backends build it late, as log lines, and find out in the first review that a log cannot answer it.

Business audit: the change and its record are saved in one commit; a refused attempt is recorded apart, so that it survives the rollback
What a reviewer asks How MP Core answers
Can the trail claim a change that did not happen? No. The record is saved in the commit of the change it describes; rolled back, neither exists
Is an attempt that was refused recorded? Yes, on a connection of its own, so that it survives the rollback of what was refused
Who is the actor? Whoever the validated token names. Work from a queue runs as a named actor, such as system:ReserveStock, so that no record is anonymous
Can a secret reach the trail? A policy names the fields that are kept. A property that looks like a credential is refused; an identifier is masked
Can a record be changed afterwards? The database role of the backend may insert and read, and nothing else
Where is it? In the backend's own database, next to the business data, and readable through a port: IAuditQuery

Choose it with --business-audit postgresql. A handler records a business action in one line, audit.RecordAsync(...); the changes of an entity are recorded without a line. In Storefront, scenario S3 reads the record of a price change that was refused.

Every message in the caller's language

A backend that answers "Price jump too large" has decided, in code, that every caller reads English. A backend that answers a code alone has left the sentence to every client, and each will write its own.

In MP Core no sentence is written in code. A rule, a validator and a failure carry a key and their arguments. The text is found when it is shown.

Localization: a key and its arguments become a text in the language the caller asked for, from stored translations, resource files and MP Core's own texts
You need MP Core gives you
The caller's language Accept-Language, over REST and over gRPC, with the same result
Your languages English is MP Core's only built-in language, and the default. Your repository adds the others, or puts one in English's place: a resource file per culture, MP Core's own keys included. The language guide
A fallback A culture, then its parents (en-US-POSIX, en-US, en), then the default. A text that exists nowhere is counted on a metric and logged once, never invented
Numbers in the sentence Named placeholders: {max_move_percent}, filled from the rule's arguments
A text changed without a release Stored translations: support edits a text, and every instance shows it within its refresh interval
A client that acts on the failure The code and the error domain never change with the language; the text is for people

In Storefront, scenario S13 has a member of staff store a translation over the Basket's resource file while the backend runs; the next refusal reads that stored text when the caller asks for the translation's culture, and the resource file's own English text otherwise. The sample's languages are the sample's own, in its own repository; MP Core carries none of them.

Built for AI coding agents

An agent that writes a handler which calls SaveChanges, or an endpoint that reads the user from a header, has written code that compiles, passes review on a busy day, and breaks a guarantee. Conventions an agent cannot see are conventions it will not keep.

So every generated backend carries ten skills. A skill is a procedure, not a prompt: what to establish before writing, what to ask the owner, what must never be decided alone, and how to prove the result.

Ten skills in five steps: plan, build, protect and operate, connect, prove. One body per skill, found by Claude Code and by Codex
Skill The agent uses it to
mpcore-plan-bounded-context Turn an approved context into a plan: aggregates, invariants, commands, queries, and the ambiguities it refuses to guess
mpcore-implement-ddd-module Create the skeleton of a module, registered and compiling, with no business in it yet
mpcore-implement-vertical-slice Build one capability end to end: rule, handler, persistence, endpoint, authorization, tests
mpcore-design-transport-contract Design the REST or gRPC contract for the transport the backend has, and say what would break a client later
mpcore-apply-security Restrict a capability to a role, from the token only, with tests for no token, the wrong right, and another actor's record
mpcore-apply-business-audit Record who did what, and decide what must never be recorded
mpcore-apply-observability Add the signal an operator would act on, and keep secrets out of logs, labels and messages
mpcore-configure-messaging Declare topics, queues, partition keys, retries, and what happens when a message is given up
mpcore-integrate-contexts Connect two contexts, or two services that share no code, and hold their contract with a test
mpcore-verify-business-behavior Prove the capability: a rule by its code, a guarantee by running it

One body, two agents. Each skill is written once, in .mpcore/skills/<name>/SKILL.md. Claude Code finds it through CLAUDE.md and .claude/skills/, Codex through AGENTS.md and .agents/skills/. Choose with --ai-tooling claude, codex or both.

See it done. In Storefront, the same task was given to Claude Code and to Codex, each with the skill and nothing else. Building with AI agents has the task as it was given, what each agent did, and what a person still had to check.

What every skill holds to. It reads the backend's manifest first, because the shape, the transport and the broker were decided when the backend was generated. It never invents a business rule to fill a gap. It never weakens a security default to make a test pass. It reports the real output of the tests.

What is guaranteed, and how we know

A guarantee about failure or concurrency is proved by a test that was seen failing without the code that keeps it, against real PostgreSQL, or by an experiment against a running system. A sentence in a document proves nothing.

Guarantee How it was proved
A message never leaves without the change that caused it, even when a save fails and is retried Eight checkouts of one basket at the same moment, repeated. Before the fix: 3 accepted, 16 orders. After: 5 accepted, 5 orders
A request repeated with the same key runs once Two concurrent attempts with one key: one commits, both callers receive its answer
An event delivered three times is handled once A test against a real broker host, seen failing with the inbox removed
A failure returned after a change rolls the change back Integration tests; and in Storefront, a refused checkout leaves the basket as it was
A refused attempt is still audited, although its transaction was rolled back An integration test: the audit record survives the rollback
No endpoint is reachable without a token unless it says so A test reads the composition of a generated backend and names the only endpoints that may be anonymous; the scenarios of both samples ask each backend without a token
A tenant travels with a message and with a call between services, and is believed from a listed service only Tests seen failing: the handler saw no tenant, and each of six guards broken on purpose failed its test. In Tiffin, Payments' audit trail names the city of all 20 payments the scenarios open; with Ordering taken off Payments' list, the next one names none
A published version is never built again The release gate verifies the packed files by their hashes, and proves that it rejects fifteen kinds of bad file

Storefront's lessons and Tiffin's findings tell what was wrong before each of these was true.

On the shoulders of

Nothing in MP Core is an invention. Every convention is a pattern somebody described, chosen over its alternatives for a reason that is written down. The decision records name the source of each; these are the ones the framework rests on.

Idea Who described it Where it lives in MP Core
Bounded context, aggregate, entity, value object, repository Eric Evans, Domain-Driven Design (2003) MPCore.Domain; one module per bounded context
One aggregate per transaction; eventual consistency between aggregates Vaughn Vernon, Implementing Domain-Driven Design (2013) A handler changes one aggregate; another module is told by a message
The always-valid domain model Vladimir Khorikov, Always-Valid Domain Model CheckRule: a rule is checked before the state changes
Business rules as named objects; modules that integrate through events Kamil Grzybek, Modular Monolith with DDD BusinessRule; module messages
Unit of Work, Repository; the smell of primitive obsession Martin Fowler, Patterns of Enterprise Application Architecture; with Kent Beck, Refactoring IUnitOfWork, IRepository; value objects instead of decimal and string
Ports and Adapters Alistair Cockburn (2005) The application layer knows ports only; adapters live in Infrastructure
Package by component; the modular monolith Simon Brown, "The Missing Chapter" in Clean Architecture One project per module; the compiler guards the boundary
Command-query separation; CQRS Bertrand Meyer; Greg Young ICommand<T> and IQuery<T>; a query declares no unit of work
Vertical slices Jimmy Bogard One use case per file: the message and its handler together
Transactional outbox; saga Chris Richardson, Microservices Patterns (2018) Messages are released after the commit
Idempotent receiver; process manager; messaging channels Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns (2003) The inbox; Process/ folders; Kafka for events, RabbitMQ for work
Why delivery at least once requires idempotence Pat Helland, Idempotence Is Not a Medical Condition (2012) The inbox and business keys
The Idempotency-Key header The IETF HTTPAPI working group; Stripe; Brandur Leach on its implementation with PostgreSQL IIdempotentExecutor, RequireIdempotencyKey()
Consumer-driven contracts Ian Robinson (2006) A test reads what one side sends with the other side's copy
Problem Details for HTTP APIs RFC 9457 Every failure over REST
Safe and idempotent methods RFC 9110 A GET sends a query and changes nothing
Exponential backoff with jitter Marc Brooker, AWS Architecture Blog (2015) Retry rules for a save that lost a race
Architecture decision records Michael Nygard, Documenting Architecture Decisions (2011) docs/decisions
Trusted publishing OpenSSF, Trusted Publishers for All Package Repositories The packages are published with no stored key

MP Core also learned from code that others gave away: Microsoft's eShop, Jason Taylor's and Steve Smith's Clean Architecture templates, and Jeremy D. Miller's Wolverine, on which its execution model stands.

The samples

Two samples: Storefront, an online store built as a modular monolith and two services, with 290 tests and 22 scenarios; and Tiffin, food delivery in nine services and two cities, with 313 tests and 16 scenarios of 286 checks. What each is built of and what it proves first

Two samples run on MP Core, and on GitHub on every change. Each is where a part of MP Core is proved, and each found what MP Core then fixed.

Storefront Tiffin
What it is An online store Food delivery, in two cities
Its shape A modular monolith of four modules, and two services Nine services, a database each, no shared assembly
Behind Apache APISIX, Keycloak Apache APISIX, Keycloak, and Access in front of Keycloak's administration
Between them Kafka and RabbitMQ Kafka, and RabbitMQ for a saga's requests and answers
Data PostgreSQL, TimescaleDB, Redis PostgreSQL, TimescaleDB, Redis, and RustFS or SeaweedFS behind the S3 API
Proved by 290 tests; 22 scenarios, 146 and 159 checks 313 tests; 16 scenarios, 286 checks with each file store
Built with MP Core 1.0.0 MP Core 1.0.0
Start with the learning path: thirteen steps through the code, one idea at a time the journey of one order, and what building it found
What MP Core gained from it the fixes of 0.9.0: lessons the tenant of a message and a service's own token (0.9.1), the tenant of a call (0.9.2)

Each starts with four commands. The numbers are those of the last runs on GitHub; each sample's README says which run.

What MP Core is not

  • It holds no business logic. No product entity, status or rule lives in the framework.
  • It is not a login server. A backend validates a token; signing in belongs to an identity provider.
  • It is not a gateway. It works behind one, and says exactly what it believes of it.
  • It is not event sourcing, and not CQRS over two databases. Commands and queries are separate messages over one PostgreSQL database by default.
  • It does not deploy. It generates no image, chart or pipeline that deploys: how a backend is deployed belongs to the platform it runs on.

Not shipped or not claimed

The guarantees above are proved by tests and by the samples. Tests do not qualify a deployment for production, and MP Core has not yet qualified one. Until a production qualification kit is built and proved on a reference sample, MP Core has not qualified:

  • production load;
  • the high availability and failover of a deployment;
  • backup and restore;
  • key custody;
  • SLOs and operations;
  • an independent security approval.

A product qualifies its own deployment before it serves production.

Packages

All packages share one version and ship together.

Group Packages
Core MPCore.Domain, MPCore.Application, MPCore.Hosting
Persistence MPCore.Persistence.Abstractions, MPCore.Persistence.EntityFrameworkCore.PostgreSql, MPCore.Persistence.Timescale
Messaging MPCore.Messaging.Abstractions, MPCore.Messaging.Wolverine, MPCore.Messaging.Wolverine.Kafka, MPCore.Messaging.Wolverine.RabbitMQ
Transport MPCore.Transport.Http, MPCore.Transport.Grpc
Security MPCore.Security.Abstractions, MPCore.Security.AspNetCore, MPCore.Tenancy.Abstractions
Validation and messages MPCore.Validation.FluentValidation, MPCore.Localization, MPCore.Localization.EntityFrameworkCore.PostgreSql
Idempotency MPCore.Idempotency.EntityFrameworkCore.PostgreSql
Audit MPCore.Audit.Abstractions, MPCore.Audit.EntityFrameworkCore.PostgreSql
Caching MPCore.Caching.Abstractions, MPCore.Caching.Memory, MPCore.Caching.Redis, MPCore.Caching.Hybrid
Operations MPCore.Observability, MPCore.Observability.Prometheus, MPCore.Resilience.Http
Tooling MPCore.Cli (the mpcore command), MPCore.Templates (dotnet new mpcore-backend)

docs/guide/packages.md says what each one is for.

Documentation

Read To
Getting started Generate a backend and write a first use case
Backend conventions Decide ownership and layers, then carry a vertical slice from contract to connected evidence
Capabilities Find everything MP Core does, and everything it does not
Reference architecture See the whole platform, part by part
Concepts Understand the execution model, the failure model, the three kinds of message, and what is guaranteed
Packages Choose what to reference
Languages Serve your own languages beside English, or one in its place, from your repository
Decisions Learn why: every convention, with its sources and its cost
Release notes See what changed, 1.0.0, 0.10.1, 0.10.0, 0.9.3, 0.9.2, 0.9.1 and 0.9.0 included

A generated backend carries its own guides (docs/architecture.md, docs/capabilities.md, src/Modules/README.md), written for the options you chose.

Build and test this repository

The API of every runtime package is compared with the last published version, and a build fails without that package. Once per checkout, and again when MPCoreBaselineVersion moves, download it from nuget.org and check it:

./eng/restore-api-baseline.sh
dotnet restore MPCore.sln --locked-mode
dotnet build MPCore.sln --configuration Release --no-restore
dotnet test MPCore.sln --configuration Release --no-build

Unit tests need nothing else. The integration tests run against real services and are skipped unless these variables name them:

Variable Service
MPCORE_TEST_POSTGRESQL a disposable PostgreSQL database (connection string)
MPCORE_TEST_TIMESCALE a disposable TimescaleDB database (connection string)
MPCORE_TEST_REDIS Redis (host:port)
MPCORE_TEST_RABBITMQ RabbitMQ (an amqp:// address)
MPCORE_TEST_KAFKA Kafka (host:port)

docker compose -f eng/compose.test.yaml up -d starts all five on your machine; CONTRIBUTING.md gives the value of each variable for them.

Versioning

MP Core follows Semantic Versioning. From 1.0.0 the rules below are a promise to the products that depend on it. ADR-020 gives the reasons and the alternatives that were not taken.

What is promised. The public API is the PublicAPI.Shipped.txt of each runtime package, which the review of 1.0 decided type by type; the command line tool and the template as recorded in docs/contracts; and the behaviour the guides document.

How a version may change it.

  • A patch version fixes. A minor version adds. A change that breaks the API, the tool or the template waits for the next major version.
  • A member is marked [Obsolete], with the one to use instead, for at least one minor version before the next major version removes it.
  • After a new major version is released, the previous major version gets security fixes, and fixes of defects that lose data or break security, for 12 months, and never past the end of support of the .NET version it targets.

What holds it. The build fails on an undeclared change of a runtime package's public API (Microsoft.CodeAnalysis.PublicApiAnalyzers), and package validation compares each package with the last published version. Tests compare the tool and the template with their recorded contracts, and keep the review of the public API and its suppressions in step with the inventories. The [Obsolete] rule and the behaviour of the guides are held by review and by the tests that name them, not by a check of their own.

What it does not cover. The text of log messages. Internal types, and anything not named above. An API marked [Experimental], each with a diagnostic id of its own that is listed here:

Diagnostic id Experimental API
none at 1.0.0

Three rules of ADR-010 also hold:

  • A published version is never rebuilt with different bytes. A corrected build takes the next version.
  • The runtime packages, the CLI and the template ship together, as one cohort under one version.
  • A generated repository references an exact version, never a range.

Contributing and security

CONTRIBUTING.md says how to propose a change. To report a vulnerability, follow SECURITY.md; do not open a public issue for it.

Licence and marks

MP Core is licensed under Apache-2.0. The names and marks of the products in the pictures belong to their owners and are used only to name those products; see NOTICE.

About

A backend framework for .NET 10: the execution model, transactional outbox, idempotency, failures over REST and gRPC, security and observability, as NuGet packages, a template and a generator.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages