Skip to content

Commit

Permalink
Feature: Projections (#27)
Browse files Browse the repository at this point in the history
* feat: abstractions for projections

* chore: add some doc comments

* wip: checkpoint

* refactor: rebase on feat/better-reducer

* refactor: not nullable, will never be null

* bugfix: add missing base class

* Delete IStatement.cs

* checkpoint

Co-Authored-By: watterssn <297749+watterssn@users.noreply.github.com>

* chore: todos for better way to update the cache

* refactor: account for changes from #29

* feat: finish implementations first pass

* refactor: rename TransactionEntity to TestEntity

(no such thing as non-transaction entity)

* refactor: rename variables to entity snapshots

to be distinct from single entity projection snapshots

* test: add entity snapshot subscriber for all entity snapshot tests

* bugfix: didn't mean to commit this yet

* refactor: better name

* test: generalize snapshot tests to work for projection snapshots as well

* chore: store entity type

* chore: add report generator for easily drilling down through coverage

* chore: clean up coverage a little

* refactor: remove unused method

* wip: coverage test

* test: add coverage for projection repository/subscriber

Co-authored-by: watterssn <297749+watterssn@users.noreply.github.com>
  • Loading branch information
the-avid-engineer and watterssn authored Mar 22, 2022
1 parent 4c4eb71 commit 80659fe
Show file tree
Hide file tree
Showing 42 changed files with 1,137 additions and 177 deletions.
12 changes: 12 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.1.2",
"commands": [
"reportgenerator"
]
}
}
}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
TestResults
CoverageResults
CoverageReport

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
Expand Down
9 changes: 9 additions & 0 deletions generate-coverage-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/sh
rm -rf ./TestResults ./CoverageReport
dotnet tool restore
dotnet restore EntityDb.sln --locked-mode
#dotnet test EntityDb.sln --no-restore -c Debug --collect:"XPlat Code Coverage" -r ./TestResults -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover
#dotnet reportgenerator -reports:"./TestResults/**/coverage.opencover.xml" -targetdir:"CoverageReport" -reporttypes:Html -license:"ew0KICAiTGljZW5zZSI6IHsNCiAgICAiSWQiOiAiZDZkYjQ2NTYtMjUyMS00MjNlLWE0MTgtZmU2NjJiNDZiMDk3IiwNCiAgICAiTG9naW4iOiAidGhlLWF2aWQtZW5naW5lZXIiLA0KICAgICJOYW1lIjogIkNocmlzIFBoaWxpcHMiLA0KICAgICJFbWFpbCI6IG51bGwsDQogICAgIkxpY2Vuc2VUeXBlIjogIlBybyIsDQogICAgIklzc3VlZEF0IjogIjIwMjItMDMtMTlUMDA6MjA6MTYuNDQ2OTg3MVoiDQogIH0sDQogICJTaWduYXR1cmUiOiAidmZtTldaN2VVeTZjbVpaUFVMS2hCUzBSYVAzNVFjdlNRMWdEbU1PWTFhenlsTlpUVDNEUjl0bWptZDc3RTZVaTAycFZOWDA2YTdCMzc4ZHV6NHZ0TFdFOWg2VDNOMUhXWTJwXHUwMDJCN0FYbVlqc1x1MDAyQmdTbVNud0tueWFiNjJ5dHhLd3Y0TWdLbjh2OHF4aXlCOVBJMGV4YkJPTE11VXAwWVQ4WDc0YW9teWhEam5aWm9kbXhrN05zYzllQTBxRnBFaEZ0QkEzRzNFSWdcdTAwMkJNNHVPb002VEtNMERaV2g1NGFsVHloU25QS2ZNSjRuMWp1OWxGaHlHektDaXhOcUJhTk5LSVl6UnFEWmhxbEpYSGFBQmM4RnNGVlNWVnBwQTN6dUR3aGxxYmxHZGhOVFpTM0w2Y1FyNXNrMS9RV1ZpNDYwbXFnVWNlSVx1MDAyQmI3R2d5eFFCM3AyUG9ZQ1FFREE9PSINCn0="
dotnet test EntityDb.sln --no-restore -c Debug --collect:"XPlat Code Coverage" -r ./TestResults
dotnet reportgenerator -reports:"./TestResults/**/coverage.cobertura.xml" -targetdir:"CoverageReport" -reporttypes:Html -license:"ew0KICAiTGljZW5zZSI6IHsNCiAgICAiSWQiOiAiZDZkYjQ2NTYtMjUyMS00MjNlLWE0MTgtZmU2NjJiNDZiMDk3IiwNCiAgICAiTG9naW4iOiAidGhlLWF2aWQtZW5naW5lZXIiLA0KICAgICJOYW1lIjogIkNocmlzIFBoaWxpcHMiLA0KICAgICJFbWFpbCI6IG51bGwsDQogICAgIkxpY2Vuc2VUeXBlIjogIlBybyIsDQogICAgIklzc3VlZEF0IjogIjIwMjItMDMtMTlUMDA6MjA6MTYuNDQ2OTg3MVoiDQogIH0sDQogICJTaWduYXR1cmUiOiAidmZtTldaN2VVeTZjbVpaUFVMS2hCUzBSYVAzNVFjdlNRMWdEbU1PWTFhenlsTlpUVDNEUjl0bWptZDc3RTZVaTAycFZOWDA2YTdCMzc4ZHV6NHZ0TFdFOWg2VDNOMUhXWTJwXHUwMDJCN0FYbVlqc1x1MDAyQmdTbVNud0tueWFiNjJ5dHhLd3Y0TWdLbjh2OHF4aXlCOVBJMGV4YkJPTE11VXAwWVQ4WDc0YW9teWhEam5aWm9kbXhrN05zYzllQTBxRnBFaEZ0QkEzRzNFSWdcdTAwMkJNNHVPb002VEtNMERaV2g1NGFsVHloU25QS2ZNSjRuMWp1OWxGaHlHektDaXhOcUJhTk5LSVl6UnFEWmhxbEpYSGFBQmM4RnNGVlNWVnBwQTN6dUR3aGxxYmxHZGhOVFpTM0w2Y1FyNXNrMS9RV1ZpNDYwbXFnVWNlSVx1MDAyQmI3R2d5eFFCM3AyUG9ZQ1FFREE9PSINCn0="
open CoverageReport/index.html
36 changes: 36 additions & 0 deletions src/EntityDb.Abstractions/Projections/IProjectionRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using EntityDb.Abstractions.Disposables;
using EntityDb.Abstractions.Snapshots;
using EntityDb.Abstractions.Transactions;
using EntityDb.Abstractions.ValueObjects;
using System.Threading.Tasks;

