Skip to content
CAI
Software that uses CAICheck a score

learningcom/Learning.EventStore

43.8

Weak · 21 September 2026

3.6k

lines of production code

C#

primary language

4

measurements over time

CAI band scale
CAI trend line
CAI lens gauges

What this system is

This system is a .NET Core library implementing an Event Sourcing infrastructure with CQRS support, designed to persist domain state as a sequence of events. It provides pluggable backends for storing events in SQL Server, PostgreSQL, or Redis, and includes features like aggregate snapshotting, distributed locking, and a resilient message queue for handling event subscriptions. The framework also offers a central command/query hub and a sample inventory application to demonstrate its usage.

How it got here

2017 — Event store modernization and async migration

10 changes.

The project migrated its build infrastructure to Visual Studio 2017 and .NET Core SDK format while upgrading to .NET Core 2.2. The core event store implementation was refactored to support asynchronous operations, Redis-based L2 caching, and string-based aggregate identifiers, replacing previous Guid-centric designs. Comprehensive unit tests were added to validate the new caching, domain, and event subscription logic.

2018–2019 — multi-backend event store and message queue

17 changes.

This period focused on expanding the event store to support SQL Server and PostgreSQL backends alongside Redis, while introducing aggregate snapshotting to optimize performance. Concurrently, the Redis message queue was significantly enhanced with distributed locking, back-pressure, and robust retry mechanisms to ensure reliable, ordered event processing.

Features

Add ASP.NET Core sample application for inventory management

A new sample web application (Learning.EventStore.Sample.Web) has been added to demonstrate an inventory management system built on Event Sourcing. The app features a HomeController with views for listing, adding, checking in, removing, and renaming inventory items. It implements a CQRS pattern with write-model commands (CreateInventoryItem, CheckInItemsToInventory, etc.) and read-model queries backed by an in-memory database. Event subscriptions automatically update the read model when domain events occur. The application is configured to connect to a local Redis instance for the event store and message queue.

sample · high confidence

Add SQL Server and Redis event store implementations

The event store now supports persistence via SQL Server and Redis in addition to existing backends. The new SqlEventStore uses Dapper to read and write events to a relational database, with settings abstracted via ISqlEventStoreSettings to support both SQL Server and Postgres (including JSONB metadata handling). The RedisEventStore stores events in Redis hashes and lists, supports optional compression, and includes retry logic for concurrent writes. Both implementations implement the IEventStore interface and publish events to the configured message queue upon successful save.

src/Learning.EventStore/DataStores · high confidence

Add distributed lock configuration and settings base class

The library now includes a new \DistributedLockSettings\ class that allows users to configure the expiration time, acquisition wait time, and retry interval for session locks. Additionally, an abstract \EventStoreSettings\ base class has been introduced to serve as a foundation for application-specific event store configurations.

src/Learning.EventStore.Common · high confidence

Added base class for aggregate snapshotting

Introduced SnapshotAggregateRoot, a new base class that aggregates can inherit from to support state snapshotting. This class provides methods to capture the current state into a snapshot object and restore the aggregate's identity and version from a previously saved snapshot, streamlining the implementation of snapshot patterns for event-sourced aggregates.

Learning.EventStore · high confidence

Initial database schema and stored procedures for SQL Server and PostgreSQL

This change introduces the foundational database artifacts for the event store, providing parallel schema definitions and stored procedures for both SQL Server and PostgreSQL. For SQL Server, it adds the \Aggregate\ and \Event\ tables (with \varchar(max)\ for unbounded IDs and \nvarchar(max)\ for event data) and the \GetEventsForAggregate\ and \SaveEventForAggregate\ stored procedures. For PostgreSQL, it adds equivalent \aggregate\ and \event\ tables (using \varchar\ for unbounded IDs and \jsonb\ for event data) and corresponding \get\_events\_for\_aggregate\ and \save\_event\_for\_aggregate\ functions, ensuring consistent event persistence logic across both database engines.

database · high confidence

Introduce CQRS Hub for command and query dispatching

