Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/charleszipp/phase
CQRS + Event Sourcing library with pluggable provider model
https://github.com/charleszipp/phase
Last synced: 17 days ago
JSON representation
CQRS + Event Sourcing library with pluggable provider model
- Host: GitHub
- URL: https://github.com/charleszipp/phase
- Owner: charleszipp
- License: mit
- Created: 2018-04-08T13:28:15.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2018-08-13T11:12:54.000Z (over 6 years ago)
- Last Synced: 2024-11-30T14:40:13.329Z (about 1 month ago)
- Language: C#
- Size: 5.46 MB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Phase
Phase is an in-process, platform agnostic, tenant isolating, CQRS & Event Sourcing for stateful services. Phase operates against in-memomry state as its primary source of data for both writes and reads. It then provides an abstraction for persisting the events to a durable store. This allows the entire framework to be executed end-to-end, in-proc from any platform.# Bootstrapping
Phase must be constructed of 3 dependent objects.
- Dependency Resolver - IoC container abstraction
- Events Provider - Persistence layer abstraction
- Tenant Key Factory - Delegate resolver for segments of a tenant keyThe following example wires up Phase with the Ninject dependency resolver provided with `Phase.Ninject` package and the In-Memory Events Provider provided with `Phase` package
```csharp
var dependencyResolver = new NinjectDependencyResolver();
var eventsProvider = new InMemoryEventsProvider(new InMemoryEventCollection(), TenantKeyFactory);
var phase = new PhaseBuilder(dependencyResolver, eventsProvider, TenantKeyFactory)
.Build();
```Phase provides a builder/decorator implementation to extend the construction of phase. Out-of-the-box the `Phase` package provides several builders for wiring up command and query handlers, aggregates, and read models
```csharp
var dependencyResolver = new NinjectDependencyResolver();
var tenantKeyFactory = (tenantInstanceName) => new Dictionary { { "boardid", tenantInstanceName } };
var eventsProvider = new InMemoryEventsProvider(new InMemoryEventCollection(), tenantKeyFactory);
var phase = new PhaseBuilder(dependencyResolver, eventsProvider, tenantKeyFactory)
.WithCommandHandler()
.WithQueryHandler()
.WithAggregateRoot()
.WithReadModel()
.WithStatefulEventSubscriber()
.Build();
```# Occupying/Vacating Phase
Phase essentially operates as a stateful actor within a system. Before any commands or queries can be executed, phase must first be occupied by a tenant. Once occupied, the commands and queries will be tenant context aware and be able to provide tenant isolation.Within a platform's startup/activation phase should be occupied for a given tenant.
```csharp
await phase.OccupyAsync(tenantId, cancellationToken);
```
Once commands and queries are no longer needed to be served for a given tenant, the Vacate method should be invoked. This will dehydrate all in-memory state associated with the given tenant
```csharp
await phase.VacateAsync(cancellationToken)
```# Putting It All Together
```csharp
var budgetId = Guid.NewGuid().ToString();
var cancellation = new CancellationTokenSource();var dependencyResolver = new NinjectDependencyResolver();
var tenantKeyFactory = (tenantInstanceName) => new Dictionary { { "boardid", tenantInstanceName } };
var eventsProvider = new InMemoryEventsProvider(new InMemoryEventCollection(), tenantKeyFactory);
var phase = new PhaseBuilder(dependencyResolver, eventsProvider, tenantKeyFactory)
.WithCommandHandler()
.WithQueryHandler()
.WithAggregateRoot()
.WithReadModel()
.WithStatefulEventSubscriber()
.Build();// occupy for the budget aggregate/tenant
await phase.OccupyAsync(budgetId, cancellation.Token);// execute a command to add a new account
await phase.ExecuteAsync(new LinkAccount(Guid.NewGuid(), "111", "Checking"), cancellation.Token);// execute a query, get the accounts
var result = await phase.QueryAsync(new GetAccounts(), cancellation.Token);// vacate for this tenant
await phase.VacateAsync(cancellation.Token);
```# Defining the Constructs
The following will illustrate how the different constructs within phase can be implemented. There are 4 major constructs## Aggregate Roots
The aggregate root represents a typical aggregate as defined in the Domain Driven Design concepts. It represents the domain's model and boundary. Its the only object in the domain model that the commands can/should reference directly. The data within aggegates are considered to be immediately consistent. This makes it safe for commands to use for validation and making decisions.```csharp
[PhaseAggregate("budgets")]
internal class BudgetAggregate : AggregateRoot
{
// define the state this aggregate uses
internal IDictionary Accounts { get; } = new Dictionary();// define events that it handles
public void Apply(AccountLinked e)
{
Accounts[e.AccountId] = new AccountEntity(e.AccountId, e.AccountNumber, e.AccountName);
}
}
```## Read Models
The read model is typically an alternate representation of the domain model structured for read efficiency. For example, if the application typically serves aggregated/summarized data disproportionately more often than it takes input from users then it can be beneficial to create a denormalized model tailored for aggregation/summarization. An example of such a model would be one that follows the traditional star or snowflake schema.
```csharp
internal class BudgetLedger : IHandleEvent, IVolatileState
{
internal IDictionary Accounts = new Dictionary();public void Handle(AccountLinked @event)
{
Accounts[@event.AccountId] = new AccountEntity(@event.AccountId, @event.AccountNumber, @event.AccountName);
}
}
```## Commands
Commands should execute an operation whose intent is to potentially modify state. Commands should use an Aggregate Root to perform validation and raise events.
```csharp
internal class LinkAccountHandler : CommandHandler
{
public override async Task Execute(LinkAccount command, CancellationToken cancellationToken)
{
// get the aggregate root
var budget = await GetAggregateAsync(TenantId, cancellationToken);// perform validation
if(budget.Accounts.Any(a => (a.Key == command.AccountId) || (a.Value.Name == command.AccountNumber && a.Value.Number == command.AccountNumber)))
throw new ArgumentException("Account already exists with the same name and number or id");// raise an event
await RaiseEventAsync(TenantId, new AccountLinked(command.AccountId, command.AccountNumber, command.AccountName), cancellationToken);
}
}
```## Queries
Queries should execute and operation whose intent is to only return data. Queries should not modify state. Queries should use a Read Model to materialize the data being requested.
```csharp
internal class GetAccountsHandler : IHandleQuery
{
// read model
private readonly BudgetLedger _ledger;// inject the read model
public GetAccountsHandler(BudgetLedger ledger)
{
_ledger = ledger;
}public Task Execute(GetAccounts query, CancellationToken cancellationToken)
{
// execute a linq to objects query against the read model to materialize and return the result
return Task.FromResult(new GetAccountsResult(
_ledger.Accounts.Select(a => new Account(a.Key, a.Value.Number, a.Value.Name)).ToList() // be sure to enumerate!
));
}
}
```