Skip to content

Releases: panahister/mpcore

MP Core 1.0.1

Choose a tag to compare

@panahister panahister released this 15 Oct 06:52

MP Core 1.0.1

A patch version. It follows 1.0.0. No behaviour and no public API of any package changes: apart from its
version number and the baseline setting, the code of every package, of MPCore.Cli and of MPCore.Templates is the
code of 1.0.0 (git diff v1.0.0 HEAD -- src tools lists only version declarations, the baseline setting, the removed
suppression files and two README files). The version exists because the API baseline cannot move without one
(eng/check-version-moved.sh counts all of src as shipped).

Moving from 1.0.0

Nothing to do in a product.

Changed

Area Now
The API baseline MPCoreBaselineVersion is 1.0.0, the last published version: on pack, every runtime package of this cohort is validated against its 1.0.0 package, which eng/restore-api-baseline.sh downloads from nuget.org and checks. The CompatibilitySuppressions.xml files of the review of 1.0 are removed: 1.0.0 carries the reviewed API, so the breaks they recorded against 0.10.1 are in the baseline itself, and ApiReviewTests fail if one of them remains
CI and the release as in 1.0.0: they restore and check the baseline package by package, after the script's self-test; require mode for the whole restore is not switched on
One test's time bound tests only, nothing ships: ResourceKeyTests allowed 5 s from the first request of a host that had just started, and failed on a cold host with the status right. It now allows 20 s, 10 s below the 30 s the port holds on, so a hanging port that is not cut off still fails it

What stays as it is

The baseline names the last published version, so after 1.0.1 is published MPCoreBaselineVersion stays 1.0.0 until the
next version starts; that version's first change takes the baseline step. Nothing is wrong in the meantime: 1.0.1 has the
API of 1.0.0.

Checking a download

The release run freezes the SHA-256 of every package it built in SHA256SUMS.txt, uploaded with the packages
as the run's artifact. The sums are of the packages as the release run built them. nuget.org adds its
repository signature (.signature.p7s) to every package after upload, so a downloaded file has another
SHA-256. To check a download, verify its signatures with dotnet nuget verify --all <file> and compare its
entries other than .signature.p7s with the built package.

MP Core 1.0.0

Choose a tag to compare

@panahister panahister released this 14 Oct 12:36

MP Core 1.0.0

The first version that makes a promise. From here a patch fixes, a minor version adds, and a break waits for the next
major version (README, Versioning; ADR-020). The public
API was decided once, type by type (the review): 35 types and 2 members became internal, and 226 types stay.

A product that moves from 0.9.3 reads this page. It follows 0.10.1, which followed
0.10.0.

Every behaviour change since 0.9.3

One row for each change of behaviour, API, tooling or generated output since 0.9.3, with what to do and where it is
explained. "Nothing" means a product has nothing to change.