The new Learning.Cqrs project provides a central Hub that routes commands and queries to their respective handlers via dependency injection. It supports both synchronous and asynchronous operations for commands (with or without return values) and queries, allowing the application to execute business logic and retrieve data through a unified interface while validating that the appropriate handler exists for each request type.

src/Learning.Cqrs · high confidence

Introduce SQL Server and PostgreSQL support for the Event Store

The Sql directory now provides concrete implementations for storing events in both SQL Server and PostgreSQL databases. This change introduces database-specific connection factories (SqlServerConnectionFactory and PostgresConnectionFactory) and settings classes (SqlServerEventStoreSettings and PostgresEventStoreSettings) that manage separate read and write connection strings. It also adds a Dapper wrapper for executing SQL queries and commands, along with an EventDto for data transfer, enabling the Event Store to persist events using stored procedures in either supported SQL dialect.

src/Learning.EventStore.Common/Sql · high confidence

Introduce aggregate snapshotting with Redis support

The event store now supports snapshotting to optimize aggregate loading. A new SnapshotRepository decorates the existing repository to automatically save snapshots of aggregates inheriting from SnapshotAggregateRoot based on a configurable interval (defaulting to every 100 events) and restore them when available. The system includes a default snapshot strategy, a generic Snapshot model, and a Redis-based implementation (RedisSnapshotStore) that stores snapshots in Redis hashes with optional gzip compression. Configuration for the snapshot store, including application name and compression settings, is handled via SnapshotStoreSettings.

src/Learning.EventStore/Snapshotting · high confidence

Behavioural changes

Add DistributedLockException for subscription-level locking failures

A new DistributedLockException class has been introduced in the common exceptions library to handle failures when acquiring distributed locks. This exception captures specific context from the RedLockNet library, including the resource identifier, wait duration, and lock status, providing detailed error information for debugging lock contention issues at the subscription level.

src/Learning.EventStore.Common/Exceptions · high confidence

Aggregate IDs migrate from Guid to string and repository methods become async

The domain layer now supports non-Guid identifiers for aggregates by changing the \Id\ property on \AggregateRoot\ from \Guid\ to \string\. This shift is accompanied by a behavioral change in the repository: when an aggregate is not found, the system now returns \default(T)\ (null) instead of throwing an \AggregateNotFoundException\. Additionally, repository and session methods have been renamed to follow async naming conventions (e.g., \Get\ to \GetAsync\, \Save\ to \SaveAsync\), and the internal tracking of aggregates in the session now uses a thread-safe \ConcurrentDictionary\ with string keys to support the new ID type.

src/Learning.EventStore/Domain · high confidence

Cache layer refactored to support Redis L2 caching and async operations

The cache implementation now supports Redis as a secondary (L2) cache alongside the primary in-memory cache, with keys prefixed by environment and prefix to avoid collisions. The cache interface and implementations have been converted to fully asynchronous operations and now use string-based IDs instead of Guids, allowing non-Guid aggregate identifiers. Additionally, the CacheRepository has been updated to use per-aggregate locking instead of a global semaphore to improve concurrency, and null aggregates are now prevented from being serialized into the cache.

src/Learning.EventStore/Cache · high confidence

Event store API simplification and string-based partitioning

The event store interface has been updated to remove generic type parameters from the Get and Save methods, simplifying the API for consumers. Additionally, the IEvent interface now uses a string for the AggregateType property instead of Guid-based identifiers, and a new StringExtensions utility provides GZip compression for event values and a CRC32-based CalculatePartition method to support event store partitioning.

src/Learning.EventStore · high confidence

Introduces back-pressure, sequential processing, and distributed locking for Redis message queues

The Redis-based message queue now supports optional back-pressure via a configurable maximum queue length, sequential message processing to ensure ordered handling, and distributed locking to prevent concurrent processing of the same event across multiple application instances. These capabilities are exposed through new overloads on the event subscriber interface and are implemented in the new Redis subscriber and message queue classes, which also handle catching up on missed events during startup and moving failed messages to a dead-letter queue.

src/Learning.MessageQueue · high confidence

Introduces retry logic and dead-letter queue management for message processing

