Browse Source

Generic methods improved.

pull/206/head
Sebastian Stehle 9 years ago
parent
commit
9c15648fd3
  1. 3
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs
  2. 15
      src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs
  3. 3
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs
  4. 10
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  5. 12
      src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs
  6. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs
  7. 6
      src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs
  8. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs
  9. 6
      src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs
  10. 16
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  11. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs
  12. 2
      src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs
  13. 2
      src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs
  14. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs
  15. 2
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs
  16. 12
      src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs
  17. 4
      src/Squidex.Infrastructure.MongoDb/States/MongoState.cs
  18. 4
      src/Squidex.Infrastructure/Commands/AggregateHandler.cs
  19. 28
      src/Squidex.Infrastructure/Commands/CommandExtensions.cs
  20. 18
      src/Squidex.Infrastructure/Commands/DomainObjectBase.cs
  21. 3
      src/Squidex.Infrastructure/Commands/IDomainObject.cs
  22. 6
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  23. 4
      src/Squidex.Infrastructure/States/IPersistence.cs
  24. 6
      src/Squidex.Infrastructure/States/ISnapshotStore.cs
  25. 13
      src/Squidex.Infrastructure/States/IStateFactory.cs
  26. 4
      src/Squidex.Infrastructure/States/IStatefulObject.cs
  27. 8
      src/Squidex.Infrastructure/States/IStore.cs
  28. 210
      src/Squidex.Infrastructure/States/Persistence.cs
  29. 232
      src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs
  30. 38
      src/Squidex.Infrastructure/States/StateFactory.cs
  31. 26
      src/Squidex.Infrastructure/States/Store.cs
  32. 32
      src/Squidex.Infrastructure/States/StoreExtensions.cs
  33. 11
      src/Squidex.Infrastructure/Tasks/TaskExtensions.cs
  34. 15
      src/Squidex/Config/Domain/StoreServices.cs
  35. 4
      tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs
  36. 10
      tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs
  37. 8
      tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs
  38. 4
      tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs
  39. 18
      tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs
  40. 38
      tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs
  41. 2
      tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

3
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppEntity.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Apps.State;
@ -18,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
public Guid Id { get; set; }
[BsonElement]
[BsonRequired]

15
src/Squidex.Domain.Apps.Entities.MongoDb/Apps/MongoAppRepository.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
{
public sealed class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, ISnapshotStore<AppState>
public sealed class MongoAppRepository : MongoRepositoryBase<MongoAppEntity>, IAppRepository, ISnapshotStore<AppState, Guid>
{
public MongoAppRepository(IMongoDatabase database)
: base(database)
@ -55,16 +55,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
return appEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
public async Task<IReadOnlyList<string>> QueryUserAppNamesAsync(string userId)
{
var appEntities =
await Collection.Find(x => x.UserIds.Contains(userId)).Project<MongoAppEntity>(Projection.Include(x => x.Id))
.ToListAsync();
return appEntities.Select(x => x.Id).ToList();
}
public async Task<(AppState Value, long Version)> ReadAsync(string key)
public async Task<(AppState Value, long Version)> ReadAsync(Guid key)
{
var existing =
await Collection.Find(x => x.Id == key)
@ -78,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Apps
return (null, EtagVersion.NotFound);
}
public async Task WriteAsync(string key, AppState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, AppState value, long oldVersion, long newVersion)
{
try
{

3
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetEntity.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using Squidex.Domain.Apps.Entities.Assets.State;
@ -17,7 +18,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
public Guid Id { get; set; }
[BsonElement]
[BsonRequired]

10
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -21,7 +21,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
{
public sealed class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository, ISnapshotStore<AssetState>
public sealed class MongoAssetRepository : MongoRepositoryBase<MongoAssetEntity>, IAssetRepository, ISnapshotStore<AssetState, Guid>
{
public MongoAssetRepository(IMongoDatabase database)
: base(database)
@ -43,7 +43,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
.Descending(x => x.State.LastModified));
}
public async Task<(AssetState Value, long Version)> ReadAsync(string key)
public async Task<(AssetState Value, long Version)> ReadAsync(Guid key)
{
var existing =
await Collection.Find(x => x.Id == key)
@ -81,7 +81,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
public async Task<IAssetEntity> FindAssetAsync(Guid id)
{
var (state, etag) = await ReadAsync(id.ToString());
var (state, etag) = await ReadAsync(id);
return state;
}
@ -95,7 +95,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
if (ids != null && ids.Count > 0)
{
filters.Add(Filter.In(x => x.Id, ids.Select(x => x.ToString())));
filters.Add(Filter.In(x => x.Id, ids));
}
if (mimeTypes != null && mimeTypes.Count > 0)
@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
return filter;
}
public async Task WriteAsync(string key, AssetState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, AssetState value, long oldVersion, long newVersion)
{
try
{

12
src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository.cs

@ -31,7 +31,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
public partial class MongoContentRepository : MongoRepositoryBase<MongoContentEntity>,
IEventConsumer,
IContentRepository,
ISnapshotStore<ContentState>
ISnapshotStore<ContentState, Guid>
{
private readonly IAppProvider appProvider;
@ -71,9 +71,9 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
await collection.Indexes.CreateOneAsync(Index.Text(x => x.DataText));
}
public async Task WriteAsync(string key, ContentState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, ContentState value, long oldVersion, long newVersion)
{
var documentId = $"{key}_{oldVersion}";
var documentId = $"{key}_{newVersion}";
var schema = await appProvider.GetSchemaAsync(value.AppId, value.SchemaId);
@ -119,12 +119,10 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Contents
}
}
public async Task<(ContentState Value, long Version)> ReadAsync(string key)
public async Task<(ContentState Value, long Version)> ReadAsync(Guid key)
{
var id = Guid.Parse(key);
var contentEntity =
await Collection.Find(x => x.Id == id && x.IsLatest)
await Collection.Find(x => x.Id == key && x.IsLatest)
.FirstOrDefaultAsync();
if (contentEntity != null)

2
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleEntity.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
public Guid Id { get; set; }
[BsonElement]
[BsonRequired]

6
src/Squidex.Domain.Apps.Entities.MongoDb/Rules/MongoRuleRepository.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
{
public sealed class MongoRuleRepository : MongoRepositoryBase<MongoRuleEntity>, IRuleRepository, ISnapshotStore<RuleState>
public sealed class MongoRuleRepository : MongoRepositoryBase<MongoRuleEntity>, IRuleRepository, ISnapshotStore<RuleState, Guid>
{
public MongoRuleRepository(IMongoDatabase database)
: base(database)
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.IsDeleted));
}
public async Task<(RuleState Value, long Version)> ReadAsync(string key)
public async Task<(RuleState Value, long Version)> ReadAsync(Guid key)
{
var existing =
await Collection.Find(x => x.Id == key)
@ -60,7 +60,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Rules
return ruleEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
public async Task WriteAsync(string key, RuleState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, RuleState value, long oldVersion, long newVersion)
{
try
{

2
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaEntity.cs

@ -19,7 +19,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
public Guid Id { get; set; }
[BsonElement]
[BsonRequired]

6
src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemaRepository.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
public sealed class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISnapshotStore<SchemaState>
public sealed class MongoSchemaRepository : MongoRepositoryBase<MongoSchemaEntity>, ISchemaRepository, ISnapshotStore<SchemaState, Guid>
{
public MongoSchemaRepository(IMongoDatabase database)
: base(database)
@ -37,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
await collection.Indexes.CreateOneAsync(Index.Ascending(x => x.Name));
}
public async Task<(SchemaState Value, long Version)> ReadAsync(string key)
public async Task<(SchemaState Value, long Version)> ReadAsync(Guid key)
{
var existing =
await Collection.Find(x => x.Id == key)
@ -69,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
return schemaEntities.Select(x => Guid.Parse(x["_id"].AsString)).ToList();
}
public async Task WriteAsync(string key, SchemaState value, long oldVersion, long newVersion)
public async Task WriteAsync(Guid key, SchemaState value, long oldVersion, long newVersion)
{
try
{

16
src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -48,14 +48,14 @@ namespace Squidex.Domain.Apps.Entities
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId.ToString());
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId);
if (IsNotFound(app))
{
return (null, null);
}
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString());
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id);
return IsNotFound(false, schema) ? (null, null) : (app.State, schema.State);
}
@ -64,7 +64,7 @@ namespace Squidex.Domain.Apps.Entities
{
var appId = await GetAppIdAsync(appName);
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId.ToString());
var app = await stateFactory.GetSingleAsync<AppDomainObject>(appId);
return IsNotFound(app) ? null : app.State;
}
@ -73,14 +73,14 @@ namespace Squidex.Domain.Apps.Entities
{
var schemaId = await GetSchemaIdAsync(appId, name);
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId.ToString());
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(schemaId);
return IsNotFound(provideDeleted, schema) ? null : schema.State;
}
public async Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool provideDeleted = false)
{
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString());
var schema = await stateFactory.GetSingleAsync<SchemaDomainObject>(id);
return IsNotFound(provideDeleted, schema) ? null : schema.State;
}
@ -91,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities
var schemas =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<SchemaDomainObject>(id.ToString())));
ids.Select(id => stateFactory.GetSingleAsync<SchemaDomainObject>(id)));
return schemas.Select(a => (ISchemaEntity)a.State).ToList();
}
@ -102,7 +102,7 @@ namespace Squidex.Domain.Apps.Entities
var rules =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<RuleDomainObject>(id.ToString())));
ids.Select(id => stateFactory.GetSingleAsync<RuleDomainObject>(id)));
return rules.Select(a => (IRuleEntity)a.State).ToList();
}
@ -113,7 +113,7 @@ namespace Squidex.Domain.Apps.Entities
var apps =
await Task.WhenAll(
ids.Select(id => stateFactory.GetSingleAsync<AppDomainObject>(id.ToString())));
ids.Select(id => stateFactory.GetSingleAsync<AppDomainObject>(id)));
return apps.Select(a => (IAppEntity)a.State).ToList();
}