Version Change What to do Details
0.10.0 CurrentActor.AuthenticatedAt is when the person authenticated, and null when the token does not say. Keep reading AuthenticatedAt, or call IsAuthenticationFresh(maximumAge, now); read the new IssuedAt for the issue time. 0.10.0, Moving from 0.9.3
0.10.0 A second call of AddMPCoreBearerAuthentication fails. A host with two issuers replaces both calls with one AddMPCoreBearerIssuers. 0.10.0, Changed
0.10.0 MP Core's own messages are English only. A product that served the Persian texts of 0.9.x copies that resource file into its own repository and registers it. 0.10.0, Moving from 0.9.3
0.10.0 MPCore.Localization carries English only and no satellite assembly. Nothing, beyond the entry above. 0.10.0, Changed
0.10.0 Several token issuers: AddMPCoreBearerIssuers registers one bearer scheme per issuer. Nothing: an addition. A host with two issuers uses it in place of two calls of AddMPCoreBearerAuthentication. 0.10.0, New
0.10.0 A second token as evidence: IMPCoreBearerTokenValidator validates a token of any configured issuer with that issuer's rules. Nothing: an addition. 0.10.0, New
0.10.0 A person's token as evidence beside the service's identity: AddMPCoreSubjectEvidence() on the calling side, AddMPCoreSubjectEvidenceValidation(...) and UseMPCoreSubjectEvidence() on the called side. Nothing: an addition. A command that needs the person declares RequireSubjectEvidence(...). 0.10.0, New
0.10.0 Mutual TLS between services: AddMPCoreMutualTls(...) and RequireWorkloadCertificate() on the called side, AddMPCoreClientCertificate(...) on a client. Nothing: an addition, off until a host calls it. 0.10.0, New
0.10.0 The generated host carries mutual TLS: Security:MutualTls in appsettings.json, off by default; the host calls AddMPCoreMutualTls when it is on, and its pipeline has UseMPCoreCertificateForwarding() and UseMPCoreMutualTls(), which do nothing while it is off. Nothing while it is off. A repository generated before keeps its host; to adopt it, copy the setting and the three calls from a new one. 0.10.0, New
0.10.0 A value that never prints: SensitiveValue, and AddMPCoreSensitiveMessageTypes(...) for types whose objects are masked whole. Nothing: an addition. The registry it fills, SensitiveMessageTypes, became internal in 1.0.0; register types with AddMPCoreSensitiveMessageTypes. 0.10.0, New
0.10.0 The name processors mask more: dotted names with a sensitive segment, sensitive names inside a nested collection, a SensitiveValue, and an object of a registered message type. Nothing, unless a test or a dashboard matched on a value that is now ***. 0.10.0, New
0.10.0 MPCore.Observability references MPCore.Application. Nothing: restore pulls it. 0.10.0, New
0.10.0 A console sink after the redaction: MPCoreObservabilityOptions.EnableConsoleLogExporter, off by default. Nothing: an addition. 0.10.0, New
0.10.0 The generated host prints through the pipeline only: ClearProviders() and Observability:EnableConsoleLogExporter. A repository generated before keeps its host; to adopt it, copy the line and the setting from a new one. 0.10.0, New
0.10.0 Token introspection (RFC 7662): AddMPCoreTokenIntrospection(...) and ITokenIntrospector. Nothing: an addition. 0.10.0, New
0.10.0 Resource keys: AddMPCoreResourceKeys(...), RequireResourceKey("<key>"), [ResourceKey("<key>")] and ExemptFromResourceKey(); the product implements IResourceAuthorizer. Nothing until a host turns them on; then every endpoint declares a key or an exemption, or the host fails to start. 0.10.0, New
0.10.0 Module rules as tests: a generated modular monolith carries ModuleRulesTests, which fail on code that breaks a module rule. A repository generated before copies the tests, runs them and fixes what they name. 0.10.0, New
0.10.0 A product's own culture in English's place, proved over REST and gRPC; no API changes. Nothing: a product that serves its own culture makes it the only supported culture and the default of both transports, as the language guide says. 0.10.0, New
0.10.0 The identity provider administered by a governed adapter (ADR-019): the template's skills allow it in the one backend the owner names, under the six rules of mpcore-apply-security; the generated README.md, Program.cs and docs/architecture.md say the same. Nothing: no package and no API change. A repository generated before keeps its skills and texts; a backend that administers the identity provider follows ADR-019 and the skill of a new repository. 0.10.0, New
0.10.0 What a module owns, as tests: ModuleRulesTests also checks one schema for each module, no foreign key across schemas, the reason of a Contracts interface that writes (CrossModuleWriteAttribute), and that a handler takes only its own module's repositories. A repository generated before copies the tests, runs them and fixes what they name. 0.10.0, New
0.10.0 The cohort version Nothing: the packages, the tool and the template move together. 0.10.0, Changed
0.10.0 The release gate Nothing: it concerns the maintainers. 0.10.0, Changed
0.10.0 The template Nothing: its comments and documents say that MP Core's messages are English and that the repository adds its languages. 0.10.0, Changed
0.10.0 What MP Core does not do Nothing: the capability catalogue names four kinds of platform work as not available, and their owner. 0.10.0, Changed
0.10.1 The list of known keys: IMessageCatalog.ListKnownKeys(). Nothing: an addition. An IMessageCatalog the product implements itself throws NotSupportedException from it until it implements the member. 0.10.1, New
0.10.1 Typed message arguments: MessageArgument and FailureMessageDescriptor(key, arguments, typedArguments). Nothing: an addition; a message with string arguments only renders as before. 0.10.1, New
0.10.1 A product's formatter for typed arguments: catalog.UseArgumentFormatter(IMessageArgumentFormatter). Nothing: without one, a typed argument renders in its invariant form in every culture. 0.10.1, New
0.10.1 The API baseline Nothing: it concerns the maintainers. 0.10.1, Changed
0.10.1 The template A repository generated by the template alone has a solution file at its root, <Product>.slnx; one generated by mpcore new backend also holds a second, see the fix below. 0.10.1, Changed
0.10.1 The template's security skill Nothing: it now says what ADR-019 section 6 says. 0.10.1, Changed
0.10.1 The release workflow Nothing: it concerns the maintainers. 0.10.1, Changed
1.0.0 35 public types became internal. Read "Moving from 0.10.x": each has what to use instead. None was used by a guide, the template, a skill or a reference sample. 1.0.0, Moving from 0.10.x
1.0.0 2 public members became internal. Nothing: HandlerActorScope.ActorName and HandlerTenantScope.TenantOf were helpers of the scopes' own code. 1.0.0, Moving from 0.10.x
1.0.0 A direct Render refuses a typed argument name that is not an argument name. Name a typed argument with a lower-case letter followed by lower-case letters, digits or underscores. 1.0.0, Moving from 0.10.x
1.0.0 Building MP Core itself needs a checked baseline. Maintainers run ./eng/restore-api-baseline.sh once per checkout; a product does nothing. 1.0.0, Moving from 0.10.x
1.0.0 API validation Nothing: it concerns the maintainers. 1.0.0, Changed
1.0.0 The API baseline of this cohort Nothing: it concerns the maintainers. 1.0.0, Changed
1.0.0 CI and the release Nothing: they restore and check the baseline before they build. 1.0.0, Changed
1.0.0 NuGet.config Nothing: it concerns the maintainers. 1.0.0, Changed
1.0.0 The contract of the tool and the template Nothing: it records what the tool and the template already do. 1.0.0, Changed
1.0.0 The guides of a generated backend Add a project you create later to the solution with dotnet sln add. 1.0.0, Changed
1.0.0 The compatibility promise Read README, Versioning, and ADR-020. 1.0.0, Changed
1.0.0 A repository generated by mpcore new backend and 0.10.1 holds tw...
Read more