The repository layer now supports tracking retry attempts and managing dead-letter queues via Redis. It stores retry metadata (count, last retry time, exception details) in hash structures and provides methods to add events to the dead-letter queue, retrieve dead-letter messages, and move stuck processing events to dead letters. This enables the system to handle failed messages with configurable retry behavior and ensures atomicity for related Redis operations using transactions.

src/Learning.MessageQueue/Repository · high confidence

Migrate to Visual Studio 2017 and .NET Core SDK format

The project has been upgraded from Visual Studio 2015 (v14) to Visual Studio 2017 (v15) and migrated from the legacy xproj/project.json format to the modern .NET Core SDK-style csproj format. This change updates the solution file to reference standard .csproj files for all projects (including Learning.EventStore, Learning.MessageQueue, Learning.Cqrs, and their tests) and removes the global.json SDK version pinning, allowing the build to use the installed .NET Core SDK version.

(repo-wide) · high confidence

Redesign of Redis subscription and retry infrastructure

The message queue's Redis subscription and retry mechanisms have been restructured to support asynchronous processing, configurable locking, and sequential execution. New abstract base classes (AsyncRedisSubscription, RetryableAsyncRedisSubscription) and their synchronous counterparts now expose options to enable message locking and enforce sequential processing order at the subscription level. Retry logic has been moved to the subscription layer, introducing configurable time-to-live for dead-lettered messages and exponential backoff intervals, while stale processing events are automatically moved to the dead letter queue. The underlying interfaces and data models have also been updated, with IServiceLocator replaced by IMessage, IEventSubscriber replaced by RetryData, and the subscription method renamed to SubscribeAsync.

src/Learning.MessageQueue/Messages · high confidence

Redis client now includes automatic retry policies and configurable event store settings

The Redis client implementation has been updated to wrap all database operations in Polly-based retry policies with exponential backoff, improving resilience against transient failures. The client now supports both lazy and direct injection of the underlying connection multiplexer. Additionally, a new RedisEventStoreSettings class has been introduced, allowing users to configure compression options (enablement and threshold) and fine-tune transaction retry behavior (count and delay) for the event store.

src/Learning.EventStore.Common/Redis · high confidence

Test coverage

Added mock implementations for event store testing; Added tests for aggregate snapshotting behavior; Added unit tests for Redis message queue publishing and retry logic; Added unit tests for RedisEventStore and string compression extensions; Added unit tests for RedisEventSubscriber error handling and subscription logic; Added unit tests for SqlServerEventStore save and get operations; Added unit tests for cache repository and Redis cache behavior; Added unit tests for domain session and repository operations; Added unit tests for the CQRS Hub handler resolution.

Dependencies

Upgrade to .NET Core 2.2 and update core dependencies

The sample web application has been upgraded to target .NET Core 2.2, and the underlying libraries (Learning.EventStore, Learning.MessageQueue, Learning.Cqrs) now target netstandard2.0. This change includes updating key dependencies such as StackExchange.Redis to 2.6.70, Newtonsoft.Json to 13.0.3, and Polly to 7.2.0, while also switching the test projects to use MSTest v2.

(dependencies) · high confidence

Written by watchdog.canine.dev from the codebase's own history, inside the signed delivery this page is composed from.

How this codebase got here

Score

  • CAI 44 → 44 (+0.0)
  • Rubric changed (rubric-2026.08.18 → rubric-2026.09.15) — scores are not directly comparable.

Lenses

  • Code Health 69 → 60 (-9.5)
  • Architecture 93 → 93 (+0.0)
  • Maturity 43 → 46 (+3.2)
  • Readiness 31 → 30 (-0.5)
  • Security 65 → 63 (-1.7)
  • Domain Modelling 95 (new)
  • Event Sourcing 61 (new)
  • Performance 61 → 61 (+0.0)