namespace EntityDb.Abstractions.Projections;

/// <summary>
/// Encapsulates the snapshot repository for a projection.
/// </summary>
/// <typeparam name="TProjection">The type of the projection.</typeparam>
public interface IProjectionRepository<TProjection> : IDisposableResource
{
/// <summary>
/// The strategy for mapping between projection id and entity id.
/// </summary>
IProjectionStrategy<TProjection> ProjectionStrategy { get; }

/// <summary>
/// The backing transaction repository.
/// </summary>
ITransactionRepository TransactionRepository { get; }

/// <summary>
/// The backing snapshot repository.
/// </summary>
ISnapshotRepository<TProjection> SnapshotRepository { get; }

/// <summary>
/// Returns the current state of a <typeparamref name="TProjection" />.
/// </summary>
/// <param name="projectionId">The id of the projection.</param>
/// <returns>The current state of a <typeparamref name="TProjection" />.</returns>
Task<TProjection> GetCurrent(Id projectionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System.Threading.Tasks;

namespace EntityDb.Abstractions.Projections;

/// <summary>
/// Represents a type used to create instances of <see cref="IProjectionRepository{TProjection}" />
/// </summary>
/// <typeparam name="TProjection">The type of projection managed by the <see cref="IProjectionRepository{TProjection}" />.</typeparam>
public interface IProjectionRepositoryFactory<TProjection>
{
/// <summary>
/// Create a new instance of <see cref="IProjectionRepository{TProjection}" />
/// </summary>
/// <param name="transactionSessionOptionsName">The agent's use case for the transaction repository.</param>
/// <param name="snapshotSessionOptionsName">The agent's use case for the snapshot repository.</param>
/// <returns>A new instance of <see cref="IProjectionRepository{TProjection}" />.</returns>
Task<IProjectionRepository<TProjection>> CreateRepository(string transactionSessionOptionsName,
string snapshotSessionOptionsName);
}
26 changes: 26 additions & 0 deletions src/EntityDb.Abstractions/Projections/IProjectionStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using EntityDb.Abstractions.ValueObjects;
using System.Threading.Tasks;

namespace EntityDb.Abstractions.Projections;

/// <summary>
/// Represents a type that can map a projection it to a set of entity ids.
/// </summary>
/// <typeparam name="TProjection"></typeparam>
public interface IProjectionStrategy<in TProjection>
{
/// <summary>
/// Map a projection id to a set of entity ids.
/// </summary>
/// <param name="projectionId">The id of the projection.</param>
/// <param name="projectionSnapshot">A snapshot of the projection, if one exists. (This can be used to avoid running a query, if one were necessary.)</param>
/// <returns>The set of entity ids to query for running the projection.</returns>
Task<Id[]> GetEntityIds(Id projectionId, TProjection projectionSnapshot);

/// <summary>
/// Map an entity id to a set of projection ids.
/// </summary>
/// <param name="entityId">The id of th entity.</param>
/// <returns>The set of projection ids to query for running the projection.</returns>
Task<Id[]> GetProjectionIds(Id entityId);
}
18 changes: 17 additions & 1 deletion src/EntityDb.Common/Annotations/EntityAnnotation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using EntityDb.Abstractions.Annotations;
using EntityDb.Abstractions.Transactions;
using EntityDb.Abstractions.Transactions.Steps;
using EntityDb.Abstractions.ValueObjects;

namespace EntityDb.Common.Annotations;
Expand All @@ -10,4 +12,18 @@ internal record EntityAnnotation<TData>
Id EntityId,
VersionNumber EntityVersionNumber,
TData Data
) : IEntityAnnotation<TData>;
) : IEntityAnnotation<TData>
{
public static EntityAnnotation<TData> CreateFrom(ITransaction transaction, ITransactionStep transactionStep,
TData data)
{
return new
(
transaction.Id,
transaction.TimeStamp,
transactionStep.EntityId,
transactionStep.EntityVersionNumber,
data
);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using EntityDb.Abstractions.Disposables;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace EntityDb.Common.Disposables;

internal class DisposableResourceBaseClass : IDisposableResource
{
[ExcludeFromCodeCoverage(Justification = "All Tests Use DisposeAsync")]
public virtual void Dispose()
{
DisposeAsync().AsTask().Wait();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using EntityDb.Abstractions.Disposables;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;

namespace EntityDb.Common.Disposables;

internal record DisposableResourceBaseRecord : IDisposableResource
{
[ExcludeFromCodeCoverage(Justification = "All Tests Use DisposeAsync")]
public virtual void Dispose()
{
DisposeAsync().AsTask().Wait();
Expand Down
8 changes: 7 additions & 1 deletion src/EntityDb.Common/Entities/IEntity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@ public interface IEntity<out TEntity>
/// <returns>A new instance of <typeparamref name="TEntity" />.</returns>
abstract static TEntity Construct(Id entityId);

/// <summary>
/// Returns the id of the entity.
/// </summary>
/// <returns>The id of this entity.</returns>
Id GetId();

/// <summary>
/// Returns the version number of the entity.
/// </summary>
/// <returns></returns>
/// <returns>The id of this entity.</returns>
VersionNumber GetVersionNumber();

/// <summary>
Expand Down
43 changes: 39 additions & 4 deletions src/EntityDb.Common/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using EntityDb.Abstractions.Agents;
using EntityDb.Abstractions.Entities;
using EntityDb.Abstractions.Projections;
using EntityDb.Abstractions.Transactions;
using EntityDb.Common.Entities;
using EntityDb.Common.Projections;
using EntityDb.Common.Snapshots;
using EntityDb.Common.Transactions;
using EntityDb.Common.Transactions.Builders;
Expand Down Expand Up @@ -85,16 +87,49 @@ public static void AddEntity<TEntity>(this IServiceCollection serviceCollection)
/// <summary>
/// Adds a transaction subscriber that records snapshots of entities.
/// </summary>
/// <typeparam name="TSnapshot">The type of the snapshot.</typeparam>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="snapshotSessionOptionsName">The agent's intent for the snapshot repository.</param>
/// <param name="synchronousMode">If <c>true</c> then snapshots will be synchronously recorded.</param>
public static void AddEntitySnapshotTransactionSubscriber<TSnapshot>(this IServiceCollection serviceCollection,
/// <typeparam name="TEntity">The type of the entity.</typeparam>
public static void AddEntitySnapshotTransactionSubscriber<TEntity>(this IServiceCollection serviceCollection,
string snapshotSessionOptionsName, bool synchronousMode = false)
where TEntity : IEntity<TEntity>, ISnapshot<TEntity>
{
serviceCollection.AddSingleton<ITransactionSubscriber>(serviceProvider =>
EntitySnapshotTransactionSubscriber<TEntity>.Create(serviceProvider, snapshotSessionOptionsName,
synchronousMode));
}

/// <summary>
/// Adds a projection strategy.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <typeparam name="TProjection">The type of the projection.</typeparam>
/// <typeparam name="TProjectionStrategy">The type of the projection strategy.</typeparam>
public static void AddProjection<TProjection, TProjectionStrategy>(
this IServiceCollection serviceCollection)
where TProjection : IProjection<TProjection>
where TProjectionStrategy : class, IProjectionStrategy<TProjection>
{
serviceCollection.AddSingleton<IProjectionStrategy<TProjection>, TProjectionStrategy>();
serviceCollection
.AddTransient<IProjectionRepositoryFactory<TProjection>, ProjectionRepositoryFactory<TProjection>>();
}

/// <summary>
/// Adds a transaction subscriber that records snapshots of projections.
/// </summary>
/// <param name="serviceCollection">The service collection.</param>
/// <param name="snapshotSessionOptionsName">The agent's intent for the snapshot repository.</param>
/// <param name="synchronousMode">If <c>true</c> then snapshots will be synchronously recorded.</param>
/// <typeparam name="TProjection">The type of the projection.</typeparam>
public static void AddProjectionSnapshotTransactionSubscriber<TProjection>(
this IServiceCollection serviceCollection,
string snapshotSessionOptionsName, bool synchronousMode = false)
where TSnapshot : ISnapshot<TSnapshot>
where TProjection : IProjection<TProjection>, ISnapshot<TProjection>
{
serviceCollection.AddSingleton<ITransactionSubscriber>(serviceProvider =>
EntitySnapshotTransactionSubscriber<TSnapshot>.Create(serviceProvider, snapshotSessionOptionsName,
ProjectionSnapshotTransactionSubscriber<TProjection>.Create(serviceProvider, snapshotSessionOptionsName,
synchronousMode));
}
}
31 changes: 31 additions & 0 deletions src/EntityDb.Common/Projections/IProjection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using EntityDb.Abstractions.Annotations;
using EntityDb.Abstractions.ValueObjects;

namespace EntityDb.Common.Projections;

/// <summary>
/// Provides basic functionality for the common implementations.
/// </summary>
/// <typeparam name="TProjection"></typeparam>
public interface IProjection<out TProjection>
{
/// <summary>
/// Creates a new instance of a <typeparamref name="TProjection" />.
/// </summary>
/// <param name="projectionId">The id of the entity.</param>
/// <returns>A new instance of <typeparamref name="TProjection" />.</returns>
abstract static TProjection Construct(Id projectionId);

/// <summary>
/// Returns the current version number of an entity.
/// </summary>
/// <returns></returns>
VersionNumber GetEntityVersionNumber(Id entityId);

/// <summary>
/// Returns a new <typeparamref name="TProjection"/> that incorporates the commands for a particular entity id.
/// </summary>
/// <param name="annotatedCommands">The annotated commands.</param>
/// <returns>A new <typeparamref name="TProjection"/> that incorporates <paramref name="annotatedCommands"/>.</returns>
TProjection Reduce(params IEntityAnnotation<object>[] annotatedCommands);
}
67 changes: 67 additions & 0 deletions src/EntityDb.Common/Projections/ProjectionRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using EntityDb.Abstractions.Projections;
using EntityDb.Abstractions.Snapshots;
using EntityDb.Abstractions.Transactions;
using EntityDb.Abstractions.ValueObjects;
using EntityDb.Common.Disposables;
using EntityDb.Common.Queries;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace EntityDb.Common.Projections;

internal sealed class ProjectionRepository<TProjection> : DisposableResourceBaseClass, IProjectionRepository<TProjection>
where TProjection : IProjection<TProjection>
{
public IProjectionStrategy<TProjection> ProjectionStrategy { get; }
public ITransactionRepository TransactionRepository { get; }
public ISnapshotRepository<TProjection> SnapshotRepository { get; }

public ProjectionRepository
(
IProjectionStrategy<TProjection> projectionStrategy,
ISnapshotRepository<TProjection> snapshotRepository,
ITransactionRepository transactionRepository
)
{
ProjectionStrategy = projectionStrategy;
TransactionRepository = transactionRepository;
SnapshotRepository = snapshotRepository;
}

public async Task<TProjection> GetCurrent(Id projectionId)
{
var projection = await SnapshotRepository.GetSnapshot(projectionId) ?? TProjection.Construct(projectionId);

var entityIds = await ProjectionStrategy.GetEntityIds(projectionId, projection);

if (entityIds.Length == 0)
{
return projection;
}

foreach (var entityId in entityIds)
{
var entityVersionNumber = projection.GetEntityVersionNumber(entityId);

var commandQuery = new GetCurrentEntityQuery(entityId, entityVersionNumber);

var annotatedCommands = await TransactionRepository.GetAnnotatedCommands(commandQuery);

projection = projection.Reduce(annotatedCommands);
}

return projection;
}

public static ProjectionRepository<TProjection> Create
(
IServiceProvider serviceProvider,
ITransactionRepository transactionRepository,
ISnapshotRepository<TProjection> snapshotRepository
)
{
return ActivatorUtilities.CreateInstance<ProjectionRepository<TProjection>>(serviceProvider,
transactionRepository, snapshotRepository);
}
}
39 changes: 39 additions & 0 deletions src/EntityDb.Common/Projections/ProjectionRepositoryFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using EntityDb.Abstractions.Projections;
using EntityDb.Abstractions.Snapshots;
using EntityDb.Abstractions.Transactions;
using System;
using System.Threading.Tasks;

namespace EntityDb.Common.Projections;

internal class ProjectionRepositoryFactory<TProjection> : IProjectionRepositoryFactory<TProjection>
where TProjection : IProjection<TProjection>
{
private readonly IServiceProvider _serviceProvider;
private readonly ITransactionRepositoryFactory _transactionRepositoryFactory;
private readonly ISnapshotRepositoryFactory<TProjection> _snapshotRepositoryFactory;

public ProjectionRepositoryFactory
(
IServiceProvider serviceProvider,
ITransactionRepositoryFactory transactionRepositoryFactory,
ISnapshotRepositoryFactory<TProjection> snapshotRepositoryFactory
)
{
_serviceProvider = serviceProvider;
_transactionRepositoryFactory = transactionRepositoryFactory;
_snapshotRepositoryFactory = snapshotRepositoryFactory;
}

public async Task<IProjectionRepository<TProjection>> CreateRepository(string transactionSessionOptionsName, string snapshotSessionOptionsName)
{
var transactionRepository =
await _transactionRepositoryFactory.CreateRepository(transactionSessionOptionsName);

var snapshotRepository =
await _snapshotRepositoryFactory.CreateRepository(snapshotSessionOptionsName);

return ProjectionRepository<TProjection>.Create(_serviceProvider,
transactionRepository, snapshotRepository);
}
}
Loading

0 comments on commit 80659fe

Please sign in to comment.