Skip to content

Commit

Permalink
Merge pull request #55 from jacqueskang/feature/caching
Browse files Browse the repository at this point in the history
feat: provide an abstract aggregate repository which supports distrib…
  • Loading branch information
jacqueskang authored Feb 11, 2020
2 parents a07a5cf + 19bd32a commit bd41d85
Show file tree
Hide file tree
Showing 10 changed files with 189 additions and 7 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Support various of event store:
- JKang.EventSourcing.Persistence.EfCore [![NuGet version](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.EfCore.svg)](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.EfCore)
- JKang.EventSourcing.Persistence.DynamoDB [![NuGet version](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.DynamoDB.svg)](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.DynamoDB)
- JKang.EventSourcing.Persistence.CosmosDB [![NuGet version](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.CosmosDB.svg)](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.CosmosDB)
- JKang.EventSourcing.Persistence.Caching [![NuGet version](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.Caching.svg)](https://badge.fury.io/nu/JKang.EventSourcing.Persistence.Caching)

## Quick Start:

Expand Down Expand Up @@ -190,13 +191,22 @@ giftCard.Debit(50); // ==> balance: 10
giftCard.Debit(20); // ==> invalid operation exception
```

## How to programmatically initialize event store?
## FAQs

### How to programmatically initialize event store?

See [this page](doc/StoreInitialization.md).

## How to use snapshots to optimize performance?
### How to use snapshots to optimize performance?

See [this page](doc/Snapshots.md).

### How to improve performance using caching?

Consider install the nuget package `JKang.EventSourcing.Persistence.Caching` and inherit the `CachedAggregateRepository` class.
It leverages `Microsoft.Extensions.Caching.Distributed.IDistributedCache` to cache aggregate every time after loaded from or saved into repository.

Consider configuring a short sliding expiration (e.g., 5 sec) to reduce the chance of having cache out of date.

---
__Please feel free to download, fork and/or provide any feedback!__
2 changes: 1 addition & 1 deletion samples/Samples.Events/Samples.Events.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JKang.EventSourcing.Abstractions" Version="1.0.0-beta.2" />
<PackageReference Include="JKang.EventSourcing.Abstractions" Version="1.0.1" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion samples/Samples.Persistence/Samples.Persistence.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JKang.EventSourcing.Abstractions" Version="1.0.0-beta.2" />
<PackageReference Include="JKang.EventSourcing.Abstractions" Version="1.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions samples/Samples.WebApp/Samples.WebApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="JKang.EventSourcing" Version="1.0.0-beta.2" />
<PackageReference Include="JKang.EventSourcing.Persistence.EfCore" Version="1.0.0-beta.2" />
<PackageReference Include="JKang.EventSourcing" Version="1.0.1" />
<PackageReference Include="JKang.EventSourcing.Persistence.EfCore" Version="1.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="3.1.1" />
</ItemGroup>

Expand Down
6 changes: 6 additions & 0 deletions src/EventSourcing.sln
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "doc", "doc", "{6374E9CF-DFD
..\doc\StoreInitialization.md = ..\doc\StoreInitialization.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "JKang.EventSourcing.Persistence.Caching", "JKang.EventSourcing.Persistence.Caching\JKang.EventSourcing.Persistence.Caching.csproj", "{5BDFE811-5E80-41E0-9641-90BB0F3C34A8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -92,6 +94,10 @@ Global
{1D8A6FFC-1D14-4EA0-A42A-959E38D46EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D8A6FFC-1D14-4EA0-A42A-959E38D46EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D8A6FFC-1D14-4EA0-A42A-959E38D46EC1}.Release|Any CPU.Build.0 = Release|Any CPU
{5BDFE811-5E80-41E0-9641-90BB0F3C34A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5BDFE811-5E80-41E0-9641-90BB0F3C34A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5BDFE811-5E80-41E0-9641-90BB0F3C34A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5BDFE811-5E80-41E0-9641-90BB0F3C34A8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using JKang.EventSourcing.Domain;
using JKang.EventSourcing.Events;
using JKang.EventSourcing.Snapshotting.Persistence;
using Microsoft.Extensions.Caching.Distributed;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace JKang.EventSourcing.Persistence
{
public abstract class CachedAggregateRepository<TAggregate, TKey>
: AggregateRepository<TAggregate, TKey>
where TAggregate : class, IAggregate<TKey>
{
private readonly IDistributedCache _cache;
private readonly DistributedCacheEntryOptions _cacheOptions;
private static readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Objects,
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None,
Converters = new[] { new StringEnumConverter() },
MetadataPropertyHandling = MetadataPropertyHandling.ReadAhead,
};

protected CachedAggregateRepository(
IEventStore<TAggregate, TKey> eventStore,
ISnapshotStore<TAggregate, TKey> snapshotStore,
IDistributedCache cache,
DistributedCacheEntryOptions cacheOptions)
: base(eventStore, snapshotStore)
{
_cache = cache;
_cacheOptions = cacheOptions;
}

protected override async Task<TAggregate> FindAggregateAsync(
TKey id,
bool ignoreSnapshot = false,
CancellationToken cancellationToken = default)
{
string key = GetCacheKey(id);
string serialized = await _cache.GetStringAsync(key, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(serialized))
{
return DeserializeAggregate(serialized);
}

TAggregate aggregate = await base.FindAggregateAsync(id, ignoreSnapshot, cancellationToken)
.ConfigureAwait(false);

await CacheAsync(aggregate, cancellationToken).ConfigureAwait(false);

return aggregate;
}

protected override async Task<IAggregateChangeset<TKey>> SaveAggregateAsync(
TAggregate aggregate,
CancellationToken cancellationToken = default)
{
if (aggregate is null)
{
throw new System.ArgumentNullException(nameof(aggregate));
}

IAggregateChangeset<TKey> changeset = await base.SaveAggregateAsync(aggregate, cancellationToken)
.ConfigureAwait(false);

await CacheAsync(aggregate, cancellationToken).ConfigureAwait(false);

return changeset;
}

private async Task CacheAsync(TAggregate aggregate,
CancellationToken cancellationToken)
{
string serialized = SerializeAggregate(aggregate);
string key = GetCacheKey(aggregate.Id);
await _cache.SetStringAsync(key, serialized, _cacheOptions, cancellationToken).ConfigureAwait(false);
}

protected virtual string GetCacheKey(TKey id) => $"{typeof(TAggregate).FullName}_{id}";

protected virtual TAggregate DeserializeAggregate(string serialized)
{
CachedAggregate cached = JsonConvert.DeserializeObject<CachedAggregate>(serialized, _jsonSerializerSettings);

if (cached.Snapshot == null)
{
return Activator.CreateInstance(typeof(TAggregate), cached.Id, cached.Events) as TAggregate;
}
else
{
return Activator.CreateInstance(typeof(TAggregate), cached.Id, cached.Snapshot, cached.Events) as TAggregate;
}
}

protected virtual string SerializeAggregate(TAggregate aggregate)
{
var cached = new CachedAggregate
{
Id = aggregate.Id,
Snapshot = aggregate.Snapshot,
Events = aggregate.Events
};
return JsonConvert.SerializeObject(cached, _jsonSerializerSettings);
}

internal class CachedAggregate
{
public TKey Id { get; set; }
public IAggregateSnapshot<TKey> Snapshot { get; set; }
public IEnumerable<IAggregateEvent<TKey>> Events { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>JKang.EventSourcing.Persistence</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="3.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\JKang.EventSourcing.Abstractions\JKang.EventSourcing.Abstractions.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using JKang.EventSourcing.Persistence;
using JKang.EventSourcing.Snapshotting.Persistence;
using Microsoft.Extensions.Caching.Distributed;
using System;
using System.Threading.Tasks;

namespace JKang.EventSourcing.TestingFixtures
{
public class CachedGiftCardRepository : CachedAggregateRepository<GiftCard, Guid>, IGiftCardRepository
{
public CachedGiftCardRepository(
IEventStore<GiftCard, Guid> eventStore,
ISnapshotStore<GiftCard, Guid> snapshotStore,
IDistributedCache cache)
: base(eventStore, snapshotStore, cache, new DistributedCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromSeconds(10)
})
{ }

public Task SaveGiftCardAsync(GiftCard giftCard) => SaveAggregateAsync(giftCard);

public Task<GiftCard> FindGiftCardAsync(Guid id) => FindAggregateAsync(id);

public Task<Guid[]> GetGiftCardIdsAsync() => GetAggregateIdsAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<ItemGroup>
<ProjectReference Include="..\JKang.EventSourcing.Abstractions\JKang.EventSourcing.Abstractions.csproj" />
<ProjectReference Include="..\JKang.EventSourcing.Persistence.Caching\JKang.EventSourcing.Persistence.Caching.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/JKang.EventSourcing.TestingWebApp/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void ConfigureServices(IServiceCollection services)
services.AddRazorPages();

services
.AddScoped<IGiftCardRepository, GiftCardRepository>();
.AddScoped<IGiftCardRepository, CachedGiftCardRepository>();

// change the following value to switch persistence mode
PersistenceMode persistenceMode = PersistenceMode.EfCore;
Expand Down

0 comments on commit bd41d85

Please sign in to comment.