Resolved (22)

  • Bounded contexts not declared
  • Duplicated block (10 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (11 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (19 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (6 lines × 2) (sample/Learning.EventStore.Sample.Web/Startup.cs)
  • High CVE: Microsoft.NETCore.App 2.1.0
  • High CVE: Microsoft.NETCore.App 2.1.0
  • High CVE: Microsoft.NETCore.App 2.1.0
  • High CVE: Microsoft.NETCore.App 2.1.0
  • High CVE: Microsoft.NETCore.App 2.2.0
  • High CVE: Microsoft.NETCore.App 2.2.0
  • LLM evaluation failed
  • Medium CVE: Microsoft.AspNetCore.App 2.2.0
  • Medium CVE: Microsoft.NETCore.App 2.1.0
  • Medium CVE: Microsoft.NETCore.App 2.1.0
  • Medium CVE: Microsoft.NETCore.App 2.1.0
  • Medium CVE: Microsoft.NETCore.App 2.1.0
  • Medium CVE: Microsoft.NETCore.App 2.2.0
  • Medium CVE: System.Data.SqlClient 4.8.0
  • No exposed public API
  • …and 2 more

New (18)

  • Duplicated block (11 lines × 2) (src/Learning.MessageQueue/RedisEventSubscriber.cs)
  • Duplicated block (12 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (22 lines × 2) (src/Learning.MessageQueue/RedisEventSubscriber.cs)
  • Duplicated block (24 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (50 lines × 2) (src/Learning.MessageQueue/Messages/RetryableAsyncRedisSubscription.cs)
  • Duplicated block (6 lines × 2) (src/Learning.Cqrs/Hub.cs)
  • Duplicated block (7–9 lines × 2) (sample/Learning.EventStore.Sample.Web/Startup.cs)
  • End-of-life runtime: .NET netcoreapp2.1
  • End-of-life runtime: .NET netcoreapp2.2
  • High CVE: Microsoft.NETCore.App 2.1.0
  • High CVE: Microsoft.NETCore.App 2.2.0
  • Inconsistent naming for identifier/ID properties across test mocks and domain models. Some use 'Id' (TestMessage, ItemsCheckedInToInventory), others use 'Number' (TestSnapshotAggregateSnapshot), and others use 'TimeStamp' or 'Name' or 'AggregateType' which are not IDs but are used in similar mock contexts. Specifically, 'TestMessage' uses 'Id' while 'TestAsyncMessage' uses 'TimeStamp' (likely a different concept, but 'TestMessage' vs 'TestAsyncMessage' naming is ambiguous). More critically, 'TestSnapshotAggregateSnapshot' uses 'Number' for what is likely a version or ID, while 'TestAggregateCreated' uses 'TimeStamp'.
  • The test helper 'DoSomethingElse' is vague and does not describe the behavior or the object it acts upon, unlike other test helpers which are more descriptive (e.g., 'CreateSnapshot', 'SaveAsync'). While not a direct conflict, it stands out as a poor naming convention compared to the rest of the codebase which uses descriptive verb-noun phrases for helpers.
  • Typo in type name 'TestAggregateDidSomeethingElse' (extra 'e' in 'Someething'). This is a naming inconsistency due to a typo, making the symbol name confusing and non-standard.
  • WriteOnlyPrivateField (test/Learning.EventStore.Test/Mocks/TestEventStore.cs)
  • bus factor 1 — one contributor carries this repository
  • redundant comment (src/Learning.EventStore/DataStores/RedisEventStore.cs)
  • redundant comment (test/Learning.Cqrs.Test/MissingCommand.cs)

API surface

  • Unchanged — 1 HTTP endpoints

Architecture

  • Unchanged — 2 containers · 2 contexts · 1 edges

Written by watchdog.canine.dev from the codebase's own history, inside the signed delivery this page is composed from.

Survey your own repository

learningcom/Learning.EventStore was measured the same way every project in this corpus was: the same rubric, at a pinned commit, with the result published in full. Point a surveyor at a repository you know and see whether you agree with it.

About this page

  • The score is its most recent published measurement, taken on 21 September 2026 at a pinned commit. It is not a live figure and does not change until the project is measured again.
  • Measured at commit 24fcfdda08dec12bb4b260ac7818606b9710aa97 — the exact code this score is about.
  • Scored under rubric-2026.09.15 — the same rubric and the same method as every other entry in this index.
  • Measured by watchdog.canine.dev using codehealth-analyzer preprod-28e75b8e3254.