Release Notes
On this page
- Memoria 2.0.0-beta.2
- Memoria 2.0.0-beta
- Memoria 1.9.1
- Memoria 1.9.0
- Memoria 1.8.0
- Memoria 1.7.0
- Memoria 1.6.0
- Memoria 1.5.0
- Memoria 1.4.1
- Memoria 1.4.0
- Memoria 1.3.2
- Memoria 1.3.1
- Memoria 1.3.0
- Memoria 1.2.1
- Memoria 1.2.0
- Memoria 1.1.0
- Memoria 1.0.0
- OpenCQRS 7.3.0
- OpenCQRS 7.2.0
- OpenCQRS 7.1.5
- OpenCQRS 7.1.4
- OpenCQRS 7.1.3
- OpenCQRS 7.1.2
- OpenCQRS 7.1.1
- OpenCQRS 7.1.0
- OpenCQRS 7.0.0
- OpenCQRS 7.0.0-rc.1
- OpenCQRS 7.0.0-beta.6
- OpenCQRS 7.0.0-beta.5
- OpenCQRS 7.0.0-beta.4
- OpenCQRS 7.0.0-beta.3
- OpenCQRS 7.0.0-beta.2
- OpenCQRS 7.0.0-beta.1
- Related
Memoria 2.0.0-beta.2
Released 21/09/2026
- The framework goes back to the Apache License 2.0, and stays there. Four days ago 2.0.0-beta offered every package under either the Reciprocal Public License 1.5 or a commercial licence. That was the wrong call and this release withdraws it. The RPL’s reciprocity is triggered by deploying rather than only by distributing, which means an engineer evaluating Memoria at any company of a reasonable size has to go to their legal department before they can prototype — a tax on the one thing a young framework needs most, charged to buy revenue that adoption has to come first to produce anyway. Every package published under the
MemoriaNuGet prefix is Apache 2.0 from this version on, as it was throughout 1.x: use it in anything, commercial or not, closed source or open, at any scale, with no edition, no threshold, no key and nothing to sign. The packages carry an SPDXApache-2.0expression in place of the packedLICENSE.mdand no longer ask for the licence to be accepted on install. Nobody needs to do anything: a version is licensed under the terms it shipped with, so anyone who took 2.0.0-beta under the RPL or the commercial licence keeps that grant, and Apache 2.0 gives strictly more than either did. See Licence - Memoria Web becomes a commercial product, and is the only part of the repository that is not open source. The tool is what the framework’s users pay for, if they pay for anything: it is opened by a team already running event-sourced systems in production, to answer questions — what does this aggregate fold to, is its snapshot behind, which events produced this state — that otherwise cost an afternoon of hand-written queries each. Its source stays in
src/Memoria.Weband stays published, so it can be read, audited, built and modified for your own use; running it is what the Memoria Web Licence governs. It is metered by services — one entry in theserviceslist of an installedmemoria.jsonmanifest, being a named set of domain assemblies read over one connection string — because that is the one thing the tool already knows about itself and can count without anybody attesting to anything. One service is free, permanently, with no limit on how many people sign in, how many instances run, or how many environments they run in; that covers every evaluation, every side project and most single-context shops. Above it, Standard, Professional and Enterprise read up to 5, up to 25 and unlimited services, at $999, $2,499 and $4,999 USD a year or a tenth of that a month, Enterprise covering affiliates too. Any paid edition bought on or before 31 December 2026 is half price for as long as the subscription runs unbroken. There is still no licence key and no activation. See Pricing - Everything the old arrangement rested on is gone, not reworded. No revenue threshold, no non-profit budget test, no outside-capital ceiling, no affiliate revenue aggregation, no government-agency exclusion, no 90-day grace period and no clause asking a licensee to confirm in writing that it qualifies. A service count replaced all of it, which is why the tool can settle the question by itself and the licence page no longer asks you to assess your own size
- Contributing to the framework no longer needs a signed agreement. The contributor licence agreement existed only so that contributions could be offered under two licences at once. Apache 2.0 section 5 already says a contribution arrives under the terms the project goes out under, so framework pull requests need nothing signed — send them. The CLA now covers
src/Memoria.Webalone, where it is still needed, and its wording narrows to match. See Contributing - No code changes. Every package is republished at 2.0.0-beta.2 so the set keeps one version, and the only difference in any of them is the licence metadata
Memoria 2.0.0-beta
Released 17/09/2026
- Memoria is dual-licensed from this version on. Every 2.x version — alpha, beta and release alike — is offered under either the Reciprocal Public License 1.5, an OSI-approved open-source licence under which the software you build with Memoria must release its source in turn, or the Memoria Commercial Licence, under which it need not. The Commercial Licence comes in four editions. Community is free of charge, and always will be, for companies and individuals with less than $5,000,000 USD in annual gross revenue and for registered non-profits with less than $5,000,000 USD in annual total budget; government or quasi-government agencies do not qualify, and neither does an organisation that has ever taken more than $10,000,000 USD in outside capital such as private equity or venture capital. Standard, Professional and Enterprise are subscriptions, scoped by the size of the organisation — up to 10 developers, up to 50, and unlimited across every affiliate — at $299, $999 and $2,999 USD a year or $29.90, $99.90 and $299.90 USD a month, and half price, kept on renewal, for anyone who subscribes while 2.0.0 is in beta. Versions 1.x stay under the Apache License 2.0 they were released with: a version is licensed under the terms it shipped with, and nothing about any 1.x package changes. The NuGet packages now carry
LICENSE.mdin place of the Apache expression and ask for the licence to be accepted on install. See Licence - Every dependency moves to its current release. The test projects swap FluentAssertions 7.2.0 for AwesomeAssertions 9.6.0, the community fork that stays on Apache 2.0, since FluentAssertions 8 went commercial; the assertions read the same and only the namespace changes. Two of the moves are major versions of a client library.
Memoria.Messaging.RabbitMqgoes to RabbitMQ.Client 7, whose API is asynchronous throughout, so the provider now opens its connection on the first send rather than in its constructor — a connection string that cannot be reached surfaces as that send’sFailure, the way any other error on a send does, instead of throwing where the provider is resolved — and a customIConnectionhanded to the provider is used as before.Memoria.Caching.Redisgoes to StackExchange.Redis 3, which speaks RESP3 to a server that offers it and RESP2 to one that does not; nothing in the provider’s use of the client changes. Everything else is a patch or minor step: Entity Framework Core, the Cosmos SDK, Npgsql, Azure Service Bus and the Microsoft.Extensions packages - A store can carry type bindings of its own, so one process can read several bounded contexts’ stores. Every store resolves the keys it holds —
OrderPlaced:1,Order:1— into CLR types through a set of type bindings, and until now that set was one per process: the static maps onTypeBindingsandDcbTypeBindings. A host reading two contexts that both declare anOrderPlacedat version 1 could not bind both. The maps now live on aTypeBindingSet, of which the process-wideTypeBindingSet.Defaultis one instance; the statics are views over it, so an application that never mentions the type is bound exactly as before. A store given a set of its own reads through that set instead:DomainDbContextandDcbDbContextgain aTypeBindingsproperty, set when the context is constructed, that every read through the context and every extension method on it resolves through;CosmosDataStoreand the in-memory variant take the set as a constructor argument, and the domain service over either reads through it. The entity and document conversions —ToDomainEvent,ToAggregate<T>,ToProjection<T>— and the event-filter helpers gain an overload taking the set, keeping the parameterless one over the default.IDomainDbContext,IDcbDbContextandICosmosDataStoregain the member with a default implementation, so an implementation written before it still compiles. What is written never changes: a key comes from the type’s own attribute under any set. Binary compatibility breaks for the extension methods that gained a parameter; source compatibility holds. See Reading more than one bounded context in one process - A zip uploaded to Memoria Web must carry a manifest, and the Installed table opens a sheet over each archive. A file called
memoria.jsonat the root of the archive declares the services the zip brings — for each, a name shown as written, the assembly files its domain types are read from, the name of the connection string it is read over, and, optionally, the claim values that may read and update it and a sentence saying what it is, shown on its sheet. The address a service is browsed under is made from its name — letters and digits kept, everything else dropped, each run of spaces one dash, lower case, soSamples Streamedlives at/samples-streamed— and that address is unique across every installed archive. An upload without a manifest, or with one that names an assembly the zip does not carry, a name with no letter or digit in it, two names that make one address, or no connection string, is refused with the rule it broke and installs nothing. Only the assemblies a service names are scanned for domain types; every other assembly in the zip is loaded as a dependency and registers nothing, whatever it carries — so a dependency that happens to declare an event no longer registers one. The Installed table lists each service a zip declares on a line of its own, marked as a place to go, and each opens a sheet over the table with two views: Info — the address it is browsed under, its connection string and what the configuration says of it, configured and which engine opens it or not configured, and who may read and update it — and Types, its assemblies with the types registered from each; the Types mark that used to open the assemblies alone is gone, since the sheet holds them. A zip already in the extensions directory without a manifest stays listed, marked No manifest, registers nothing, and its row says why. The two sample projects carry a manifest each, copied beside the assembly so the sample zips are built by zipping the output. Services are the first step of reading more than one store in one instance; the home page, the addresses and the per-service roles follow. See What to put in a zip - Memoria Web’s home page lists the services installed, and each is browsed under its own name. Every page that reads a store now sits under the service’s name —
/orders/streamed/events,/orders/dcb/aggregates/data— and shows the types that service’s assemblies registered and no other’s; the four update posts move under the name with them. A service’s own page, at/orders, lays out its models the way the home page used to lay out the one store’s: side by side when it registered types under both, one alone otherwise. Inside a service the bar carries Home, the service’s name leading to its page, the menus over its models, and Settings; outside one, Home and Settings. A name no manifest declares is not found, and so are the old addresses under no service —/streamed/eventsanswers 404 now. A service’s name cannot make an address the tool already answers on (settings,preferences,about,forbidden,signed-out,login,logout,error,not-found), and a manifest that tries is refused. Behind it, the catalogue knows which service each type belongs to by the file its assembly came from, so the sample zips — one service each — appear as two services on Home rather than two halves of one page. See Memoria Web - Each service in Memoria Web reads the store its manifest names, so one instance holds several. A service’s manifest names a connection string, and the configuration holds one string per store under
ConnectionStrings:{name}: two services over two databases, or two over one, in one instance. Which engine opens each string is read off it as before, or settled under its own name byDatabases:{name}:Provider; a Cosmos string’s database and container names sit underDatabases:{name}:Cosmos. The one string the tool always read,Memoria, still reads the olderDatabase:ProviderandDatabase:Cosmos:*, so a configuration from before services works unchanged — but it is no longer required: the tool starts with no connection string at all, and a service naming one the configuration lacks is listed on the home page as unreachable, its pages saying so rather than failing, until the string is there. A configured string that cannot be read still refuses start-up, under whatever name. Start-up logs one line per service — which string it reads and with which engine, or a warning that the string is not configured — in place of the one Store opened with line, and the per-store warm-up runs for each relational store. Behind it, each service is read through aTypeBindingSetof its own, built from its assemblies alone, so two services declaring one event name at one version —ProductCreated:1in each — each list their own rows under it, which the process-wide maps could not do; the tool no longer writes those maps at all. See The connection strings - A Memoria Web service is read by the operators its manifest names, and silence is nobody. The claim values a manifest lists under
roles.readandroles.updatenow grant Reader and Updater for that service alone, read off the same claim the configuration’s mappings are, soorders-teamin a manifest means whatmemoria-adminsmeans in the settings; update includes read. The configuration gainsAuthorization:Roles:Readerbeside the other two, and a value mapped there reads every service, as an Updater updates every service and an Administrator does everything. What changes for an existing deployment: a signed-in operator whose claims match no mapping is no longer a Reader of everything. They see no service — the home page says that nothing their sign-in carries names one, and who to ask — and an address they type under a service sends them to the page that says which service and which role, as the update posts send a service’s reader who presses Update. Map the values that used to read everything underAuthorization:Roles:Reader, or name them in each service’s manifest, before upgrading. Running open, the manifests’ roles are as unread as the configuration’s, and everything answers. The start-up line naming the mappings names the Reader one too. See Roles - The major version steps to 2 for the licence change; the RabbitMQ provider’s connection timing above is the one behaviour that moves with it. Every package is republished at 2.0.0-beta so the set keeps one version
Memoria 1.9.1
Released 13/09/2026
- Compare any two versions of a model, on every detail page of Memoria Web. Each aggregate and projection detail page — streamed and dynamic consistency boundary alike — gains a Compare tab and a Compare column on its events tab. It works in versions rather than sequences: a model’s versions count its own applied events from one and climb by one down its history, where a sequence or a position counts the whole stream or log and, on a stream several models share, skips the other models’ events. The tab folds the model in memory at each of the two versions, through the store’s own up-to-sequence and up-to-position reads, and lays the two states over each other as one table — the State tab’s rows with a value from each version and a mark on every row where the two differ, changed, added or removed, nested rows kept aligned by path. Above the table, two cards say what each version is: the version, the sequence or position it was folded up to, the event that produced it and when it was appended, as the store holds it. A form takes any two versions, bounded by the last; two links step the pair one version up or down; and each row’s Compare link opens the version that event produced against the one before it, so a history is walked one event at a time at a click per row. Nothing is written — the stored snapshot stays where it was — and a compared version the snapshot has not reached says so beside its number, with a link to the Update tab that would fold it in. Like every other view in the tool it opens by address, so a comparison can be linked to and gone back from
- Every event row says whether the stored snapshot has applied it. A tick or a clock at the start of each row on the four events tabs, under no heading, decided from the snapshot’s own record of the latest sequence or position it folded rather than by counting: an event at or below that place is in the snapshot, one above it is not yet, and with no snapshot stored none is
- The Info tab warns when a snapshot has fallen behind its history, beside the version it is about: how many events the model is folded from that the stored row has not applied, and a link to the Update tab that would fold them in. Under a clock rather than a warning triangle, because nothing failed — a history that has moved on since a row was written is the ordinary working state of an event-sourced store — and the same clock marks a compared version the snapshot has not reached. A count that failed says nothing rather than nothing found, and a stored version above the count says nothing either, since a model’s
Applymay decline an event its type filter let through - A View Type pop-up on every detail page. A button across from the heading opens the declared type over the row being read — the same four views the types pages show, info, state, events and identifiers, drawn by the same component — so a reader matching a stored row against what its type declares does not leave the row to do it. It opens by address, so it can be linked to and the browser’s back button closes it
- Retired types read as obsolete wherever a type is named. A type carrying
[Obsolete]gets a mark after its name in every list of types, and a line under its heading wherever it is opened, in the attribute’s own words when it carries any. Retired is not the same as old: a type with a later version beside it is old and the version says so; a retired type is one nothing should write through any more, and the attribute is the only place the domain says that. The sample domain gains a retired feature in each half — a closed loyalty scheme on the customer stream, dropped supplier ratings on the supplier boundary — so the marks have something to mark - The installed archives table on the Settings page gains a Types column: a mark per row opens what that zip brought, each assembly file in it and the domain types registered from each, with a file that registered nothing saying so on a line of its own, which is the case worth noticing — a dependency the domain needs, or an assembly that did not load
- Text typed into an event table’s filter is marked wherever the table matched it, in payload names and values alike, and the pager under every table draws its pages in groups of five rather than ten
Memoria.EventSourcing.Dcb.Store.EntityFrameworkCoregainsGetEventHeaders, reading the position, type binding key and append date of every event in a boundary, in position order, without its payload — what a reader needs to know the shape of a history rather than what it carries, and what the tool’s compare tab reads, since the payloads are what make a boundary heavy. It is public for the reasonGetEventEntitiesis. No other package changes in this release; every package is republished at 1.9.1 so the set keeps one version
Memoria 1.9.0
Released 12/09/2026
- Memoria Web, a browser tool for reading a store. A store is a log of serialised payloads under type bindings, and until now the only way to see what was in one was to query it by hand and deserialise the results yourself — with no way at all to ask the question that matters, what does this aggregate fold to, and is its snapshot behind?
src/Memoria.Webanswers it. Point it at a database, upload a zip of your own domain assemblies on its Settings page, and it lists the events appended, the aggregates and projections snapshotted from them, and the types both were written through — both consistency models side by side, each with the types it declares and the rows the store holds. It has no reference to any domain: the assemblies are scanned forIStreamId,IAggregateRoot,IProjection, their identifiers andIEvent— and the DCB equivalents — and registered in place, so an upload, a removal or a refresh changes what is resolved without a restart. It creates nothing and deletes nothing; the one write it offers is refreshing a snapshot that has fallen behind its stream or its boundary. It is in the repository rather than on NuGet, so it is built and run like any other application here. It ships with no authentication or authorization, and an uploaded assembly is loaded into the process and run — keep it on localhost or behind a proxy that authenticates every request, form posts included. Both are coming in the next release, and until then a proxy is the only thing that can stand in for them. See Memoria Web, Configuration and Deployment - Every aggregate and projection detail page carries a Json tab beside State, showing the stored payload as the store holds it: laid out one value per line, coloured by kind of token, capped in height so a large model scrolls inside its box, with a Copy button that puts the laid-out text on the clipboard. It is the row’s own text rather than the model serialised again, so a payload the model cannot read back — or one carrying more than the model declares — is still there to see, and one that is not JSON at all is shown as it is under a note saying why. Every table of events offers the same card per row, from a Json column beside the payload that opens it in a pop-up — the two event data pages and the events tab of any model. Like every other view in the tool it opens by address, so it needs no script, can be linked to, and the browser’s back button closes it. See Memoria Web
- PostgreSQL, SQL Server and SQLite are read through Entity Framework Core and carry both consistency models. Cosmos DB is read through the Cosmos SDK, the way the store writes it, and carries the streamed model only: there is no Cosmos dynamic consistency boundary store, so those pages are left out of the menu and answer 404, rather than being linked and empty. Which engine the store is in is read off the connection string by a keyword only one provider takes; a string carrying signals for two of them, or for none, asks for
Database:Providerrather than being guessed at, because a guess is a connection failure minutes later against a store that is perfectly reachable - New sample seeder,
Memoria.Web.Samples. Two jobs in one project on purpose: it carries a sample ecommerce domain modelled in both consistency models, and it fills a store with data written through that domain using the framework itself — so nothing reaches the tool that was never written through Memoria first. Orders are driven through the aggregate’s own methods and every DCB append follows the read-decide-append cycle on condition that the boundary has not moved, so the log holds only sequences the domain would have allowed. Snapshots are deliberately left up to date, behind, and absent, because a store where everything is current has nothing to demonstrate; the run prints where each one stands, so you know which rows have an update waiting for them in the tool. It creates what is missing before it writes, and shares the tool’s own connection-string resolution, so a string that reaches one reaches the other. See Try it with sample data - The Entity Framework Core event table is renamed from
eventstoDomainEvents. It was the one store table that did not say whose it was: the other two have always beenDomainAggregatesandDomainProjections, and the dynamic consistency boundary store’s four areDcb*. ADbContextderiving fromDomainDbContextis meant to carry tables of your own beside the store’s, and these databases are often shared with schema somebody else manages —eventsis a name an application, an outbox, an audit trail or an analytics pipeline is quite likely to want, and Memoria took it. Breaking for existing databases: the store reads and writesDomainEventsfrom 1.9.0, so a database created by an earlier version must be renamed before an upgraded application runs against it, or the first read fails with a missing-object error from the engine. Nothing else changes — no column, index, entity, API or stored payload — and aDbContextthat mapsEventEntityto a name of its own keeps it. See Upgrade to 1.9.0 - New install scripts and rename scripts for 1.9.0.
scripts/install/1.9.0-install-{sqlserver,postgresql}.sqlcreate the three tables under the new name;scripts/migrations/1.9.0-rename-events-{sqlserver,postgresql}.sqlmove an existing database across. The rename is metadata-only — no rows copied, no index rebuilt — so it costs the same on ten events as on ten million, and it is idempotent. A database holding both tables is refused rather than treated as a no-op: that is what running the install script before the rename produces, and quietly doing nothing would strand every existing event in a table the store no longer reads. Container tests stand up a real 1.7.0 database, apply the rename, and compare every column and index against one built from the model, then assert that an event written before the rename is read back by the store afterwards - The three indexes keep their names —
IX_Events_EventType,IX_Events_StreamId_CreatedDate,IX_Events_StreamId_Sequence— because they are named for the entity rather than the table and follow it across the rename. The primary key is named after the table, soPK_eventsbecomesPK_DomainEvents; the scripts rename it, as does an EFRenameTable
Memoria 1.8.0
Released 04/09/2026
- Cosmos DB detects a document identifier collision instead of writing through it. Events, aggregates and projections share one container and one partition key, but their document ids are built from different things —
{streamId}:{sequence},{aggregateId}:{typeVersion}and{projectionId}:{typeVersion}— so an aggregate named after its own stream put its version 1 snapshot on the id of the event at sequence 1. Reading it threwArgumentNullExceptionfrom inside a dictionary lookup, and saving it upserted the snapshot over the event, destroying it. One aggregate per stream under a shared identifier is a common convention, so this was reachable by accident; the existing tests missed it only because their stream and aggregate ids happen to render different prefixes. Reads and saves now returnmemoria/document-id-collisionnaming both document types, and the event survives. Not retryable — it is a modelling mistake, and the fix is to change the identifier. See Cosmos DB - Saving a Cosmos aggregate now always reads the existing document first, rather than only when the in-memory version says the aggregate already exists. That inference came from the model, not the store, so a first save wrote without ever looking at what it was overwriting — and it also stamped a fresh
CreatedDateover an existing document whenever the two disagreed. Costs one point read on the first save of each aggregate - Projections are now treated like aggregates across every store. Two gaps closed, both of them things a projection silently did without while its aggregate counterpart did not. A fold into a projection now emits a new
Projection Foldedactivity event —AggregateDiagnostics.AddAggregateFoldedEventwas called from four aggregate paths and none for projections, so the question that event exists to answer, this model is in a state nobody expects, which events produced it?, could not be answered for a read model at all. AndUpdateProjectionis now public onIDomainServiceand the Entity Framework Core extensions, matchingUpdateAggregate; the refresh itself already existed and backedReadMode.SnapshotWithNewEvents, but was reachable only through it. A read model differs from a write model in one thing — it never produces events — soAdd,UncommittedEventsand appending are the only things it should lack. See Observability Projection Foldedis a separate event name rather than a reuse ofAggregate Folded. The tag shapes match apart fromprojectionIdreplacingaggregateId, so a query across both models is a two-name filter — but reporting a projection fold as an aggregate fold would make that name wrong about half the time, which is the same reasoning that keepsConcurrency Conflictapart fromConcurrency ExceptionIProjectionIdgainsEventPropertyFilter, the memberIAggregateIdhas had since 1.2.0. It exists because a stream shared by several models does not say which events belong to which, and a read model is no less likely to share a stream than a write model — until now a projection simply had no way to say. The filter is honoured everywhere a projection is folded: the cold build, the refresh underSnapshotWithNewEvents, and all threeGetInMemoryProjectionoverloads, on every store. Breaking: every projection identifier needs the new member, and returningnullpreserves the previous behaviour exactly. See Upgrade to 1.8.0- Breaking for anyone implementing
IDomainServicedirectly. The interface gainsUpdateProjection. Every store Memoria ships implements it; a custom implementation needs the one new member. Deriving from a shipped store or using the extension methods needs no change - Dynamic consistency boundaries, in two new packages.
Memoria.EventSourcing.DcbandMemoria.EventSourcing.Dcb.Store.EntityFrameworkCoreadd a second consistency model alongside streams. A stream fixes a decision’s boundary at design time; under DCB the boundary is aTagQuerychosen per decision and evaluated at the moment of the append. Events carry tags, and a boundary selects the events carrying any of those tags — or, where the question is about a combination rather than either thing, all of them. One event can therefore fill a seat when read throughcourse:c1and use up one of a student’s ten when read throughstudent:s7. This makes expressible the case no stream holds: a rule spanning a course’s capacity and a student’s course count, where a stream per course cannot see the student’s other subscriptions, a stream per student cannot see how full the course is, and one stream for the school serialises every subscription in it. Nothing depends on these packages: installing the streamed store pulls in no DCB, and vice versa. See Dynamic consistency boundaries and Streams or DCB? - An append is refused when its boundary moved.
AppendConditionpairs the boundary with the position the decision read it at. The check has two halves: readingMAX(Position)inside the boundary catches everything two sequential appends can produce, and aDcbTagHeadsrow per tag — carrying a concurrency token replaced by every append that writes under that tag or names it in a condition — catches the interleaving the first half cannot see. Two appends contend exactly when their boundaries overlap; disjoint boundaries never do. Refusals arememoria/concurrency-conflict, the same failure type a stream conflict produces, so an existing retry policy works unchanged against both models, and carrylatestPositionso a retry needs no extra read StreamIdandLatestEventSequencemove offIEventSourcedModelonto a newIStreamedModel/StreamedModellayer, so a DCB model reuses the fold without inheriting identity that means nothing to it.LatestEventSequenceis anintcounting within one stream; a DCB position counts the whole log, so DCB models carrylong LatestPositionon their ownIDcbModel/DcbModelbase. Breaking only for code that namesIEventSourcedModeland reads either through it — everything typed againstAggregateRoot,Projection,IAggregateRootorIProjectionis unaffected, and the compiler catches every occurrence. See Upgrade to 1.8.0- Registration is now additive.
AddMemoriaEventSourcingassigned the type-binding dictionaries outright, so registering both consistency models in either order silently discarded one of them — every affected read would later fail to deserialise with nothing pointing at the cause. Both registrations now merge: rebinding a key to the same type is agreement, rebinding it to a different type throws and names both. Events share one map because an event is the same event whichever model appends it; aggregates and projections do not, so a streamedOrderand a DCBOrdercan coexist without either being renamed - Snapshots are keyed by the boundary that produced them. The same aggregate id folded over a wider boundary is a different state, so a snapshot read back under a different query misses rather than returning the wrong fold. All four
ReadModevalues behave as they do for streams - The tag columns are case-sensitive by construction. Tags compare ordinally in .NET, so
seat:A1andseat:a1are two tags. SQL Server’s usual default collation would make them one row and quietly widen every boundary naming them, so the store pinsSQL_Latin1_General_CP1_CS_ASthere and"C"on PostgreSQL. Container tests assert the collation on both engines and, separately, the read and write behaviour it protects - No Cosmos DB provider, for a structural reason. A DCB append needs a tag query and its writes to be atomic together; Cosmos DB gives atomicity only inside one logical partition, and a tag query is not partition-scoped. Making it correct would force the whole log into one partition and cap the store at that partition’s limits. See Providers
- No DCB Npgsql package either. The streamed store’s Npgsql sibling exists solely to make
eventPropertyFilterwork againstjsonb; DCB has no property filter, because tags do that job. Thejsonbcolumn mapping is worth having and needs no package — it is four lines in your ownOnModelCreating, documented and verified by a container test. See Use PostgreSQL withjsonb - A boundary is a union or an intersection.
TagQuery.AnyOfselects the events carrying any of its tags;TagQuery.AllOfselects only those carrying all of them. The subscription rule needs the union — a boundary of only the events concerning both a course and a student cannot see how full the course is — but is alice already on maths? is answered by a single event, and reading it through the union means folding every seat in the course and every course alice has taken to find it. The intersection reads that one event, and keeps doing so as the school grows; it also removes thewhen subscribed.CourseId == CourseIdguards a model over a union needs to sort out what it folded. Two caveats: an intersection narrows what a decision reads, not what it may condition on — an append is conditioned on the boundary it folded, or a wider one — and an intersection is only as good as the tagging, since an event appended under one of the two tags is not inside it. A union is oneEXISTSover anIN; an intersection is oneEXISTSper tag, each seeking the(Tag, Position)primary key, so neither needs an index of its own. A query mixing the two is not in this release. See Dynamic consistency boundaries Positionis monotonic but not gap-free, because it is an identity column and concurrent transactions commit out of order. That is safe for the append condition, which is evaluated inside the transaction holding the relevant tag heads, and unsafe for a catch-up subscription — so this store deliberately ships none. See Entity Framework Core (DCB)- New install scripts for the DCB schema.
scripts/install/1.8.0-install-dcb-{sqlserver,postgresql}.sqlcreate the four tables the store needs, idempotently, and are held to the same comparison the streamed scripts are: build one database from the script, another from the model, compare every column, index and collation, and fail on any divergence - New example.
examples/Memoria.Examples.EventSourcing.Dcb.EntityFrameworkCoreruns the course-subscription scenario end to end, including two decisions that do not contend, one refused for resting on stale facts, and the same two tags read as a union and as an intersection so the difference in what each folds is visible
Memoria 1.7.0
Released 31/08/2026
- The link between aggregates and events is gone, and with it
GetEventsAppliedToAggregate. Both stores recorded one row or document per(aggregate, event)pair on every snapshot write — the Entity Framework CoreDomainAggregateEventstable and the Cosmos DBAggregateEventdocument. Exactly one thing read them, and it could not be kept correct: an aggregate’s store id embeds its[AggregateType]version, so bumping that version leaves older snapshots under their old identity, whileEventTypeFilterlives on the single CLR type and only ever reports today’s filter. Any answer computed from the stream would use the current filter against a snapshot built with a different one, and be wrong with no error. The link was correct because it recorded actual event ids at the time they were applied, and that cannot be reconstructed afterwards — so the method is removed rather than reimplemented as an approximation.IDomainService.GetEventsAppliedToAggregateand the store-specificGetEventEntitiesAppliedToAggregate,GetAggregateEventEntitiesandGetAggregateEventDocumentsall go. See Upgrade to 1.7.0 - Snapshot writes now record what they folded on the current
Activity. The link existed so a production issue in an aggregate’s state could be explained faster. That question is better answered where tracing already lives, so every store now emits anAggregate Foldedactivity event carryingappliedFromSequence,appliedToSequence,appliedCount,versionBeforeandversionAfter.appliedCountcounts events the fold consumed;versionAfter - versionBeforecounts those that actually changed the aggregate, and the gap between them shows events that matched the type filter and were ignored — something the link could never express. Every tag is a bounded scalar, so a fold over a thousand events costs what a fold over two does, and nothing is allocated when nothing is listening. Coverage was uneven before: Cosmos DB emitted transport-level events on snapshot writes, the Entity Framework Core store emitted none, and the in-memory Cosmos store emitted none. All three now emit the same record. Every activity event the stores emit is documented for the first time in Observability - Writes are cheaper in both stores. A save or a cold aggregate build wrote one extra row or document per event; it no longer does. In Cosmos DB that halved what one
SaveAggregatecould carry, because each event cost two of the 100 operations a transactional batch allows —MaxUncommittedEventsPerAggregateSaverises from 49 to 99. The snapshot write also stops splitting itself across batches: it is one upsert of one document whatever the stream length, so the batching machinery and its partial-write recovery argument are gone - The SQL Server 900-byte key limit is gone.
PK_DomainAggregateEventsspannednvarchar(255)plusnvarchar(450)— 1410 bytes of maximum potential key against SQL Server’s 900-byte limit for a clustered index key — so a long stream id combined with a long aggregate id failed at insert time, and the documentation carried the constraint. Dropping the table removes the constraint from the product. PostgreSQL was never affected - Breaking for
IDomainDbContextimplementors and direct extension callers. TheAggregateEventsDbSetis gone from the interface,DomainDbContextandIdentityDomainDbContext;TrackAggregatereturns a 2-tuple instead of a 3-tuple; andTrackEventEntitiesreturns the aggregate entity alone instead of a tuple. On the Cosmos DB store,ICosmosDataStorelosesGetAggregateEventDocumentsand theGetEventDocuments(streamId, eventIds)overload. See Upgrade to 1.7.0 - New install scripts, drop scripts and a Cosmos DB indexing policy for 1.7.0.
scripts/install/1.7.0-install-{sqlserver,postgresql}.sqlcreate the three tables the store now needs.scripts/migrations/1.7.0-drop-aggregate-events-{sqlserver,postgresql}.sqlremove the retired table from an existing database — idempotent, and covered by container tests that stand up a real 1.5.0 schema and rehearse the upgrade.scripts/install/1.7.0-cosmos-indexing-policy.jsondrops/aggregateIdand/appliedDate, which now index nothing; the 1.6.0 policy stays, because it is still correct for 1.5.0 and 1.6.0. See Upgrade to 1.7.0
Memoria 1.6.0
Released 30/08/2026
- Cheaper
GetEventsAppliedToAggregateon the Cosmos DB store. It fetched the events an aggregate was built from by matching their string ids, which cost more than the ordered read the query already performs. Because every event id this store writes is{streamId}:{sequence}, the sequences are recoverable from the ids with no extra read, and matching on the numeric sequence measured 7.68 RU against 10.60 for 150 events — about 16% off the whole operation. An id the store did not write falls back to matching on the id, so documents placed in the container by other means are still found. No API change - Oversized Cosmos DB writes are now reported as such, and long streams can be snapshotted. The Cosmos DB provider writes several documents per event into a transactional batch, which Cosmos caps at 100 operations — so
SaveEventsfailed above 100 events,SaveAggregateabove 49, and building an aggregate over a stream of 100 events or more failed outright. Every one surfaced asmemoria/storage-failure, indistinguishable from the database being unreachable. The two appending paths now refuse oversized input up front with a newmemoria/batch-limit-exceededfailure (ErrorCode.BadRequest, tagged withrequestedEventCountandmaximumEventCount) naming both counts; they still commit atomically with the sequence check, so their batches cannot be split. The two snapshot-writing paths — theGetAggregatecold path and snapshot refresh — now split their writes across as many batches as needed, so a stream of any length can be snapshotted. Those writes go over events that are already durable: link documents are written first and idempotently, the snapshot last, so a failure part-way leaves no snapshot and the next read simply redoes the work. See Cosmos DB write limits - The Cosmos DB store now shares one
CosmosClientacross the application.CosmosDataStoreandCosmosDomainServiceeach constructed and disposed their own client, and both are scoped — so an ASP.NET Core application created and destroyed two clients per request, plus a third on everyCosmosSetupcall. ACosmosClientperforms its own account discovery, builds its own routing map and, inDirectmode (the Memoria default), opens its own connections to every replica it touches; none of that survives disposal. A newCosmosClientProviderowns one client and the container resolved fromCosmosOptions, registered as a singleton byAddMemoriaCosmos.Disposeon both store types is now a no-op — they still implementIDisposable, so existingusingblocks compile and now stop tearing down connections other scopes are using. Constructing these types directly is a breaking change: see Upgrade to 1.6.0 - A recommended indexing policy for the Cosmos DB container.
CosmosSetupcreates the container with the Cosmos DB default policy, which indexes every path of every document — including the serialiseddatapayload, the largest property in the document, which no Memoria query can filter on (eventPropertyFiltercompiles toCONTAINS, which never uses an index).scripts/installnow ships a policy that indexes only the seven paths the store filters or sorts on, with idempotent PowerShell and Bash scripts to apply it. Measured against the emulator it takes about 2.4% off writes and 3-6% off reads. It deliberately defines no composite indexes: three were drafted and measured, and they added roughly 7% to every write while returning nothing, because these queries are single-partition and the range index onsequencealready serves their ordering — the guide carries the numbers.CosmosSetup.CreateDatabaseAndContainerIfNotExistnow applies it to containers it creates, so a consumer who never reads the guide still gets it; passnew IndexingPolicy()to keep the Cosmos DB default. Existing containers are untouched —CosmosSetup.ReplaceIndexingPolicybrings one across when you ask, since that starts a background reindex.CosmosIndexingPolicy.CreateRecommended()is the policy in code, and a test compares it against the shipped JSON so the two cannot drift. See Tune the Cosmos DB container and Upgrade to 1.6.0 - The Cosmos DB store test suite now runs.
Memoria.EventSourcing.Store.Cosmos.Testsreferencedxunitbut notxunit.runner.visualstudio, sodotnet testdiscovered no tests and exited zero — the 85 tests it inherits from the shared store suite had never executed. With the runner added they all pass against the Azure Cosmos DB emulator. They need one, and no CI runner provides it, so they carry[Trait("Category", "Emulator")]and are excluded from CI: run them locally before changing that store
Memoria 1.5.0
Released 29/08/2026
- Store failures are now classified. Every event store provider previously returned one indistinguishable failure for every failure path, so a caller could not tell an optimistic concurrency conflict — which is retryable by reloading — from the database being unreachable. Failures now carry an
ErrorCodeand a stableType:memoria/concurrency-conflict(ErrorCode.Conflict),memoria/storage-failure(ErrorCode.Error). Constants live on the newStoreFailuresclass. Applied across the Entity Framework Core and Cosmos DB providers together, and asserted in the shared store test suite so providers stay consistent. See Upgrade to 1.5.0 for what this may break, and Failure classification for the reference - New
ErrorCode.Conflict, appended last so the numeric values of existing members are unchanged - Saving an aggregate with no uncommitted events now succeeds on every provider, writing nothing. The Entity Framework Core store previously returned a failure while Cosmos DB returned success, and both already treated
SaveEventswith an empty array as success — so the Entity Framework CoreSaveAggregatepath was the only one that disagreed, including with its own sibling. If you relied on that failure to detect a command that produced no events, checkUncommittedEventsyourself before saving - Failure
Tagsnow carry the caller’s own context —streamId,expectedEventSequence,latestEventSequenceon a conflict,operationon a storage failure — plustraceIdwhen there is a currentActivity. A retry can readlatestEventSequencedirectly instead of issuing another read. Provider exception detail is deliberately excluded: it names tables, columns and constraints, and aFailuremapped onto an HTTP response would disclose it. That detail continues to be recorded on the currentActivity ErrorHandling.DefaultFailureon both store providers is superseded byStoreFailuresand is no longer returned, but remains so existing references compile- Event store index changes (Entity Framework Core).
IX_Events_StreamId_Sequenceis now unique, a newIX_Events_StreamId_CreatedDateserves the date-range reads (GetEventsFromDate,GetEventsUpToDate,GetEventsBetweenDates) that previously had to scan a whole stream, and two redundant indexes are dropped:IX_Events_StreamId(a prefix ofIX_Events_StreamId_Sequence) andIX_AggregateEvents_AggregateId(the leading column of the composite primary key). No table is rewritten and no data is migrated, but existing databases need the schema change applied — see Upgrade to 1.5.0 for the EF migration path and idempotent SQL Server and PostgreSQL scripts - Faster writes and reads in the Entity Framework Core store: event, aggregate and projection type-binding keys are resolved once per CLR type instead of by reflection on every write; the event-type filter uses a cached reverse index instead of scanning the binding dictionary on every query; and redundant sorts were removed from the aggregate and projection read paths
- The Entity Framework Core store no longer leaves written event and aggregate-event rows attached to the change tracker after a save, so a context reused across many saves no longer accumulates tracked entities
- The payload serializer is now replaceable.
IDomainSerializercontrols how event, aggregate and projection payloads are written and read by every store provider, withDomainSerializer.Currentdefaulting to the same Newtonsoft implementation used previously — nothing changes unless you replace it. Replacing it on an existing store is not a free choice: everything already persisted was written by the previous implementation and serializers differ in ways that fail silently, so verify against real stored payloads first - Install scripts for the store schema. Idempotent SQL Server and PostgreSQL scripts under
scripts/installcreate the four tables the Entity Framework Core store needs, so a database managed with DbUp, Flyway or by hand can be stood up without writing a migration. Consumers using EF Core migrations need nothing new —dotnet ef migrations addalready generates the schema from the model, and Memoria deliberately ships no migration files that could drift from it. Both scripts are verified on every CI run by building one database from the script and another from the model and comparing every column and index. See Install the store schema
Memoria 1.4.1
Released 25/08/2026
GetProjectionnow accepts aReadModeparameter matching the aggregate read modes (SnapshotOnly,SnapshotWithNewEvents,SnapshotOrCreate,SnapshotWithNewEventsOrCreate), enabling on-demand projection reconstruction from the event stream and snapshot refresh when new events have arrived; supported by the Entity Framework Core, Npgsql, and Cosmos DB store providers (and their in-memory variants)- New
GetInMemoryProjectionmethods onIDomainServicethat fold matching events into a fresh projection without persisting a snapshot, with overloads for the full stream, up to a specific sequence, and up to a specific date — the projection equivalent ofGetInMemoryAggregate
Memoria 1.4.0
Released 22/08/2026
- New
Projectionread-model base class for building query-optimised read models from events, and a sharedEventSourcedModelbase class (with matchingIEventSourcedModel/IProjectioninterfaces) thatAggregateRootandProjectionboth inherit for stream identity, versioning, and event application. Instance identity stays specific to each:AggregateIdonIAggregateRoot,ProjectionIdonIProjection - New
SaveProjectionandGetProjectionmethods onIDomainServicethat persist and retrieve projection snapshots, supported by the Entity Framework Core, Npgsql, and Cosmos DB store providers (and their in-memory variants). Each store uses a dedicated projection type: EF Core persists aProjectionEntityin its ownDomainProjectionstable, while Cosmos persists aProjectionDocumentin the same container as aggregates (discriminated bydocumentType) - New
[ProjectionType]attribute andIProjectionId<T>identifier for projections; projection types are auto-registered duringAddMemoriaEventSourcingassembly scanning
Memoria 1.3.2
Released 16/05/2026
- Replace Scrutor with a custom scanning mechanism.
Memoria 1.3.1
Released 13/05/2026
eventPropertyFilternow works correctly for non-string property values (numbers, booleans, null), not just strings, across the Entity Framework Core, Npgsql, and Cosmos DB store providers- New
Memoria.EventSourcing.Filtering.EventPropertyFilterValuehelper that coerces a stringly-typed filter value into the matching JSON-scalar literal so filters target the same form Newtonsoft.Json wrote into the event data
Memoria 1.3.0
Released 13/05/2026
- New
Memoria.EventSourcing.Store.EntityFrameworkCore.Npgsqlpackage: replaces the default substring-based event property filter with one that uses the Postgres@>JSON-containment operator, soeventPropertyFilterworks correctly againstjsonbcolumns and benefits from GIN indexes - New
IEventDataFilterextension point inMemoria.EventSourcing.Store.EntityFrameworkCore(in theFilteringnamespace) for plugging in provider-specific JSON filter strategies; the defaultSubstringEventDataFilterpreserves existing behavior ontextcolumns EntityFrameworkCoreDomainServiceand everyIDomainDbContextevent-query extension method now accept an optionalIEventDataFilter(non-breaking; defaults to substring)
Memoria 1.2.1
Released 12/05/2026
- Dependencies upgrade
Memoria 1.2.0
Released 10/05/2026
- Event property filtering on aggregate ids via
IAggregateId.EventPropertyFilter(key/value pairs applied when retrieving or reconstructing an aggregate) - New
eventPropertyFilterparameter across allIDomainServiceevent queries (GetEvents,GetEventsFromSequence,GetEventsUpToSequence,GetEventsBetweenSequences,GetEventsFromDate,GetEventsUpToDate,GetEventsBetweenDates,GetLatestEventSequence) - Property and type filters can be combined and are supported by both Cosmos DB and Entity Framework Core store providers (and their in-memory variants)
Memoria 1.1.0
Released 01/02/2026
- Upgrade to .NET 10
- New In Memory Service Bus provider
- New In Memory RabbitMQ provider
Memoria 1.0.0
Released 10/10/2025
- Rename OpenCQRS to Memoria
OpenCQRS 7.3.0
Released 09/10/2025
- New Cosmos InMemory store provider
OpenCQRS 7.2.0
Released 27/09/2025
- Read mode when getting an aggregate (BREAKING CHANGE):
- SnapshotOnly
- SnapshotWithNewEvents
- SnapshotOrCreate
- SnapshotWithNewEventsOrCreate
OpenCQRS 7.1.5
Released 15/09/2025
- Get aggregate with apply new events false returns now null if the aggregate doesn’t exist
- Update aggregate returns null if aggregate doesn’t exist
OpenCQRS 7.1.4
Released 13/09/2025
- Upgrade Nuget packages to latest versions
OpenCQRS 7.1.3
Released 13/09/2025
- UpdateAggregate stores a new aggregate if it doesn’t exist
- Minor improvements
OpenCQRS 7.1.2
Released 10/09/2025
- Rename IDomainEvent to IEvent
OpenCQRS 7.1.1
Released 10/09/2025
- Rename Aggregate to AggregateRoot
OpenCQRS 7.1.0
Released 10/09/2025
- New methods in the domain service (EntityFrameworkCore and CosmosDB):
- Get domain events between two sequences
- Get domain events up to a specific date
- Get domain events from a specific date
- Get domain events between two dates
- Get in memory aggregate up to a specific date
- Custom command handlers or services
- Updated XML documentation
OpenCQRS 7.0.0
Released 07/09/2025
- Upgrade to .NET 9
- New mediator pattern with commands, queries, and notifications
- Cosmos DB store provider
- Entity Framework Core store provider
- Extensions for db context in the Entity Framework Core store provider
- Support for IdentityDbContext from ASP.NET Core Identity
- Command validation
- Command sequences
- Automatic publishing of notifications and messages (ServiceBus or RabbitMQ) on the back of a successfully processed command
- Automatic caching of query results (MemoryCache or RedisCache)
- More flexible and extensible architecture
OpenCQRS 7.0.0-rc.1
Released 06/09/2025
- Memory Caching Provider
- Redis Caching Provider
OpenCQRS 7.0.0-beta.6
Released 05/09/2025
- Service Bus Provider
- RabbitMQ Provider
- Automatic publishing of messages on the back of a successfully processed command
OpenCQRS 7.0.0-beta.5
Released 01/09/2025
- Cosmos DB store provider
OpenCQRS 7.0.0-beta.4
Released 29/08/2025
- Send and publish methods that automatically publish notifications on the back of a successfully processed command
- Automatically validate commands before they are sent to the command handler
- Command sequences that allow to chain multiple commands in a specific order
OpenCQRS 7.0.0-beta.3
Released 26/08/2025
- Rename track methods in the Entity Framework Core store provider
- Rename database tables in the Entity Framework Core store provider
OpenCQRS 7.0.0-beta.2
Released 26/08/2025
- Replace events with notifications
OpenCQRS 7.0.0-beta.1
Released 25/08/2025
- Complete rewrite of the framework
- Upgrade to .NET 9