MP Core 0.10.1

Choose a tag to compare

@panahister panahister released this 12 Oct 17:17

MP Core 0.10.1

A patch version: additions only. It follows 0.10.0.

Moving from 0.10.0

Nothing to do. Every change is an addition; IsKnownKey, MP Core's own texts and the rendering of every message
with string arguments only are unchanged.

New

Package What it adds
MPCore.Localization The list of known keys. IMessageCatalog.ListKnownKeys() returns every key that has a default text, once each and ordered by key (ordinal), as KnownMessageKey(Key, DefaultText, Placeholders, Origin): the neutral default text rendering falls back to, the names of its placeholders by the rule the catalog renders with, and MessageTextOrigin.MPCore or MessageTextOrigin.Product. MP Core's own keys are in the list. A key in several sources is listed once, with the text of the highest precedence. A stored translation never adds a key. It reads no database and no network and is computed once. Sources list their keys through the optional IListableMessageTemplateSource, which ResourceMessageTemplateSource implements. ADR-012, addendum of 2026-10-12
MPCore.Application Typed message arguments. MessageArgument.Date(DateOnly), DateAndTime(DateTimeOffset), Integer(long) and Decimal(decimal), each with its invariant form (InvariantValue: ISO 8601, invariant digits). FailureMessageDescriptor(key, arguments, typedArguments) carries them beside the string arguments, in TypedArguments, within the same limits: 16 arguments in all, names by the same rule and never repeated, 256 characters a value. A date and time without an offset is refused: DateAndTime(DateTime) does not compile. ADR-012, addendum of 2026-10-12
MPCore.Localization A product's formatter for typed arguments. catalog.UseArgumentFormatter(IMessageArgumentFormatter) inside AddMPCoreMessageCatalog: the catalog calls it once for each typed argument with its name, its typed value and the negotiated culture, for REST detail, gRPC LocalizedMessage and IMessageCatalog.Render(key, arguments, typedArguments, culture) alike. Without one, a typed argument renders in its invariant form in every culture. A formatter that throws or returns null, white space or more than 256 characters gives the invariant form instead; the request does not fail, mpcore.localization.argument_fallback counts it, and the value is never logged

How to consume the list: the product exposes it through a query of its own, and its command that stores a
translation checks the key and the placeholders, because the store checks neither.

public sealed record ListTranslatableTexts : IQuery<IReadOnlyList<KnownMessageKey>>;

public static class ListTranslatableTextsHandler
{
    public static IReadOnlyList<KnownMessageKey> Handle(ListTranslatableTexts query, IMessageCatalog catalog) =>
        catalog.ListKnownKeys();
}

How to pass a typed argument, and how a product formats it:

// The application layer: the value, not its text.
var message = new FailureMessageDescriptor(
    "orders.ship_by_passed",
    new Dictionary<string, string> { ["order"] = order.Number },
    new Dictionary<string, MessageArgument>
    {
        ["deadline"] = MessageArgument.DateAndTime(order.ShipBy),
        ["total"] = MessageArgument.Decimal(order.Total),
    });

// The host: how each value looks in each language this product serves.
public sealed class ProductArgumentFormatter : IMessageArgumentFormatter
{
    public string? Format(string name, MessageArgument value, CultureInfo culture) => value.Value switch
    {
        DateTimeOffset instant => instant.ToString("g", culture),
        decimal amount => amount.ToString("N2", culture),
        _ => value.InvariantValue,
    };
}

builder.Services.AddMPCoreMessageCatalog(catalog => catalog
    .AddResources<OrderingMessages>()
    .UseArgumentFormatter(new ProductArgumentFormatter()));

The template keeps {deadline} and {total}; no text or stored translation changes.

Changed