2
src/Squidex.Domain.Apps.Entities/Apps/AppDomainObject.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Apps
{
public class AppDomainObject : DomainObjectBase<AppDomainObject, AppState>
public class AppDomainObject : DomainObjectBase<AppState>
{
public AppDomainObject Create(CreateApp command)
{

2
src/Squidex.Domain.Apps.Entities/Assets/AssetDomainObject.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Assets
{
public class AssetDomainObject : DomainObjectBase<AssetDomainObject, AssetState>
public class AssetDomainObject : DomainObjectBase<AssetState>
{
public AssetDomainObject Create(CreateAsset command)
{

2
src/Squidex.Domain.Apps.Entities/Contents/ContentDomainObject.cs

@ -17,7 +17,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Contents
{
public class ContentDomainObject : DomainObjectBase<ContentDomainObject, ContentState>
public class ContentDomainObject : DomainObjectBase<ContentState>
{
public ContentDomainObject Create(CreateContent command)
{

2
src/Squidex.Domain.Apps.Entities/Rules/RuleDomainObject.cs

@ -16,7 +16,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Rules
{
public class RuleDomainObject : DomainObjectBase<RuleDomainObject, RuleState>
public class RuleDomainObject : DomainObjectBase<RuleState>
{
public void Create(CreateRule command)
{

2
src/Squidex.Domain.Apps.Entities/Schemas/SchemaDomainObject.cs

@ -19,7 +19,7 @@ using Squidex.Infrastructure.Reflection;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public class SchemaDomainObject : DomainObjectBase<SchemaDomainObject, SchemaState>
public class SchemaDomainObject : DomainObjectBase<SchemaState>
{
private readonly FieldRegistry registry;

12
src/Squidex.Infrastructure.MongoDb/States/MongoSnapshotStore.cs

@ -13,7 +13,7 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States
{
public class MongoSnapshotStore<T> : MongoRepositoryBase<MongoState<T>>, ISnapshotStore<T>, IExternalSystem
public class MongoSnapshotStore<T, TKey> : MongoRepositoryBase<MongoState<T, TKey>>, ISnapshotStore<T, TKey>, IExternalSystem
{
private readonly JsonSerializer serializer;
@ -30,10 +30,10 @@ namespace Squidex.Infrastructure.States
return $"States_{typeof(T).Name}";
}
public async Task<(T Value, long Version)> ReadAsync(string key)
public async Task<(T Value, long Version)> ReadAsync(TKey key)
{
var existing =
await Collection.Find(x => x.Id == key)
await Collection.Find(x => Equals(x.Id, key))
.FirstOrDefaultAsync();
if (existing != null)
@ -44,11 +44,11 @@ namespace Squidex.Infrastructure.States
return (default(T), EtagVersion.NotFound);
}
public async Task WriteAsync(string key, T value, long oldVersion, long newVersion)
public async Task WriteAsync(TKey key, T value, long oldVersion, long newVersion)
{
try
{
await Collection.UpdateOneAsync(x => x.Id == key && x.Version == oldVersion,
await Collection.UpdateOneAsync(x => Equals(x.Id, key) && x.Version == oldVersion,
Update
.Set(x => x.Doc, value)
.Set(x => x.Version, newVersion),
@ -59,7 +59,7 @@ namespace Squidex.Infrastructure.States
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey)
{
var existingVersion =
await Collection.Find(x => x.Id == key).Only(x => x.Id, x => x.Version)
await Collection.Find(x => Equals(x.Id, key)).Only(x => x.Id, x => x.Version)
.FirstOrDefaultAsync();
if (existingVersion != null)

4
src/Squidex.Infrastructure.MongoDb/States/MongoState.cs

@ -12,12 +12,12 @@ using Squidex.Infrastructure.MongoDb;
namespace Squidex.Infrastructure.States
{
public sealed class MongoState<T>
public sealed class MongoState<T, TKey>
{
[BsonId]
[BsonElement]
[BsonRepresentation(BsonType.String)]
public string Id { get; set; }
public TKey Id { get; set; }
[BsonRequired]
[BsonElement]

4
src/Squidex.Infrastructure/Commands/AggregateHandler.cs

@ -67,7 +67,7 @@ namespace Squidex.Infrastructure.Commands
var domainCommand = GetCommand(context);
var domainObjectId = domainCommand.AggregateId;
var domainObject = await stateFactory.CreateAsync<T>(domainObjectId.ToString());
var domainObject = await stateFactory.CreateAsync<T>(domainObjectId);
if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version)
{
@ -102,7 +102,7 @@ namespace Squidex.Infrastructure.Commands
using (await lockPool.LockAsync(Tuple.Create(typeof(T), domainObjectId)))
{
var domainObject = await stateFactory.GetSingleAsync<T>(domainObjectId.ToString());
var domainObject = await stateFactory.GetSingleAsync<T>(domainObjectId);
if (domainCommand.ExpectedVersion != EtagVersion.Any && domainCommand.ExpectedVersion != domainObject.Version)
{

28
src/Squidex.Infrastructure/Commands/CommandExtensions.cs

@ -16,42 +16,22 @@ namespace Squidex.Infrastructure.Commands
{
public static Task CreateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{
return handler.CreateAsync<T>(context, x =>
{
creator(x);
return TaskHelper.Done;
});
return handler.CreateAsync<T>(context, creator.ToAsync());
}
public static Task UpdateAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{
return handler.UpdateAsync<T>(context, x =>
{
updater(x);
return TaskHelper.Done;
});
return handler.UpdateAsync<T>(context, updater.ToAsync());
}
public static Task CreateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> creator) where T : class, IDomainObject
{
return handler.CreateSyncedAsync<T>(context, x =>
{
creator(x);
return TaskHelper.Done;
});
return handler.CreateSyncedAsync<T>(context, creator.ToAsync());
}
public static Task UpdateSyncedAsync<T>(this IAggregateHandler handler, CommandContext context, Action<T> updater) where T : class, IDomainObject
{
return handler.UpdateSyncedAsync<T>(context, x =>
{
updater(x);
return TaskHelper.Done;
});
return handler.UpdateSyncedAsync<T>(context, updater.ToAsync());
}
public static Task HandleAsync(this ICommandMiddleware commandMiddleware, CommandContext context)

18
src/Squidex.Infrastructure/Commands/DomainObjectBase.cs

@ -15,26 +15,26 @@ using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public abstract class DomainObjectBase<TBase, TState> : IDomainObject where TState : IDomainState, new()
public abstract class DomainObjectBase<T> : IDomainObject where T : IDomainState, new()
{
private readonly List<Envelope<IEvent>> uncomittedEvents = new List<Envelope<IEvent>>();
private Guid id;
private TState state;
private IPersistence<TState> persistence;
private T state;
private IPersistence<T> persistence;
public long Version
{
get { return state.Version; }
}
public TState State
public T State
{
get { return state; }
}
protected DomainObjectBase()
{
state = new TState();
state = new T();
state.Version = EtagVersion.Empty;
}
@ -48,11 +48,11 @@ namespace Squidex.Infrastructure.Commands
uncomittedEvents.Clear();
}
public Task ActivateAsync(string key, IStore store)
public Task ActivateAsync(Guid key, IStore<Guid> store)
{
id = Guid.Parse(key);
id = key;
persistence = store.WithSnapshots<TBase, TState>(key, s => state = s);
persistence = store.WithSnapshots<T, Guid>(key, s => state = s);
return persistence.ReadAsync();
}
@ -73,7 +73,7 @@ namespace Squidex.Infrastructure.Commands
uncomittedEvents.Add(@event.To<IEvent>());
}
public void UpdateState(TState newState)
public void UpdateState(T newState)
{
state = newState;
}

3
src/Squidex.Infrastructure/Commands/IDomainObject.cs

@ -6,13 +6,14 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
namespace Squidex.Infrastructure.Commands
{
public interface IDomainObject : IStatefulObject
public interface IDomainObject : IStatefulObject<Guid>
{
long Version { get; }

6
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -15,7 +15,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing.Grains
{
public class EventConsumerGrain : DisposableObjectBase, IStatefulObject, IEventSubscriber
public class EventConsumerGrain : DisposableObjectBase, IStatefulObject<string>, IEventSubscriber
{
private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore;
@ -49,9 +49,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
}
}
public Task ActivateAsync(string key, IStore store)
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithSnapshots<EventConsumerGrain, EventConsumerState>(key, s => state = s);
persistence = store.WithSnapshots<EventConsumerState, string>(key, s => state = s);
return persistence.ReadAsync();
}

4
src/Squidex.Infrastructure/States/IPersistence.cs

@ -12,6 +12,10 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
public interface IPersistence : IPersistence<object>
{
}
public interface IPersistence<TState>
{
long Version { get; }

6
src/Squidex.Infrastructure/States/ISnapshotStore.cs

@ -10,10 +10,10 @@ using System.Threading.Tasks;
namespace Squidex.Infrastructure.States
{
public interface ISnapshotStore<T>
public interface ISnapshotStore<T, TKey>
{
Task WriteAsync(string key, T value, long oldVersion, long newVersion);
Task WriteAsync(TKey key, T value, long oldVersion, long newVersion);
Task<(T Value, long Version)> ReadAsync(string key);
Task<(T Value, long Version)> ReadAsync(TKey key);
}
}

13
src/Squidex.Infrastructure/States/IStateFactory.cs

@ -6,14 +6,23 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.States
{
public interface IStateFactory
{
Task<T> GetSingleAsync<T>(string key) where T : IStatefulObject;
Task<T> GetSingleAsync<T>(string key) where T : IStatefulObject<string>;
Task<T> CreateAsync<T>(string key) where T : IStatefulObject;
Task<T> GetSingleAsync<T>(Guid key) where T : IStatefulObject<Guid>;
Task<T> GetSingleAsync<T, TKey>(TKey key) where T : IStatefulObject<TKey>;
Task<T> CreateAsync<T>(string key) where T : IStatefulObject<string>;
Task<T> CreateAsync<T>(Guid key) where T : IStatefulObject<Guid>;
Task<T> CreateAsync<T, TKey>(TKey key) where T : IStatefulObject<TKey>;
}
}

4
src/Squidex.Infrastructure/States/IStatefulObject.cs

@ -10,8 +10,8 @@ using System.Threading.Tasks;
namespace Squidex.Infrastructure.States
{
public interface IStatefulObject
public interface IStatefulObject<TKey>
{
Task ActivateAsync(string key, IStore store);
Task ActivateAsync(TKey key, IStore<TKey> store);
}
}

8
src/Squidex.Infrastructure/States/IStore.cs

@ -12,12 +12,12 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
public interface IStore
public interface IStore<TKey>
{
IPersistence<object> WithEventSourcing<TOwner>(string key, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence WithEventSourcing(TKey key, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence<TState> WithSnapshots<TOwner, TState>(string key, Func<TState, Task> applySnapshot);
IPersistence<T> WithSnapshots<T>(TKey key, Func<T, Task> applySnapshot);
IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(string key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent);
IPersistence<T> WithSnapshotsAndEventSourcing<T>(TKey key, Func<T, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent);
}
}

210
src/Squidex.Infrastructure/States/Persistence.cs

@ -7,8 +7,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
@ -16,217 +14,17 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
internal sealed class Persistence<TOwner, TState> : IPersistence<TState>
internal sealed class Persistence<TOwner, TKey> : Persistence<TOwner, object, TKey>, IPersistence
{
private readonly string ownerKey;
private readonly ISnapshotStore<TState> snapshotStore;
private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly PersistenceMode persistenceMode;
private readonly Action invalidate;
private readonly Func<TState, Task> applyState;
private readonly Func<Envelope<IEvent>, Task> applyEvent;
private long versionSnapshot = EtagVersion.Empty;
private long versionEvents = EtagVersion.Empty;
private long version;
public long Version
{
get { return version; }
}
public Persistence(string ownerKey,
public Persistence(TKey ownerKey,
Action invalidate,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISnapshotStore<TState> snapshotStore,
ISnapshotStore<object, TKey> snapshotStore,
IStreamNameResolver streamNameResolver,
PersistenceMode persistenceMode,
Func<TState, Task> applyState,
Func<Envelope<IEvent>, Task> applyEvent)
: base(ownerKey, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent)
{
this.ownerKey = ownerKey;
this.applyState = applyState;
this.applyEvent = applyEvent;
this.invalidate = invalidate;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.persistenceMode = persistenceMode;
this.snapshotStore = snapshotStore;
this.streamNameResolver = streamNameResolver;
}
public async Task ReadAsync(long expectedVersion = EtagVersion.Any)
{
versionSnapshot = EtagVersion.Empty;
versionEvents = EtagVersion.Empty;
await ReadSnapshotAsync();
await ReadEventsAsync();
UpdateVersion();
if (expectedVersion != EtagVersion.Any && expectedVersion != version)
{
if (version == EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(ownerKey, typeof(TOwner));
}
else
{
throw new DomainObjectVersionException(ownerKey, typeof(TOwner), version, expectedVersion);
}
}
}
private async Task ReadSnapshotAsync()
{
if (UseSnapshots())
{
var (state, position) = await snapshotStore.ReadAsync(ownerKey);
if (position < EtagVersion.Empty)
{
position = EtagVersion.Empty;
}
versionSnapshot = position;
versionEvents = position;
if (applyState != null && position >= 0)
{
await applyState(state);
}
}
}
private async Task ReadEventsAsync()
{
if (UseEventSourcing())
{
var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1);
foreach (var @event in events)
{
versionEvents++;
if (@event.EventStreamNumber != versionEvents)
{
throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps.");
}
var parsedEvent = ParseKnownEvent(@event);
if (parsedEvent != null && applyEvent != null)
{
await applyEvent(parsedEvent);
}
}
}
}
public async Task WriteSnapshotAsync(TState state)
{
var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1;
if (newVersion != versionSnapshot)
{
try
{
await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion);
}
catch (InconsistentStateException ex)
{
throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
}
versionSnapshot = newVersion;
}
UpdateVersion();
invalidate?.Invoke();
}
public async Task WriteEventsAsync(IEnumerable<Envelope<IEvent>> events)
{
Guard.NotNull(events, nameof(@events));
var eventArray = events.ToArray();
if (eventArray.Length > 0)
{
var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any;
var commitId = Guid.NewGuid();
var eventStream = GetStreamName();
var eventData = GetEventData(eventArray, commitId);
try
{
await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData);
}
catch (WrongEventVersionException ex)
{
throw new DomainObjectVersionException(ownerKey, typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
}
versionEvents += eventArray.Length;
}
UpdateVersion();
invalidate?.Invoke();
}
private EventData[] GetEventData(Envelope<IEvent>[] events, Guid commitId)
{
return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray();
}
private string GetStreamName()
{
return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey);
}
private bool UseSnapshots()
{
return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing;
}
private bool UseEventSourcing()
{
return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing;
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
{
try
{
return eventDataFormatter.Parse(storedEvent.Data);
}
catch (TypeNameNotFoundException)
{
return null;
}
}
private void UpdateVersion()
{
if (persistenceMode == PersistenceMode.Snapshots)
{
version = versionSnapshot;
}
else if (persistenceMode == PersistenceMode.EventSourcing)
{
version = versionEvents;
}
else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing)
{
version = Math.Max(versionEvents, versionSnapshot);
}
}
}
}

232
src/Squidex.Infrastructure/States/Persistence{TOwner,TState,TKey}.cs

@ -0,0 +1,232 @@
// ==========================================================================
// Persistence.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Infrastructure.EventSourcing;
#pragma warning disable RECS0012 // 'if' statement can be re-written as 'switch' statement
namespace Squidex.Infrastructure.States
{
internal class Persistence<TOwner, TState, TKey> : IPersistence<TState>
{
private readonly TKey ownerKey;
private readonly ISnapshotStore<TState, TKey> snapshotStore;
private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly PersistenceMode persistenceMode;
private readonly Action invalidate;
private readonly Func<TState, Task> applyState;
private readonly Func<Envelope<IEvent>, Task> applyEvent;
private long versionSnapshot = EtagVersion.Empty;
private long versionEvents = EtagVersion.Empty;
private long version;
public long Version
{
get { return version; }
}
public Persistence(TKey ownerKey,
Action invalidate,
IEventStore eventStore,
IEventDataFormatter eventDataFormatter,
ISnapshotStore<TState, TKey> snapshotStore,
IStreamNameResolver streamNameResolver,
PersistenceMode persistenceMode,
Func<TState, Task> applyState,
Func<Envelope<IEvent>, Task> applyEvent)
{
this.ownerKey = ownerKey;
this.applyState = applyState;
this.applyEvent = applyEvent;
this.invalidate = invalidate;
this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter;
this.persistenceMode = persistenceMode;
this.snapshotStore = snapshotStore;
this.streamNameResolver = streamNameResolver;
}
public async Task ReadAsync(long expectedVersion = EtagVersion.Any)
{
versionSnapshot = EtagVersion.Empty;
versionEvents = EtagVersion.Empty;
await ReadSnapshotAsync();
await ReadEventsAsync();
UpdateVersion();
if (expectedVersion != EtagVersion.Any && expectedVersion != version)
{
if (version == EtagVersion.Empty)
{
throw new DomainObjectNotFoundException(ownerKey.ToString(), typeof(TOwner));
}
else
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), version, expectedVersion);
}
}
}
private async Task ReadSnapshotAsync()
{
if (UseSnapshots())
{
var (state, position) = await snapshotStore.ReadAsync(ownerKey);
if (position < EtagVersion.Empty)
{
position = EtagVersion.Empty;
}
versionSnapshot = position;
versionEvents = position;
if (applyState != null && position >= 0)
{
await applyState(state);
}
}
}
private async Task ReadEventsAsync()
{
if (UseEventSourcing())
{
var events = await eventStore.GetEventsAsync(GetStreamName(), versionEvents + 1);
foreach (var @event in events)
{
versionEvents++;
if (@event.EventStreamNumber != versionEvents)
{
throw new InvalidOperationException("Events must follow the snapshot version in consecutive order with no gaps.");
}
var parsedEvent = ParseKnownEvent(@event);
if (parsedEvent != null && applyEvent != null)
{
await applyEvent(parsedEvent);
}
}
}
}
public async Task WriteSnapshotAsync(TState state)
{
var newVersion = UseEventSourcing() ? versionEvents : versionSnapshot + 1;
if (newVersion != versionSnapshot)
{
try
{
await snapshotStore.WriteAsync(ownerKey, state, versionSnapshot, newVersion);
}
catch (InconsistentStateException ex)
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
}
versionSnapshot = newVersion;
}
UpdateVersion();
invalidate?.Invoke();
}
public async Task WriteEventsAsync(IEnumerable<Envelope<IEvent>> events)
{
Guard.NotNull(events, nameof(@events));
var eventArray = events.ToArray();
if (eventArray.Length > 0)
{
var expectedVersion = UseEventSourcing() ? version : EtagVersion.Any;
var commitId = Guid.NewGuid();
var eventStream = GetStreamName();
var eventData = GetEventData(eventArray, commitId);
try
{
await eventStore.AppendEventsAsync(commitId, GetStreamName(), expectedVersion, eventData);
}
catch (WrongEventVersionException ex)
{
throw new DomainObjectVersionException(ownerKey.ToString(), typeof(TOwner), ex.CurrentVersion, ex.ExpectedVersion);
}
versionEvents += eventArray.Length;
}
UpdateVersion();
invalidate?.Invoke();
}
private EventData[] GetEventData(Envelope<IEvent>[] events, Guid commitId)
{
return @events.Select(x => eventDataFormatter.ToEventData(x, commitId, true)).ToArray();
}
private string GetStreamName()
{
return streamNameResolver.GetStreamName(typeof(TOwner), ownerKey.ToString());
}
private bool UseSnapshots()
{
return persistenceMode == PersistenceMode.Snapshots || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing;
}
private bool UseEventSourcing()
{
return persistenceMode == PersistenceMode.EventSourcing || persistenceMode == PersistenceMode.SnapshotsAndEventSourcing;
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
{
try
{
return eventDataFormatter.Parse(storedEvent.Data);
}
catch (TypeNameNotFoundException)
{
return null;
}
}
private void UpdateVersion()
{
if (persistenceMode == PersistenceMode.Snapshots)
{
version = versionSnapshot;
}
else if (persistenceMode == PersistenceMode.EventSourcing)
{
version = versionEvents;
}
else if (persistenceMode == PersistenceMode.SnapshotsAndEventSourcing)
{
version = Math.Max(versionEvents, versionSnapshot);
}
}
}
}

38
src/Squidex.Infrastructure/States/StateFactory.cs

@ -27,12 +27,12 @@ namespace Squidex.Infrastructure.States
private readonly object lockObject = new object();
private IDisposable pubSubSubscription;
public sealed class ObjectHolder<T> where T : IStatefulObject
public sealed class ObjectHolder<T, TKey> where T : IStatefulObject<TKey>
{
private readonly Task activationTask;
private readonly T obj;
public ObjectHolder(T obj, string key, IStore store)
public ObjectHolder(T obj, TKey key, IStore<TKey> store)
{
this.obj = obj;
@ -81,11 +81,21 @@ namespace Squidex.Infrastructure.States
});
}
public async Task<T> CreateAsync<T>(string key) where T : IStatefulObject
public Task<T> CreateAsync<T>(string key) where T : IStatefulObject<string>
{
return CreateAsync<T, string>(key);
}
public Task<T> CreateAsync<T>(Guid key) where T : IStatefulObject<Guid>
{
return CreateAsync<T, Guid>(key);
}
public async Task<T> CreateAsync<T, TKey>(TKey key) where T : IStatefulObject<TKey>
{
Guard.NotNull(key, nameof(key));
var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver);
var stateStore = new Store<T, TKey>(eventStore, eventDataFormatter, services, streamNameResolver);
var state = (T)services.GetService(typeof(T));
await state.ActivateAsync(key, stateStore);
@ -93,25 +103,35 @@ namespace Squidex.Infrastructure.States
return state;
}
public Task<T> GetSingleAsync<T>(string key) where T : IStatefulObject
public Task<T> GetSingleAsync<T>(string key) where T : IStatefulObject<string>
{
return GetSingleAsync<T, string>(key);
}
public Task<T> GetSingleAsync<T>(Guid key) where T : IStatefulObject<Guid>
{
return GetSingleAsync<T, Guid>(key);
}
public Task<T> GetSingleAsync<T, TKey>(TKey key) where T : IStatefulObject<TKey>
{
Guard.NotNull(key, nameof(key));
lock (lockObject)
{
if (statesCache.TryGetValue<ObjectHolder<T>>(key, out var stateObj))
if (statesCache.TryGetValue<ObjectHolder<T, TKey>>(key, out var stateObj))
{
return stateObj.ActivateAsync();
}
var state = (T)services.GetService(typeof(T));
var stateStore = new Store(eventStore, eventDataFormatter, services, streamNameResolver, () =>
var stateStore = new Store<T, TKey>(eventStore, eventDataFormatter, services, streamNameResolver, () =>
{
pubSub.Publish(new InvalidateMessage { Key = key }, false);
pubSub.Publish(new InvalidateMessage { Key = key.ToString() }, false);
});
stateObj = new ObjectHolder<T>(state, key, stateStore);
stateObj = new ObjectHolder<T, TKey>(state, key, stateStore);
statesCache.CreateEntry(key)
.SetValue(stateObj)

26
src/Squidex.Infrastructure/States/Store.cs

@ -12,7 +12,7 @@ using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Infrastructure.States
{
public sealed class Store : IStore
internal sealed class Store<TOwner, TKey> : IStore<TKey>
{
private readonly Action invalidate;
private readonly IServiceProvider services;
@ -34,28 +34,32 @@ namespace Squidex.Infrastructure.States
this.streamNameResolver = streamNameResolver;
}
public IPersistence<object> WithEventSourcing<TOwner>(string key, Func<Envelope<IEvent>, Task> applyEvent)
public IPersistence<TState> WithSnapshots<TState>(TKey key, Func<TState, Task> applySnapshot)
{
return CreatePersistence<TOwner, object>(key, PersistenceMode.EventSourcing, null, applyEvent);
return CreatePersistence(key, PersistenceMode.Snapshots, applySnapshot, null);
}
public IPersistence<TState> WithSnapshots<TOwner, TState>(string key, Func<TState, Task> applySnapshot)
public IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
return CreatePersistence<TOwner, TState>(key, PersistenceMode.Snapshots, applySnapshot, null);
return CreatePersistence(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
}
public IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(string key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
public IPersistence WithEventSourcing(TKey key, Func<Envelope<IEvent>, Task> applyEvent)
{
return CreatePersistence<TOwner, TState>(key, PersistenceMode.SnapshotsAndEventSourcing, applySnapshot, applyEvent);
Guard.NotDefault(key, nameof(key));
var snapshotStore = (ISnapshotStore<object, TKey>)services.GetService(typeof(ISnapshotStore<object, TKey>));
return new Persistence<TOwner, TKey>(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent);
}
private IPersistence<TState> CreatePersistence<TOwner, TState>(string key, PersistenceMode mode, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
private IPersistence<TState> CreatePersistence<TState>(TKey key, PersistenceMode mode, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent)
{
Guard.NotNullOrEmpty(key, nameof(key));
Guard.NotDefault(key, nameof(key));
var snapshotStore = (ISnapshotStore<TState>)services.GetService(typeof(ISnapshotStore<TState>));
var snapshotStore = (ISnapshotStore<TState, TKey>)services.GetService(typeof(ISnapshotStore<TState, TKey>));
return new Persistence<TOwner, TState>(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
return new Persistence<TOwner, TState, TKey>(key, invalidate, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
}
}
}

32
src/Squidex.Infrastructure/States/StoreExtensions.cs

@ -14,39 +14,19 @@ namespace Squidex.Infrastructure.States
{
public static class StoreExtensions
{
public static IPersistence<object> WithEventSourcing<TOwner>(this IStore store, string key, Action<Envelope<IEvent>> applyEvent)
public static IPersistence WithEventSourcing<TKey>(this IStore<TKey> store, TKey key, Action<Envelope<IEvent>> applyEvent)
{
return store.WithEventSourcing<TOwner>(key, x =>
{
applyEvent(x);
return TaskHelper.Done;
});
return store.WithEventSourcing(key, applyEvent.ToAsync());
}
public static IPersistence<TState> WithSnapshots<TOwner, TState>(this IStore store, string key, Action<TState> applySnapshot)
public static IPersistence<TState> WithSnapshots<TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot)
{
return store.WithSnapshots<TOwner, TState>(key, x =>
{
applySnapshot(x);
return TaskHelper.Done;
});
return store.WithSnapshots(key, applySnapshot.ToAsync());
}
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TOwner, TState>(this IStore store, string key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
public static IPersistence<TState> WithSnapshotsAndEventSourcing<TState, TKey>(this IStore<TKey> store, TKey key, Action<TState> applySnapshot, Action<Envelope<IEvent>> applyEvent)
{
return store.WithSnapshotsAndEventSourcing<TOwner, TState>(key, x =>
{
applySnapshot(x);
return TaskHelper.Done;
}, x =>
{
applyEvent(x);
return TaskHelper.Done;
});
return store.WithSnapshotsAndEventSourcing(key, applySnapshot.ToAsync(), applyEvent.ToAsync());
}
}
}

11
src/Squidex.Infrastructure/Tasks/TaskExtensions.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Tasks
@ -15,5 +16,15 @@ namespace Squidex.Infrastructure.Tasks
public static void Forget(this Task task)
{
}
public static Func<T, Task> ToAsync<T>(this Action<T> action)
{
return x =>
{
action(x);
return TaskHelper.Done;
};
}
}
}

15
src/Squidex/Config/Domain/StoreServices.cs

@ -6,6 +6,7 @@
// All rights reserved.
// ==========================================================================
using System;
using IdentityServer4.Stores;
using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Identity;
@ -64,8 +65,8 @@ namespace Squidex.Config.Domain
.As<IXmlRepository>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState>>()
services.AddSingletonAs(c => new MongoSnapshotStore<EventConsumerState, string>(mongoDatabase, c.GetRequiredService<JsonSerializer>()))
.As<ISnapshotStore<EventConsumerState, string>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoUserStore(mongoDatabase))
@ -93,27 +94,27 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => new MongoAppRepository(mongoDatabase))
.As<IAppRepository>()
.As<ISnapshotStore<AppState>>()
.As<ISnapshotStore<AppState, Guid>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoAssetRepository(mongoDatabase))
.As<IAssetRepository>()
.As<ISnapshotStore<AssetState>>()
.As<ISnapshotStore<AssetState, Guid>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoRuleRepository(mongoContentDatabase))
.As<IRuleRepository>()
.As<ISnapshotStore<RuleState>>()
.As<ISnapshotStore<RuleState, Guid>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoSchemaRepository(mongoDatabase))
.As<ISchemaRepository>()
.As<ISnapshotStore<SchemaState>>()
.As<ISnapshotStore<SchemaState, Guid>>()
.As<IExternalSystem>();
services.AddSingletonAs(c => new MongoContentRepository(mongoContentDatabase, c.GetService<IAppProvider>()))
.As<IContentRepository>()
.As<ISnapshotStore<ContentState>>()
.As<ISnapshotStore<ContentState, Guid>>()
.As<IEventConsumer>()
.As<IExternalSystem>();

4
tests/Squidex.Domain.Apps.Entities.Tests/TestHelpers/HandlerTestBase.cs

@ -106,7 +106,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
{
handler.Init(domainObject);
await domainObject.ActivateAsync(Id.ToString(), A.Fake<IStore>());
await domainObject.ActivateAsync(Id, A.Fake<IStore<Guid>>());
await action(domainObject);
if (!handler.IsCreated && shouldCreate)
@ -119,7 +119,7 @@ namespace Squidex.Domain.Apps.Entities.TestHelpers
{
handler.Init(domainObject);
await domainObject.ActivateAsync(Id.ToString(), A.Fake<IStore>());
await domainObject.ActivateAsync(Id, A.Fake<IStore<Guid>>());
await action(domainObject);
if (!handler.IsUpdated && shouldUpdate)

10
tests/Squidex.Infrastructure.Tests/Commands/AggregateHandlerTests.cs

@ -23,7 +23,7 @@ namespace Squidex.Infrastructure.Commands
{
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IServiceProvider serviceProvider = A.Fake<IServiceProvider>();
private readonly IStore store = A.Fake<IStore>();
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IStateFactory stateFactory = A.Fake<IStateFactory>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Envelope<IEvent> event1 = new Envelope<IEvent>(new MyEvent());
@ -40,18 +40,18 @@ namespace Squidex.Infrastructure.Commands
command = new MyCommand { AggregateId = domainObjectId, ExpectedVersion = EtagVersion.Any };
context = new CommandContext(command);
A.CallTo(() => store.WithSnapshots<MyDomainObject, MyDomainState>(domainObjectId.ToString(), A<Func<MyDomainState, Task>>.Ignored))
A.CallTo(() => store.WithSnapshots(domainObjectId, A<Func<MyDomainState, Task>>.Ignored))
.Returns(persistence);
A.CallTo(() => stateFactory.CreateAsync<MyDomainObject>(domainObjectId.ToString()))
A.CallTo(() => stateFactory.CreateAsync<MyDomainObject>(domainObjectId))
.Returns(Task.FromResult(domainObject));
A.CallTo(() => stateFactory.GetSingleAsync<MyDomainObject>(domainObjectId.ToString()))
A.CallTo(() => stateFactory.GetSingleAsync<MyDomainObject>(domainObjectId))
.Returns(Task.FromResult(domainObject));
sut = new AggregateHandler(stateFactory, serviceProvider, log);
domainObject.ActivateAsync(domainObjectId.ToString(), store).Wait();
domainObject.ActivateAsync(domainObjectId, store).Wait();
}
[Fact]

8
tests/Squidex.Infrastructure.Tests/Commands/DomainObjectBaseTests.cs

@ -21,14 +21,14 @@ namespace Squidex.Infrastructure.Commands
{
public class DomainObjectBaseTests
{
private readonly IStore store = A.Fake<IStore>();
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>();
private readonly IPersistence<MyDomainState> persistence = A.Fake<IPersistence<MyDomainState>>();
private readonly Guid id = Guid.NewGuid();
private readonly MyDomainObject sut = new MyDomainObject();
public DomainObjectBaseTests()
{
A.CallTo(() => store.WithSnapshots<MyDomainObject, MyDomainState>(id.ToString(), A<Func<MyDomainState, Task>>.Ignored))
A.CallTo(() => store.WithSnapshots<MyDomainState>(id, A<Func<MyDomainState, Task>>.Ignored))
.Returns(persistence);
}
@ -58,7 +58,7 @@ namespace Squidex.Infrastructure.Commands
[Fact]
public async Task Should_write_state_and_events_when_saved()
{
await sut.ActivateAsync(id.ToString(), store);
await sut.ActivateAsync(id, store);
var event1 = new MyEvent();
var event2 = new MyEvent();
@ -84,7 +84,7 @@ namespace Squidex.Infrastructure.Commands
A.CallTo(() => persistence.WriteEventsAsync(A<IEnumerable<Envelope<IEvent>>>.Ignored))
.Throws(new InvalidOperationException());
await sut.ActivateAsync(id.ToString(), store);
await sut.ActivateAsync(id, store);
var event1 = new MyEvent();
var event2 = new MyEvent();

4
tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs

@ -38,7 +38,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>();
private readonly IPersistence<EventConsumerState> persistence = A.Fake<IPersistence<EventConsumerState>>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IStore store = A.Fake<IStore>();
private readonly IStore<string> store = A.Fake<IStore<string>>();
private readonly IEventDataFormatter formatter = A.Fake<IEventDataFormatter>();
private readonly EventData eventData = new EventData();
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent());
@ -54,7 +54,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
consumerName = eventConsumer.GetType().Name;
A.CallTo(() => store.WithSnapshots<EventConsumerGrain, EventConsumerState>(consumerName, A<Func<EventConsumerState, Task>>.Ignored))
A.CallTo(() => store.WithSnapshots(consumerName, A<Func<EventConsumerState, Task>>.Ignored))
.Invokes(new Action<string, Func<EventConsumerState, Task>>((key, a) => apply = a))
.Returns(persistence);

18
tests/Squidex.Infrastructure.Tests/States/StateEventSourcingTests.cs

@ -22,10 +22,10 @@ namespace Squidex.Infrastructure.States
{
public class StateEventSourcingTests
{
private class MyStatefulObject : IStatefulObject
private class MyStatefulObject : IStatefulObject<string>
{
private readonly List<IEvent> appliedEvents = new List<IEvent>();
private IPersistence<object> persistence;
private IPersistence persistence;
public long ExpectedVersion { get; set; }
@ -34,9 +34,9 @@ namespace Squidex.Infrastructure.States
get { return appliedEvents; }
}
public Task ActivateAsync(string key, IStore store)
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithEventSourcing<MyStatefulObject>(key, e => appliedEvents.Add(e.Payload));
persistence = store.WithEventSourcing(key, e => appliedEvents.Add(e.Payload));
return persistence.ReadAsync(ExpectedVersion);
}
@ -47,15 +47,15 @@ namespace Squidex.Infrastructure.States
}
}
private class MyStatefulObjectWithSnapshot : IStatefulObject
private class MyStatefulObjectWithSnapshot : IStatefulObject<string>
{
private IPersistence<object> persistence;
public long ExpectedVersion { get; set; }
public Task ActivateAsync(string key, IStore store)
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithSnapshotsAndEventSourcing<MyStatefulObject, object>(key, s => TaskHelper.Done, s => TaskHelper.Done);
persistence = store.WithSnapshotsAndEventSourcing<object>(key, s => TaskHelper.Done, s => TaskHelper.Done);
return persistence.ReadAsync(ExpectedVersion);
}
@ -69,7 +69,7 @@ namespace Squidex.Infrastructure.States
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<object> snapshotStore = A.Fake<ISnapshotStore<object>>();
private readonly ISnapshotStore<object, string> snapshotStore = A.Fake<ISnapshotStore<object, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
@ -79,7 +79,7 @@ namespace Squidex.Infrastructure.States
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(MyStatefulObjectWithSnapshot)))
.Returns(statefulObjectWithSnapShot);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<object>)))
A.CallTo(() => services.GetService(typeof(ISnapshotStore<object, string>)))
.Returns(snapshotStore);
A.CallTo(() => streamNameResolver.GetStreamName(typeof(MyStatefulObject), key))

38
tests/Squidex.Infrastructure.Tests/States/StateSnapshotTests.cs

@ -21,7 +21,7 @@ namespace Squidex.Infrastructure.States
{
public class StateSnapshotTests : IDisposable
{
private class MyStatefulObject : IStatefulObject
private class MyStatefulObject : IStatefulObject<string>
{
private IPersistence<int> persistence;
private int state;
@ -38,9 +38,9 @@ namespace Squidex.Infrastructure.States
get { return state; }
}
public Task ActivateAsync(string key, IStore store)
public Task ActivateAsync(string key, IStore<string> store)
{
persistence = store.WithSnapshots<MyStatefulObject, int>(key, s => state = s);
persistence = store.WithSnapshots<int, string>(key, s => state = s);
return persistence.ReadAsync(ExpectedVersion);
}
@ -63,7 +63,7 @@ namespace Squidex.Infrastructure.States
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<int> snapshotStore = A.Fake<ISnapshotStore<int>>();
private readonly ISnapshotStore<int, string> snapshotStore = A.Fake<ISnapshotStore<int, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
private readonly StateFactory sut;
@ -71,7 +71,7 @@ namespace Squidex.Infrastructure.States
{
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.Returns(statefulObject);
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int>)))
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int, string>)))
.Returns(snapshotStore);
sut = new StateFactory(pubSub, cache, eventStore, eventDataFormatter, services, streamNameResolver);
@ -91,7 +91,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 1));
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -107,7 +107,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, EtagVersion.NotFound));
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
Assert.Equal(-1, statefulObject.Version);
Assert.Equal( 0, statefulObject.State);
@ -121,7 +121,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, EtagVersion.Empty));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectNotFoundException>(() => sut.GetSingleAsync<MyStatefulObject, string>(key));
}
[Fact]
@ -132,7 +132,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((2, 2));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject>(key));
await Assert.ThrowsAsync<DomainObjectVersionException>(() => sut.GetSingleAsync<MyStatefulObject, string>(key));
}
[Fact]
@ -143,7 +143,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((0, EtagVersion.Empty));
await sut.GetSingleAsync<MyStatefulObject>(key);
await sut.GetSingleAsync<MyStatefulObject, string>(key);
}
[Fact]
@ -151,7 +151,7 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = EtagVersion.Any;
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
Assert.Same(statefulObject, actualObject);
Assert.NotNull(cache.Get<object>(key));
@ -162,12 +162,12 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = EtagVersion.Any;
var actualObject1 = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject1 = await sut.GetSingleAsync<MyStatefulObject, string>(key);
Assert.Same(statefulObject, actualObject1);
Assert.NotNull(cache.Get<object>(key));
var actualObject2 = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject2 = await sut.GetSingleAsync<MyStatefulObject, string>(key);
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.MustHaveHappened(Repeated.Exactly.Once);
@ -178,12 +178,12 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = EtagVersion.Any;
var actualObject1 = await sut.CreateAsync<MyStatefulObject>(key);
var actualObject1 = await sut.CreateAsync<MyStatefulObject, string>(key);
Assert.Same(statefulObject, actualObject1);
Assert.Null(cache.Get<object>(key));
var actualObject2 = await sut.CreateAsync<MyStatefulObject>(key);
var actualObject2 = await sut.CreateAsync<MyStatefulObject, string>(key);
A.CallTo(() => services.GetService(typeof(MyStatefulObject)))
.MustHaveHappened(Repeated.Exactly.Twice);
@ -204,7 +204,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.ReadAsync(key))
.Returns((123, 13));
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
Assert.Same(statefulObject, actualObject);
Assert.Equal(123, statefulObject.State);
@ -231,7 +231,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => snapshotStore.WriteAsync(key, 123, 13, 14))
.Throws(new InconsistentStateException(1, 1, new InvalidOperationException()));
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
await Assert.ThrowsAsync<DomainObjectVersionException>(() => statefulObject.WriteStateAsync());
}
@ -241,7 +241,7 @@ namespace Squidex.Infrastructure.States
{
statefulObject.ExpectedVersion = EtagVersion.Any;
var actualObject = await sut.GetSingleAsync<MyStatefulObject>(key);
var actualObject = await sut.GetSingleAsync<MyStatefulObject, string>(key);
await InvalidateCacheAsync();
@ -260,7 +260,7 @@ namespace Squidex.Infrastructure.States
for (var i = 0; i < 1000; i++)
{
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject>(key)));
tasks.Add(Task.Run(() => sut.GetSingleAsync<MyStatefulObject, string>(key)));
}
var retrievedStates = await Task.WhenAll(tasks);

2
tests/Squidex.Infrastructure.Tests/TestHelpers/MyDomainObject.cs

@ -10,7 +10,7 @@ using Squidex.Infrastructure.Commands;
namespace Squidex.Infrastructure.TestHelpers
{
internal sealed class MyDomainObject : DomainObjectBase<MyDomainObject, MyDomainState>
internal sealed class MyDomainObject : DomainObjectBase<MyDomainState>
{
}
}

Loading…
Cancel
Save