Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5c98247
feat: abstractions for projections
the-avid-engineer Feb 28, 2022
a4b2c15
chore: add some doc comments
the-avid-engineer Mar 1, 2022
3640944
wip: checkpoint
the-avid-engineer Mar 1, 2022
3e3da3c
refactor: rebase on feat/better-reducer
the-avid-engineer Mar 2, 2022
9e2ab0c
refactor: not nullable, will never be null
the-avid-engineer Mar 2, 2022
c96fc18
bugfix: add missing base class
the-avid-engineer Mar 3, 2022
5689127
Delete IStatement.cs
the-avid-engineer Mar 3, 2022
a4020d1
checkpoint
the-avid-engineer Mar 3, 2022
41fa659
chore: todos for better way to update the cache
the-avid-engineer Mar 3, 2022
228d2b9
refactor: account for changes from #29
the-avid-engineer Mar 10, 2022
746bab3
feat: finish implementations first pass
the-avid-engineer Mar 18, 2022
724d49f
refactor: rename TransactionEntity to TestEntity
the-avid-engineer Mar 18, 2022
5f02e9d
refactor: rename variables to entity snapshots
the-avid-engineer Mar 18, 2022
2cc0fa0
test: add entity snapshot subscriber for all entity snapshot tests
the-avid-engineer Mar 18, 2022
97e5913
bugfix: didn't mean to commit this yet
the-avid-engineer Mar 18, 2022
c8f7934
refactor: better name
the-avid-engineer Mar 18, 2022
e939068
test: generalize snapshot tests to work for projection snapshots as well
the-avid-engineer Mar 18, 2022
2c5f595
chore: store entity type
the-avid-engineer Mar 19, 2022
2233b58
chore: add report generator for easily drilling down through coverage
the-avid-engineer Mar 19, 2022
f698bcf
chore: clean up coverage a little
the-avid-engineer Mar 19, 2022
1e85a8a
refactor: remove unused method
the-avid-engineer Mar 19, 2022
05ad389
wip: coverage test
the-avid-engineer Mar 19, 2022
9ac4bff
test: add coverage for projection repository/subscriber
the-avid-engineer Mar 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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