Area Now
The API baseline MPCoreBaselineVersion is 0.10.0, the last published version: on pack, every runtime package of this cohort is validated against its 0.10.0 package. The baseline store is seeded with the 0.10.0 packages as the release run built them, which equal nuget.org's apart from its repository signature
The template a generated backend has a solution file at its root, <Product>.slnx, which lists exactly the projects generated for its shape; dotnet build --configuration Release and dotnet test at the root now work, as its docs/getting-started.md says. Before, the root build failed with MSB1003. The generated projects and their layout are unchanged. A product that adds a module may add its projects to the solution; the root build builds them anyway through the host's references
The template's security skill says what ADR-019 section 6 now says: the administration token is obtained and presented by AddMPCoreServiceIdentity and never reaches application code; a credential the adapter holds itself is a SensitiveValue; the client to the provider is registered with RedactLoggedHeaders for Authorization, and a test captures the log of one call. It shows that registration and that test; MP Core's tests build the example in a generated backend and run it. No runtime behaviour changes
The release workflow refuses to publish a release that the repository's README.md does not name in its install lines and its release-notes links (eng/check-readme-names-version.sh, which first proves with a self-test that it refuses a README naming another version). A prerelease of main is not checked: README names releases only

Known limits

Limit What to do
A source that does not implement IListableMessageTemplateSource adds no key, while IsKnownKey still asks it a product source that holds default texts implements the interface
The list is computed once per catalog resource files cannot change while the process runs; a source whose default texts can change is not listed again
An IMessageCatalog implemented by a product throws NotSupportedException from ListKnownKeys() implement the member, or use MP Core's catalog
A broken business rule's arguments and FluentValidation's limits stay strings pass a typed argument through FailureMessageDescriptor
A localizer of a product's own renders typed arguments only if it reads TypedArguments; an IMessageCatalog of its own that does not implement the new Render overload receives them in their invariant form implement the overload, or use MP Core's catalog
The formatter is an instance given at composition, shared by every request, and called for every typed argument of a rendered message give it what it needs when it is built; make it safe to call concurrently
Four kinds only pass a time of day, a duration or an amount with its currency as the kinds it is made of, or as a string

Checking a download

The release run freezes the SHA-256 of every package it built in SHA256SUMS.txt, uploaded with the packages
as the run's artifact. The sums are of the packages as the release run built them. nuget.org adds its
repository signature (.signature.p7s) to every package after upload, so a downloaded file has another
SHA-256. To check a download, verify its signatures with dotnet nuget verify --all <file> and compare its
entries other than .signature.p7s with the built package.

MP Core 0.10.0

Choose a tag to compare

@panahister panahister released this 12 Oct 12:06

MP Core 0.10.0

A minor version: three behaviours change, CurrentActor.AuthenticatedAt, a second call of
AddMPCoreBearerAuthentication, which now fails, and MP Core's own messages, which are English only. It follows
0.9.3.

Moving from 0.9.3

CurrentActor.AuthenticatedAt is when the person authenticated, and null when the token does not say.
It is read from auth_time only. Before, a token without auth_time gave its issue time (iat) instead, so
a token refreshed long after the person signed in looked freshly authenticated. What to do:

  • Code that judged how recently a person signed in keeps reading AuthenticatedAt, or calls
    IsAuthenticationFresh(maximumAge, now). A null AuthenticatedAt is never fresh; with Keycloak, an access
    token carries auth_time, and a refreshed one keeps the original.
  • Code that wanted the issue time reads the new IssuedAt.

Nothing else to do for a host with one issuer.

A host that called AddMPCoreBearerAuthentication twice, once per issuer, never worked: the second scheme
kept no authority and no issuer. It now fails at that second call. Replace both calls with one call of
AddMPCoreBearerIssuers, as below.

MP Core's own messages are English only. English is MP Core's only built-in language and its default;
every product decides its own languages in its own repository (ADR-012, amendment of 2026-10-08).
MPCore.Localization no longer carries the Persian translation of MP Core's keys (fa), which 0.9.0 to
0.9.3 shipped. A product that relied on it now receives MP Core's English text for those keys in fa. To
keep the Persian texts, the product supplies them itself:

  1. Copy src/MPCore.Localization/Resources/MPCoreMessages.fa.resx from the 0.9.3 source (tag v0.9.3)
    into the product as Resources/<Product>Platform.fa.resx. The keys of 0.10.0 are the keys of 0.9.3.
  2. Add an empty neutral Resources/<Product>Platform.resx and a marker class <Product>Platform beside
    them.
  3. Register it: AddMPCoreMessageCatalog(catalog => catalog.AddResources<<Product>Platform>()), next to the
    modules' resource files. At the product's precedence it wins over MP Core's English in fa.
  4. Keep fa in SupportedCultures of both transports. A product that serves it instead of English also
    makes it the DefaultCulture of both transports and of the catalog.

The language guide gives each step with placeholders and lists every key.

New

Package What it adds
MPCore.Security.AspNetCore Several token issuers. AddMPCoreBearerIssuers(issuers => issuers.Add("<scheme>", options => ...)): one bearer scheme per issuer, each with its own authority or metadata, issuer, audiences and keys and every rule of ADR-007 section 6, behind a default scheme (Bearer) that picks the issuer a token names. A token is checked only against the keys of that issuer; a token that names no configured issuer is refused with 401. Startup fails when two schemes name one issuer. ADR-016
MPCore.Security.AspNetCore A second token as evidence. IMPCoreBearerTokenValidator.ValidateAsync(token) validates a token of any configured issuer with that issuer's own parameters, keys refreshed from its metadata, and returns the validated issuer, the scheme and the claims, or a fixed reason. The token never becomes CurrentActor and is never logged. Registered by both AddMPCoreBearerAuthentication and AddMPCoreBearerIssuers

| MPCore.Security.Abstractions | SubjectEvidence, a bounded record of the person behind a service's call (subject, session, issuer, authentication time), and ISubjectEvidenceAccessor |
| MPCore.Security.AspNetCore | A person's token as evidence beside the service's identity. Calling side: AddMPCoreSubjectEvidence() on a client's builder carries the token of the user being served in x-subject-token, never as the call's credential, over HTTPS only. Called side: AddMPCoreSubjectEvidenceValidation(...) and UseMPCoreSubjectEvidence() admit only listed services, validate the token with the host's issuer rules, azp, typ, sid and auth_time age, remove the header before any application code, and expose SubjectEvidence. A command requires it with RequireSubjectEvidence("<issuer scheme>"); a refusal is 403 / PermissionDenied. ADR-014, addendum of 2026-10-08 |

| MPCore.Security.AspNetCore | Mutual TLS between services. AddMPCoreMutualTls(...): every TLS listener requires a client certificate whose chain ends at a configured authority (never the system store), valid now and for client authentication, with a listed workload name (URI or DNS subject alternative name), checked at the handshake; revocation as configured. RequireWorkloadCertificate() on an endpoint, with UseMPCoreMutualTls(), refuses a request without one with 401 / Unauthenticated. UseMPCoreCertificateForwarding() reads a certificate a proxy forwards in X-Client-Cert, only from TrustedProxies; for a request from a trusted proxy the client certificate is the forwarded one or none, never the certificate of the proxy's own connection. The certificate never becomes the current actor. ADR-017 |
| MPCore.Resilience.Http | AddMPCoreClientCertificate(...) on a client's builder: presents a certificate from PEM files or one the host loads, and trusts only the configured authorities for the called service's certificate |
| MPCore.Templates | Security:MutualTls in appsettings.json, off by default; the generated host calls AddMPCoreMutualTls when it is on, and has UseMPCoreCertificateForwarding() and UseMPCoreMutualTls() in its pipeline, which do nothing while it is off |

| MPCore.Application | A value that never prints. SensitiveValue: ToString(), interpolation, records, exception messages, System.Text.Json and the debugger all show ***; the value leaves only through Reveal(); equality is in constant time; a JSON value that is not a string is refused with a JsonException, so a malformed request is answered with 400. SensitiveMessageTypes: types whose objects MP Core's processors mask whole, a service of the host's container (one per host, never shared by hosts of a process); AddMPCoreSensitiveMessageTypes(params Type[]) registers it and adds types. ADR-018 |
| MPCore.Observability | The name processors also mask a dotted name with a sensitive segment and sensitive names inside a nested collection; a SensitiveValue and an object of a type in the host's SensitiveMessageTypes are masked whole (the processors take the registry from the container; their constructors without it mask no object by its type). The name list is unchanged. The package now references MPCore.Application |
| MPCore.Transport.Grpc | AddMPCoreSensitiveMessages("package.Service") on the gRPC builder: the request and response messages of a named service are masked whole in logs and traces, from host start: the types are read from the mapped endpoints when the host starts, before it listens, so a message logged from the application-started callback or by a handler is masked with no call needed first. A request object logged before that, by a hosted service that starts before the fill or by code between Build and Run, is printed whole. The host fails to start when a name is that of no mapped service |
| MPCore.Observability | A console sink after the redaction. MPCoreObservabilityOptions.EnableConsoleLogExporter (off by default) writes each log record to standard output, after the sensitive-value processor: the time, level, category, event id, formatted message and exception, no attributes or scopes. A host that clears the logging providers keeps its logs on the console. Additive |
| MPCore.Templates | The generated host prints through the pipeline only. Program.cs calls builder.Logging.ClearProviders() before AddMPCoreFoundation, and Observability:EnableConsoleLogExporter (true) keeps the logs on the console. The console, debug and event-source providers ASP.NET Core adds printed a protobuf request of a sensitive service whole, a one-time code included. Seen failing: a generated host is built, run and called, and its console shows Verify ***; before the change it showed the phone and the code. ADR-018, section 4 |

| MPCore.Security.Abstractions | CurrentActor.IssuedAt (iat); CurrentActor.IsAuthenticationFresh(maximumAge, now), never fresh without an authentication time; CurrentActor.AdditionalClaims, a read-only map of allowlisted claims, at most 16 of at most 256 characters; CurrentActorBuilder.IssuedAt and AdditionalClaims |
| MPCore.Security.AspNetCore | ActorClaimMappingOptions.AdditionalClaims: the allowlist of claims MP Core does not map, such as acr. A claim is exposed only when the token carries it once, with a bounded value; startup fails on more than 16 |

| MPCore.Resilience.Http | Token introspection (RFC 7662). AddMPCoreTokenIntrospection(...) and ITokenIntrospector.IntrospectAsync(token): posts the token to the introspection endpoint, named or read once from discovery, authenticated with the client's own credentials (RFC 7662, section 2.1), over HTTPS only. It fails closed: an error, a timeout, an answer that is not a JSON object, an active other than true, or an expired token are all inactive. The result carries Active, Subject, SessionId, ClientId and ExpiresAt, never the token. No cache by default; with MaximumCacheDuration, an active answer is kept no longer than that nor than the token's exp, and an inactive answer is never kept. No token or secret is logged |

| MPCore.Security.Abstractions | IResourceAuthorizer and ResourceDecision: the port a product implements to decide whether the current actor may act on a resource key |
| MPCore.Security.AspNetCore | Resource keys. AddMPCoreResourceKeys(...); RequireResourceKey("<key>") on a REST endpoint and `[Res...

Read more

MP Core 0.9.3

Choose a tag to compare

@panahister panahister released this 28 Sep 12:48

MP Core 0.9.3

A patch version: an addition only. It follows 0.9.2.

Asked for by running the Tiffin sample: an order
whose restaurant never answered stayed paid for ever (finding T-08). ADR-015 has the decision.

Moving from 0.9.2

Nothing to do. The change is an addition; a message published without a delay is delivered as before.

New

Package What it adds
MPCore.Messaging.Abstractions MessageDeliveryContext.DeliverAfter: the message is delivered no sooner than this delay after the work that publishes it commits
MPCore.Messaging.Wolverine The delay, on every transport: the message waits in the host's durable store and is sent when it is due. A negative delay is refused

Changed

What Now
The guide's list of what MP Core deliberately does not do no longer names a delay set by the publisher
MP Core's own integration tests run against RabbitMQ and Kafka too: both are in eng/compose.test.yaml and in the continuous integration. CONTRIBUTING.md names the two new variables

Verified

What Result
DelayedDeliveryTests, against PostgreSQL, RabbitMQ and Kafka 7 tests. Before the change, a message with a delay of three seconds arrived at once on each of the three
Where a delayed message waits an experiment over RabbitMQ: a Scheduled envelope in the host's PostgreSQL, and no message in the queue
MP Core's tests, 15 assemblies, against PostgreSQL, TimescaleDB, Redis, RabbitMQ and Kafka 485 passed, none skipped, on this machine and on GitHub
The API baseline of 0.9.2, for every package accepted
Both template shapes, generated from the packed template and built against the packed packages service and modular monolith: 0 warnings, 0 errors
The Storefront sample's tests, against the packed packages 262 passed
The Tiffin sample against this source, sixteen scenarios on empty databases 286 checks passed; S15: an order a restaurant never answered is cancelled and paid back

MP Core 0.9.2

Choose a tag to compare

@panahister panahister released this 28 Sep 09:32

MP Core 0.9.2

A patch version: additions only. It follows 0.9.1.

Asked for by running the Tiffin sample: a service
that calls another on behalf of a tenant worked for nobody there (finding T-05). ADR-014 has the decision,
in its addendum of 2026-09-28.

Moving from 0.9.1

Nothing to do. Every change is an addition; a host that configures nothing behaves as before, and no
header is believed.

New

Package What it adds
MPCore.Tenancy.Abstractions TenantHeader: the header x-tenant-id, and what a tenant name is
MPCore.Resilience.Http The tenant of a call. AddMPCoreTenantPropagation() on a client's builder writes the tenant of the work into x-tenant-id, on REST and on gRPC clients built by the factory. The package now references MPCore.Tenancy.Abstractions
MPCore.Security.AspNetCore TenantClaimOptions.TrustedServiceClients, and AddMPCoreTenancyFromClaim(Action<TenantClaimOptions>). The header is believed only from a service whose client id is listed, never from a user, never over a tenant in the token

Documented

Package What it now says
MPCore.Security.AspNetCore When a caller is a service: a client_id claim, and no user name or a service-account- one. The summary said something else. Keycloak 25 and later write client_id only for a client with the service_account scope

Verified

What Result
New tests 11 in TenantOverServiceCallsTests, 7 in TenantPropagationTests
Each guard broken on purpose the client list, the kind of caller, the order of token and header, the validation of the value, the writing of the header, the caller's own header: each made its tests fail
Tiffin against this source, fifteen scenarios on empty databases 266 checks passed. Payments' audit trail names the city of all 20 payments Ordering opened; with Ordering taken off Payments' list, the next one names none
MP Core's tests, 15 assemblies, against PostgreSQL, TimescaleDB and Redis 478 passed
The API baseline of 0.9.1, for every package accepted: packing compares each package with its 0.9.1
Both template shapes, generated from the packed template and built against the packed packages service and modular monolith: 0 warnings, 0 errors
The Storefront sample's tests, against the packed packages 262 passed

MP Core 0.9.1

Choose a tag to compare

@panahister panahister released this 28 Sep 06:16

MP Core 0.9.1

A patch version: two additions and nothing removed. It follows 0.9.0.

Both additions were asked for by running the Tiffin sample:
nine services, two tenants. ADR-014 has the decisions.

Moving from 0.9.0

Nothing to do. Every change is an addition; the API baseline of 0.9.0 accepts the packages.

Change What you will notice
UseMPCoreWolverine registers WolverineTenantMessagePublisher as IMessagePublisher A message carries the tenant of the work that published it, in the header x-tenant-id. A single-tenant host publishes as before
Every handler runs HandlerTenantMiddleware A handler that runs from a queue has the tenant of its message in ITenantContext and TenantScope.Current, and its audit records name it

New

Package What it adds
MPCore.Messaging.Wolverine The tenant of a message. WolverineTenantMessagePublisher writes it; HandlerTenantMiddleware opens it for the handler, the save included. A tenant the publisher names in MessageDeliveryContext wins over the ambient one
MPCore.Resilience.Http The identity of a service. AddMPCoreServiceIdentity puts a token on every request of a client: issued to the service itself (OAuth 2.0 client credentials), asked for once, kept until shortly before it expires, forgotten when it was refused, and never sent in cleartext. Works for REST and for gRPC clients

Documented

Package What it now says
MPCore.Persistence.Timescale AddCompression orders a hypertable's compressed chunks newest first (DESC), and the column it takes is a name alone. It always did; its documentation did not say so, and a sample took it for oldest first

Verified

What Result
MP Core's tests, 15 assemblies, against PostgreSQL, TimescaleDB and Redis 460 passed
Of them new 3 for the tenant of a message, 12 for the identity of a service
The tests of the tenant, before the change failed: the handler saw no tenant
The tests of the identity, with the code broken on purpose 4 of them failed, in two experiments
The API baseline of 0.9.0, for every package accepted: packing compares each package with its 0.9.0
The packages 30 packages and 28 symbol packages
Both template shapes, generated from the packed template and built against the packed packages service and modular monolith: 0 warnings, 0 errors
The Storefront sample's tests, against the packed packages 261 passed
The Storefront sample's scenarios, against this source, on empty databases 144 passed, 1 skipped (S21: the edge does not verify tokens in that run)
The Tiffin sample, against this source 213 tests and 266 checks of 15 scenarios passed

MP Core 0.9.0

Choose a tag to compare

@panahister panahister released this 27 Sep 17:09

MP Core 0.9.0

The first version published to nuget.org, and the first that is not a prerelease. It follows the internal
prerelease 0.2.0-alpha.9. Everything below was found, or asked for, by running a generated backend end
to end: the Storefront sample.

0.9.0 is a stable version whose major version is 0. The public API may still change in a minor
version; these notes will say what changed and how to move.

Moving from 0.2.0-alpha.9

Change What to do
EntityFrameworkAuditSink<TContext> no longer takes IServiceScopeFactory Nothing, unless you construct the sink yourself; AddMPCoreAudit registers it
ICurrentActorAccessor, ITenantContext, IAuditContext and the audit interceptor are singletons Nothing, unless you registered your own scoped implementation; register it as a singleton that reads ambient state
A broken business rule reaches the caller under the rule's own error domain and code, not mpcore.domain Clients that matched on the generic code match on the rule's code
MPCoreObservabilityOptions.ServiceNamespace defaults to default Set it, as the template always did
A modular monolith is generated with one project per module Nothing for an existing repository; nothing forces a migration
The packages carry the Apache-2.0 licence and are published on nuget.org Remove any internal feed from NuGet.Config

New

Package What it adds
MPCore.Validation.FluentValidation Input validators run as Wolverine middleware before the handler. An invalid message never reaches it, and the caller receives one violation per field, with a field path, a rule code and a message key.
MPCore.Localization A message catalog: failure message keys are rendered in the language the caller asked for, from resource files, with a culture chain and named placeholders. MP Core's own messages ship in English and Persian.
MPCore.Localization.EntityFrameworkCore.PostgreSql Translations an administrator edits at run time, stored in the product's own database and refreshed on every instance.
MPCore.Idempotency.EntityFrameworkCore.PostgreSql Request idempotency and the consumer inbox (ADR-013).
MPCore.Application IIdempotentExecutor and its ports; IFailureMessageLocalizer.
MPCore.Domain BusinessRule, with an error domain, a code and a message key, validated when the rule is created; Entity<TId>.CheckRule.
MPCore.Transport.Http, MPCore.Transport.Grpc Idempotency-Key and Idempotency-Replayed; RequireIdempotencyKey().
MPCore.Messaging.Abstractions PublishAsync(message, MessageDeliveryContext); the port IMessageInbox.
MPCore.Messaging.Wolverine UseMPCoreInbox(); WolverineFoundationOptions.ApplicationAssembly; every handler with no other actor runs as system:<Message>.
MPCore.Templates A design-time context factory for every backend, so that dotnet ef works whatever is attached to the context later; it reads the database from ConnectionStrings__PostgreSql. One project per module for a modular monolith; the message catalog and the validators registered for every host; guides that name the source of every convention.

Fixed

Where What was wrong
MPCore.Messaging.Wolverine A message could leave without the change that caused it. A command that was invoked, whose save failed and whose retry changed nothing, delivered the messages of the failed attempt. An attempt that does not run to its end is now cleared (HandlerAttemptMiddleware; ADR-011, addendum of 2026-09-27).
MPCore.Application, MPCore.Messaging.Wolverine In the same case the idempotency key was stored with the answer of the attempt that had failed.
MPCore.Messaging.Wolverine The rollback policy moved the handler ahead of every middleware registered after it, so validators ran after the handler.
MPCore.Messaging.Wolverine The host project's own handlers were never discovered.
MPCore.Messaging.Wolverine Work done by a queued handler was audited as anonymous.
MPCore.Messaging.Wolverine A queued message that breaks a business rule was retried; it is now dead-lettered after one attempt.
MPCore.Audit.EntityFrameworkCore.PostgreSql A host with business audit could not start in Development, and every handler that recorded a business action failed at its first message.
MPCore.Observability No Wolverine metric was exported; no MPCore.* meter was exported.
MPCore.Persistence.EntityFrameworkCore.PostgreSql, MPCore.Audit.EntityFrameworkCore.PostgreSql, MPCore.Messaging.Wolverine Microsoft.EntityFrameworkCore.Relational resolved to an older patch than EF Core.
MPCore.Templates A generated project could not build on Apple Silicon without Rosetta.
MPCore.Templates A generated host had no health check. grpc.health.v1.Health answered UNKNOWN, which a gRPC probe reads as not serving, and /health/ready answered Healthy without asking anything. A host now has a check for the process and one for its database (Hosting/HostHealthChecks.cs).

Decisions recorded

Record Subject
ADR-004, addendum Public source, public packages, continuous integration without automated publication
ADR-007, addendum The current-actor accessor is a singleton that reads ambient state
ADR-011, addenda What a generated host taught the code-generation constraints; an attempt that failed takes its messages with it
ADR-012 One project per module; business rules, input validation and messages; how one module makes another change
ADR-013 Request idempotency and the consumer inbox

ADR-012, ADR-013 and the addenda are recorded as proposed and await acceptance.

Known limits

  • A process that stops between a commit and the delivery of its messages relies on Wolverine's recovery of
    durable envelopes. MP Core has no test of it.
  • Nothing stops a message from printing a secret into the log when its handling fails: Wolverine prints a
    failed message, and a record prints every property. Override ToString on a message that carries one,
    and keep secrets out of messages between modules.
  • A command that ends in the error queue answers nothing, so a process that waits for it waits for ever
    unless the product answers when the message is given up. The sample shows how.
  • IMessagePublisher cannot set a partition key or a delay.
  • The name of an integration event (IntegrationEvent.EventName) is not the name the broker carries. Two
    services that share no assembly agree on one topic or queue per contract, and the consumer names the
    type it reads with Wolverine's DefaultIncomingMessage<T>(). The sample shows how.
  • MPCore.Observability.Prometheus depends on a prerelease of the OpenTelemetry Prometheus exporter.

Verification

Check Result
All tests, including integration against PostgreSQL, Redis and TimescaleDB 444 passed, 0 failed, 0 skipped, in 15 test assemblies
Release build, warnings as errors 0 warnings, 0 errors
Restore in locked mode passed
Packed as 0.9.0 30 packages, 28 symbol packages
eng/verify-release-artifacts.sh 0.9.0 on the packed files verified, 89 checks
A backend generated from the packed template in three combinations of options, built against the packed packages 0 warnings, 0 errors
The Storefront sample, built and tested against the packed packages 254 tests passed; every scenario passed against the running backends

Measured on 2026-09-27, on macOS (Apple Silicon), from the working tree that became the first commit of
this repository. The packages that are published are packed again from that commit by a maintainer; these
numbers are the evidence that the tree was fit to be packed, not a description of the published files.