Browse Source

Improved event handling.

pull/1/head
Sebastian 9 years ago
parent
commit
08233b2a78
  1. 2
      src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs
  2. 53
      src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs
  3. 43
      src/Squidex.Infrastructure/CQRS/Events/Envelope.cs
  4. 65
      src/Squidex.Infrastructure/CQRS/Events/Envelope_1.cs
  5. 2
      src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs
  6. 2
      src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs
  7. 3
      src/Squidex.Infrastructure/Timers/CompletionTimer.cs
  8. 46
      src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs
  9. 9
      src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs
  10. 5
      src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs
  11. 25
      src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs
  12. 3
      src/Squidex.Read/Apps/Repositories/IAppRepository.cs
  13. 3
      src/Squidex.Read/Apps/Services/IAppProvider.cs
  14. 36
      src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs
  15. 3
      src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs
  16. 3
      src/Squidex.Read/Schemas/Services/ISchemaProvider.cs
  17. 35
      src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs
  18. 2
      src/Squidex.Write/Apps/AppDomainObject.cs
  19. 2
      src/Squidex/Config/Domain/ReadModule.cs
  20. 25
      src/Squidex/Config/Domain/StoreMongoDbModule.cs
  21. 18
      src/Squidex/Config/Domain/Usages.cs
  22. 71
      tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs
  23. 1
      tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs
  24. 6
      tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs
  25. 6
      tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs

2
src/Squidex.Infrastructure/CQRS/DomainObjectBase.cs

@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.CQRS
protected void RaiseEvent(IEvent @event) protected void RaiseEvent(IEvent @event)
{ {
RaiseEvent(Envelope<IEvent>.Create(@event)); RaiseEvent(Envelope.Create(@event));
} }
protected void RaiseEvent<TEvent>(Envelope<TEvent> @event) where TEvent : class, IEvent protected void RaiseEvent<TEvent>(Envelope<TEvent> @event) where TEvent : class, IEvent

53
src/Squidex.Infrastructure/CQRS/Events/CompoundEventConsumer.cs

@ -0,0 +1,53 @@
// ==========================================================================
// CompoundEventConsumer.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Linq;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.CQRS.Events
{
public sealed class CompoundEventConsumer : IEventConsumer
{
private readonly IEventConsumer[] inners;
public string Name { get; }
public CompoundEventConsumer(IEventConsumer first, params IEventConsumer[] inners)
{
Guard.NotNull(first, nameof(first));
Guard.NotNull(inners, nameof(inners));
this.inners = new[] { first }.Union(inners).ToArray();
Name = first.GetType().Name;
}
public CompoundEventConsumer(string name, params IEventConsumer[] inners)
{
Guard.NotNull(inners, nameof(inners));
Guard.NotNullOrEmpty(name, nameof(name));
this.inners = inners;
Name = name;
}
public Task ClearAsync()
{
return Task.WhenAll(inners.Select(i => i.ClearAsync()));
}
public async Task On(Envelope<IEvent> @event)
{
foreach (var inner in inners)
{
await inner.On(@event);
}
}
}
}

43
src/Squidex.Infrastructure/CQRS/Events/Envelope.cs

@ -11,55 +11,18 @@ using NodaTime;
namespace Squidex.Infrastructure.CQRS.Events namespace Squidex.Infrastructure.CQRS.Events
{ {
public class Envelope<TPayload> where TPayload : class public static class Envelope
{ {
private readonly EnvelopeHeaders headers; public static Envelope<IEvent> Create<TPayload>(TPayload payload) where TPayload : IEvent
private readonly TPayload payload;
public EnvelopeHeaders Headers
{
get { return headers; }
}
public TPayload Payload
{
get { return payload; }
}
public Envelope(TPayload payload)
: this(payload, new EnvelopeHeaders())
{
}
public Envelope(TPayload payload, PropertiesBag bag)
: this(payload, new EnvelopeHeaders(bag))
{
}
public Envelope(TPayload payload, EnvelopeHeaders headers)
{
Guard.NotNull(payload, nameof(payload));
Guard.NotNull(headers, nameof(headers));
this.payload = payload;
this.headers = headers;
}
public static Envelope<TPayload> Create(TPayload payload)
{ {
var eventId = Guid.NewGuid(); var eventId = Guid.NewGuid();
var envelope = var envelope =
new Envelope<TPayload>(payload) new Envelope<IEvent>(payload)
.SetEventId(eventId) .SetEventId(eventId)
.SetTimestamp(SystemClock.Instance.GetCurrentInstant()); .SetTimestamp(SystemClock.Instance.GetCurrentInstant());
return envelope; return envelope;
} }
public Envelope<TOther> To<TOther>() where TOther : class
{
return new Envelope<TOther>(payload as TOther, headers.Clone());
}
} }
} }

65
src/Squidex.Infrastructure/CQRS/Events/Envelope_1.cs

@ -0,0 +1,65 @@
// ==========================================================================
// Envelope.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System;
using NodaTime;
namespace Squidex.Infrastructure.CQRS.Events
{
public class Envelope<TPayload> where TPayload : class
{
private readonly EnvelopeHeaders headers;
private readonly TPayload payload;
public EnvelopeHeaders Headers
{
get { return headers; }
}
public TPayload Payload
{
get { return payload; }
}
public Envelope(TPayload payload)
: this(payload, new EnvelopeHeaders())
{
}
public Envelope(TPayload payload, PropertiesBag bag)
: this(payload, new EnvelopeHeaders(bag))
{
}
public Envelope(TPayload payload, EnvelopeHeaders headers)
{
Guard.NotNull(payload, nameof(payload));
Guard.NotNull(headers, nameof(headers));
this.payload = payload;
this.headers = headers;
}
public static Envelope<TPayload> Create(TPayload payload)
{
var eventId = Guid.NewGuid();
var envelope =
new Envelope<TPayload>(payload)
.SetEventId(eventId)
.SetTimestamp(SystemClock.Instance.GetCurrentInstant());
return envelope;
}
public Envelope<TOther> To<TOther>() where TOther : class
{
return new Envelope<TOther>(payload as TOther, headers.Clone());
}
}
}

2
src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs

@ -76,7 +76,7 @@ namespace Squidex.Infrastructure.CQRS.Events
return; return;
} }
var consumerName = eventConsumer.GetType().Name; var consumerName = eventConsumer.Name;
var consumerStarted = false; var consumerStarted = false;
timer = new CompletionTimer(delay, async ct => timer = new CompletionTimer(delay, async ct =>

2
src/Squidex.Infrastructure/CQRS/Events/IEventConsumer.cs

@ -12,6 +12,8 @@ namespace Squidex.Infrastructure.CQRS.Events
{ {
public interface IEventConsumer public interface IEventConsumer
{ {
string Name { get; }
Task ClearAsync(); Task ClearAsync();
Task On(Envelope<IEvent> @event); Task On(Envelope<IEvent> @event);

3
src/Squidex.Infrastructure/Timers/CompletionTimer.cs

@ -7,6 +7,7 @@
// ========================================================================== // ==========================================================================
using System; using System;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -45,7 +46,7 @@ namespace Squidex.Infrastructure.Timers
} }
catch (TaskCanceledException) catch (TaskCanceledException)
{ {
Console.WriteLine("Task in TriggerTimer has been cancelled."); Debug.WriteLine("Task in TriggerTimer has been cancelled.");
} }
} }
} }

46
src/Squidex.Read.MongoDb/Apps/MongoAppRepository_EventHandling.cs

@ -6,9 +6,7 @@
// All rights reserved. // All rights reserved.
// ========================================================================== // ==========================================================================
using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Events;
using Squidex.Events.Apps; using Squidex.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
@ -20,36 +18,27 @@ namespace Squidex.Read.MongoDb.Apps
{ {
public partial class MongoAppRepository public partial class MongoAppRepository
{ {
public event Action<NamedId<Guid>> AppSaved; public string Name
{
get { return GetType().Name; }
}
public Task On(Envelope<IEvent> @event) public Task On(Envelope<IEvent> @event)
{ {
return this.DispatchActionAsync(@event.Payload, @event.Headers); return this.DispatchActionAsync(@event.Payload, @event.Headers);
} }
protected async Task On(AppCreated @event, EnvelopeHeaders headers) protected Task On(AppCreated @event, EnvelopeHeaders headers)
{ {
await Collection.CreateAsync(@event, headers, a => return Collection.CreateAsync(@event, headers, a =>
{ {
SimpleMapper.Map(@event, a); SimpleMapper.Map(@event, a);
}); });
AppSaved?.Invoke(@event.AppId);
}
protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{
return UpdateAsync(@event, headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
SimpleMapper.Map(@event, contributor);
});
} }
protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers) protected Task On(AppContributorRemoved @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Contributors.Remove(@event.ContributorId); a.Contributors.Remove(@event.ContributorId);
}); });
@ -57,7 +46,7 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppClientAttached @event, EnvelopeHeaders headers) protected Task On(AppClientAttached @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppClientEntity()); a.Clients[@event.Id] = SimpleMapper.Map(@event, new MongoAppClientEntity());
}); });
@ -65,7 +54,7 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppClientRevoked @event, EnvelopeHeaders headers) protected Task On(AppClientRevoked @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Clients.Remove(@event.Id); a.Clients.Remove(@event.Id);
}); });
@ -73,7 +62,7 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppClientRenamed @event, EnvelopeHeaders headers) protected Task On(AppClientRenamed @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Clients[@event.Id].Name = @event.Name; a.Clients[@event.Id].Name = @event.Name;
}); });
@ -81,7 +70,7 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers) protected Task On(AppLanguageAdded @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Languages.Add(@event.Language.Iso2Code); a.Languages.Add(@event.Language.Iso2Code);
}); });
@ -89,7 +78,7 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers) protected Task On(AppLanguageRemoved @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.Languages.Remove(@event.Language.Iso2Code); a.Languages.Remove(@event.Language.Iso2Code);
}); });
@ -97,17 +86,20 @@ namespace Squidex.Read.MongoDb.Apps
protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers) protected Task On(AppMasterLanguageSet @event, EnvelopeHeaders headers)
{ {
return UpdateAsync(@event, headers, a => return Collection.UpdateAsync(@event, headers, a =>
{ {
a.MasterLanguage = @event.Language.Iso2Code; a.MasterLanguage = @event.Language.Iso2Code;
}); });
} }
public async Task UpdateAsync(AppEvent @event, EnvelopeHeaders headers, Action<MongoAppEntity> updater) protected Task On(AppContributorAssigned @event, EnvelopeHeaders headers)
{ {
await Collection.UpdateAsync(@event, headers, updater); return Collection.UpdateAsync(@event, headers, a =>
{
var contributor = a.Contributors.GetOrAddNew(@event.ContributorId);
AppSaved?.Invoke(@event.AppId); SimpleMapper.Map(@event, contributor);
});
} }
} }
} }

9
src/Squidex.Read.MongoDb/Contents/MongoContentRepository_EventHandling.cs

@ -32,6 +32,11 @@ namespace Squidex.Read.MongoDb.Contents
} }
} }
public string Name
{
get { return GetType().Name; }
}
public async Task ClearAsync() public async Task ClearAsync()
{ {
using (var collections = await database.ListCollectionsAsync()) using (var collections = await database.ListCollectionsAsync())
@ -127,11 +132,11 @@ namespace Squidex.Read.MongoDb.Contents
}); });
} }
private async Task ForSchemaIdAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Task> action) private Task ForSchemaIdAsync(Guid schemaId, Func<IMongoCollection<MongoContentEntity>, Task> action)
{ {
var collection = GetCollection(schemaId); var collection = GetCollection(schemaId);
await action(collection); return action(collection);
} }
private IMongoCollection<MongoContentEntity> GetCollection(Guid schemaId) private IMongoCollection<MongoContentEntity> GetCollection(Guid schemaId)

5
src/Squidex.Read.MongoDb/History/MongoHistoryEventRepository.cs

@ -67,6 +67,11 @@ namespace Squidex.Read.MongoDb.History
return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList(); return entities.Select(x => (IHistoryEventEntity)new ParsedHistoryEvent(x, texts)).ToList();
} }
public string Name
{
get { return GetType().Name; }
}
public async Task On(Envelope<IEvent> @event) public async Task On(Envelope<IEvent> @event)
{ {
foreach (var creator in creators) foreach (var creator in creators)

25
src/Squidex.Read.MongoDb/Schemas/MongoSchemaRepository_EventHandling.cs

@ -8,12 +8,10 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Core.Schemas; using Squidex.Core.Schemas;
using Squidex.Events; using Squidex.Events;
using Squidex.Events.Schemas; using Squidex.Events.Schemas;
using Squidex.Events.Schemas.Utils; using Squidex.Events.Schemas.Utils;
using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Dispatching; using Squidex.Infrastructure.Dispatching;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
@ -23,20 +21,21 @@ namespace Squidex.Read.MongoDb.Schemas
{ {
public partial class MongoSchemaRepository public partial class MongoSchemaRepository
{ {
public event Action<NamedId<Guid>, NamedId<Guid>> SchemaSaved; public string Name
{
get { return GetType().Name; }
}
public Task On(Envelope<IEvent> @event) public Task On(Envelope<IEvent> @event)
{ {
return this.DispatchActionAsync(@event.Payload, @event.Headers); return this.DispatchActionAsync(@event.Payload, @event.Headers);
} }
protected async Task On(SchemaCreated @event, EnvelopeHeaders headers) protected Task On(SchemaCreated @event, EnvelopeHeaders headers)
{ {
var schema = SchemaEventDispatcher.Dispatch(@event); var schema = SchemaEventDispatcher.Dispatch(@event);
await Collection.CreateAsync(@event, headers, s => { UpdateSchema(s, schema); SimpleMapper.Map(@event, s); }); return Collection.CreateAsync(@event, headers, s => { UpdateSchema(s, schema); SimpleMapper.Map(@event, s); });
SchemaSaved?.Invoke(@event.AppId, @event.SchemaId);
} }
protected Task On(FieldDeleted @event, EnvelopeHeaders headers) protected Task On(FieldDeleted @event, EnvelopeHeaders headers)
@ -89,18 +88,14 @@ namespace Squidex.Read.MongoDb.Schemas
return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry)); return UpdateSchema(@event, headers, s => SchemaEventDispatcher.Dispatch(@event, s, registry));
} }
protected async Task On(SchemaDeleted @event, EnvelopeHeaders headers) protected Task On(SchemaDeleted @event, EnvelopeHeaders headers)
{ {
await Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true); return Collection.UpdateAsync(@event, headers, e => e.IsDeleted = true);
SchemaSaved?.Invoke(@event.AppId, @event.SchemaId);
} }
private async Task UpdateSchema(SchemaEvent @event, EnvelopeHeaders headers, Func<Schema, Schema> updater) private Task UpdateSchema(SquidexEvent @event, EnvelopeHeaders headers, Func<Schema, Schema> updater)
{ {
await Collection.UpdateAsync(@event, headers, e => UpdateSchema(e, updater)); return Collection.UpdateAsync(@event, headers, e => UpdateSchema(e, updater));
SchemaSaved?.Invoke(@event.AppId, @event.SchemaId);
} }
private void UpdateSchema(MongoSchemaEntity entity, Func<Schema, Schema> updater) private void UpdateSchema(MongoSchemaEntity entity, Func<Schema, Schema> updater)

3
src/Squidex.Read/Apps/Repositories/IAppRepository.cs

@ -9,14 +9,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read.Apps.Repositories namespace Squidex.Read.Apps.Repositories
{ {
public interface IAppRepository public interface IAppRepository
{ {
event Action<NamedId<Guid>> AppSaved;
Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId); Task<IReadOnlyList<IAppEntity>> QueryAllAsync(string subjectId);
Task<IAppEntity> FindAppAsync(Guid appId); Task<IAppEntity> FindAppAsync(Guid appId);

3
src/Squidex.Read/Apps/Services/IAppProvider.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read.Apps.Services namespace Squidex.Read.Apps.Services
{ {
@ -17,7 +16,5 @@ namespace Squidex.Read.Apps.Services
Task<IAppEntity> FindAppByIdAsync(Guid id); Task<IAppEntity> FindAppByIdAsync(Guid id);
Task<IAppEntity> FindAppByNameAsync(string name); Task<IAppEntity> FindAppByNameAsync(string name);
void Remove(NamedId<Guid> id);
} }
} }

36
src/Squidex.Read/Apps/Services/Implementations/CachingAppProvider.cs

@ -9,8 +9,12 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Events;
using Squidex.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Utils; using Squidex.Read.Utils;
@ -18,11 +22,16 @@ using Squidex.Read.Utils;
namespace Squidex.Read.Apps.Services.Implementations namespace Squidex.Read.Apps.Services.Implementations
{ {
public class CachingAppProvider : CachingProviderBase, IAppProvider public class CachingAppProvider : CachingProviderBase, IAppProvider, IEventConsumer
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(30);
private readonly IAppRepository repository; private readonly IAppRepository repository;
public string Name
{
get { return GetType().Name; }
}
public CachingAppProvider(IMemoryCache cache, IAppRepository repository) public CachingAppProvider(IMemoryCache cache, IAppRepository repository)
: base(cache) : base(cache)
{ {
@ -71,7 +80,9 @@ namespace Squidex.Read.Apps.Services.Implementations
return result; return result;
} }
public void Remove(NamedId<Guid> id) public Task On(Envelope<IEvent> @event)
{
void Remove(NamedId<Guid> id)
{ {
var cacheKeyById = BuildIdCacheKey(id.Id); var cacheKeyById = BuildIdCacheKey(id.Id);
var cacheKeyByName = BuildNameCacheKey(id.Name); var cacheKeyByName = BuildNameCacheKey(id.Name);
@ -83,6 +94,22 @@ namespace Squidex.Read.Apps.Services.Implementations
Cache.Invalidate(cacheKeyByName); Cache.Invalidate(cacheKeyByName);
} }
if (@event.Payload is AppClientAttached ||
@event.Payload is AppClientRenamed ||
@event.Payload is AppClientRevoked ||
@event.Payload is AppContributorAssigned ||
@event.Payload is AppContributorRemoved ||
@event.Payload is AppCreated ||
@event.Payload is AppLanguageAdded ||
@event.Payload is AppLanguageRemoved ||
@event.Payload is AppMasterLanguageSet)
{
Remove(((AppEvent)@event.Payload).AppId);
}
return TaskHelper.Done;
}
private static string BuildNameCacheKey(string name) private static string BuildNameCacheKey(string name)
{ {
return $"App_Ids_{name}"; return $"App_Ids_{name}";
@ -92,5 +119,10 @@ namespace Squidex.Read.Apps.Services.Implementations
{ {
return $"App_Names_{schemaId}"; return $"App_Names_{schemaId}";
} }
public Task ClearAsync()
{
return TaskHelper.Done;
}
} }
} }

3
src/Squidex.Read/Schemas/Repositories/ISchemaRepository.cs

@ -9,14 +9,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read.Schemas.Repositories namespace Squidex.Read.Schemas.Repositories
{ {
public interface ISchemaRepository public interface ISchemaRepository
{ {
event Action<NamedId<Guid>, NamedId<Guid>> SchemaSaved;
Task<IReadOnlyList<ISchemaEntity>> QueryAllAsync(Guid appId); Task<IReadOnlyList<ISchemaEntity>> QueryAllAsync(Guid appId);
Task<IReadOnlyList<ISchemaEntityWithSchema>> QueryAllWithSchemaAsync(Guid appId); Task<IReadOnlyList<ISchemaEntityWithSchema>> QueryAllWithSchemaAsync(Guid appId);

3
src/Squidex.Read/Schemas/Services/ISchemaProvider.cs

@ -8,7 +8,6 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
namespace Squidex.Read.Schemas.Services namespace Squidex.Read.Schemas.Services
{ {
@ -17,7 +16,5 @@ namespace Squidex.Read.Schemas.Services
Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid id, bool provideDeleted = false); Task<ISchemaEntityWithSchema> FindSchemaByIdAsync(Guid id, bool provideDeleted = false);
Task<ISchemaEntityWithSchema> FindSchemaByNameAsync(Guid appId, string name); Task<ISchemaEntityWithSchema> FindSchemaByNameAsync(Guid appId, string name);
void Remove(NamedId<Guid> appId, NamedId<Guid> schemaId);
} }
} }

35
src/Squidex.Read/Schemas/Services/Implementations/CachingSchemaProvider.cs

@ -9,8 +9,11 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Squidex.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching; using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.Tasks;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Utils; using Squidex.Read.Utils;
@ -19,11 +22,16 @@ using Squidex.Read.Utils;
namespace Squidex.Read.Schemas.Services.Implementations namespace Squidex.Read.Schemas.Services.Implementations
{ {
public class CachingSchemaProvider : CachingProviderBase, ISchemaProvider public class CachingSchemaProvider : CachingProviderBase, ISchemaProvider, IEventConsumer
{ {
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10); private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly ISchemaRepository repository; private readonly ISchemaRepository repository;
public string Name
{
get { return GetType().Name; }
}
public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository) public CachingSchemaProvider(IMemoryCache cache, ISchemaRepository repository)
: base(cache) : base(cache)
{ {
@ -82,7 +90,9 @@ namespace Squidex.Read.Schemas.Services.Implementations
return result; return result;
} }
public void Remove(NamedId<Guid> appId, NamedId<Guid> schemaId) public Task On(Envelope<IEvent> @event)
{
void Remove(NamedId<Guid> appId, NamedId<Guid> schemaId)
{ {
var cacheKeyById = BuildIdCacheKey(schemaId.Id); var cacheKeyById = BuildIdCacheKey(schemaId.Id);
var cacheKeyByName = BuildNameCacheKey(appId.Id, schemaId.Name); var cacheKeyByName = BuildNameCacheKey(appId.Id, schemaId.Name);
@ -94,6 +104,22 @@ namespace Squidex.Read.Schemas.Services.Implementations
Cache.Invalidate(cacheKeyByName); Cache.Invalidate(cacheKeyByName);
} }
if (@event.Payload is FieldEvent fieldEvent)
{
Remove(fieldEvent.AppId, fieldEvent.SchemaId);
}
else if (@event.Payload is SchemaDeleted schemaDeletedEvent)
{
Remove(schemaDeletedEvent.AppId, schemaDeletedEvent.SchemaId);
}
else if (@event.Payload is SchemaUpdated schemaUpdatedEvent)
{
Remove(schemaUpdatedEvent.AppId, schemaUpdatedEvent.SchemaId);
}
return TaskHelper.Done;
}
private static string BuildNameCacheKey(Guid appId, string name) private static string BuildNameCacheKey(Guid appId, string name)
{ {
return $"Schema_Ids_{appId}_{name}"; return $"Schema_Ids_{appId}_{name}";
@ -103,5 +129,10 @@ namespace Squidex.Read.Schemas.Services.Implementations
{ {
return $"Schema_Names_{schemaId}"; return $"Schema_Names_{schemaId}";
} }
public Task ClearAsync()
{
return TaskHelper.Done;
}
} }
} }

2
src/Squidex.Write/Apps/AppDomainObject.cs

@ -207,7 +207,7 @@ namespace Squidex.Write.Apps
@event.AppId = new NamedId<Guid>(Id, name); @event.AppId = new NamedId<Guid>(Id, name);
} }
base.RaiseEvent(Envelope<IEvent>.Create(@event)); RaiseEvent(Envelope.Create(@event));
} }
private static AppLanguageAdded CreateInitialLanguage(NamedId<Guid> id) private static AppLanguageAdded CreateInitialLanguage(NamedId<Guid> id)

2
src/Squidex/Config/Domain/ReadModule.cs

@ -35,10 +35,12 @@ namespace Squidex.Config.Domain
{ {
builder.RegisterType<CachingAppProvider>() builder.RegisterType<CachingAppProvider>()
.As<IAppProvider>() .As<IAppProvider>()
.AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<CachingSchemaProvider>() builder.RegisterType<CachingSchemaProvider>()
.As<ISchemaProvider>() .As<ISchemaProvider>()
.AsSelf()
.SingleInstance(); .SingleInstance();
builder.RegisterType<AppHistoryEventsCreator>() builder.RegisterType<AppHistoryEventsCreator>()

25
src/Squidex/Config/Domain/StoreMongoDbModule.cs

@ -17,6 +17,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Infrastructure.MongoDb; using Squidex.Infrastructure.MongoDb;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services.Implementations;
using Squidex.Read.Contents.Repositories; using Squidex.Read.Contents.Repositories;
using Squidex.Read.History.Repositories; using Squidex.Read.History.Repositories;
using Squidex.Read.MongoDb.Apps; using Squidex.Read.MongoDb.Apps;
@ -26,6 +27,7 @@ using Squidex.Read.MongoDb.Infrastructure;
using Squidex.Read.MongoDb.Schemas; using Squidex.Read.MongoDb.Schemas;
using Squidex.Read.MongoDb.Users; using Squidex.Read.MongoDb.Users;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas.Services.Implementations;
using Squidex.Read.Users.Repositories; using Squidex.Read.Users.Repositories;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
@ -85,7 +87,8 @@ namespace Squidex.Config.Domain
IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection); IndexChecks.EnsureUniqueIndexOnNormalizedUserName(usersCollection);
return new UserStore<IdentityUser>(usersCollection); return new UserStore<IdentityUser>(usersCollection);
}).SingleInstance(); })
.SingleInstance();
builder.Register<IRoleStore<IdentityRole>>(c => builder.Register<IRoleStore<IdentityRole>>(c =>
{ {
@ -94,7 +97,8 @@ namespace Squidex.Config.Domain
IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection); IndexChecks.EnsureUniqueIndexOnNormalizedRoleName(rolesCollection);
return new RoleStore<IdentityRole>(rolesCollection); return new RoleStore<IdentityRole>(rolesCollection);
}).SingleInstance(); })
.SingleInstance();
builder.RegisterType<MongoUserRepository>() builder.RegisterType<MongoUserRepository>()
.As<IUserRepository>() .As<IUserRepository>()
@ -129,7 +133,6 @@ namespace Squidex.Config.Domain
builder.RegisterType<MongoSchemaRepository>() builder.RegisterType<MongoSchemaRepository>()
.WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName)) .WithParameter(ResolvedParameter.ForNamed<IMongoDatabase>(MongoDatabaseName))
.As<ISchemaRepository>() .As<ISchemaRepository>()
.As<IEventConsumer>()
.As<IExternalSystem>() .As<IExternalSystem>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
@ -141,6 +144,22 @@ namespace Squidex.Config.Domain
.As<IExternalSystem>() .As<IExternalSystem>()
.AsSelf() .AsSelf()
.SingleInstance(); .SingleInstance();
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoSchemaRepository>(),
c.Resolve<CachingSchemaProvider>()))
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
builder.Register(c =>
new CompoundEventConsumer(
c.Resolve<MongoAppRepository>(),
c.Resolve<CachingAppProvider>()))
.As<IEventConsumer>()
.AsSelf()
.SingleInstance();
} }
} }
} }

18
src/Squidex/Config/Domain/Usages.cs

@ -11,10 +11,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events; using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services;
using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas.Services;
namespace Squidex.Config.Domain namespace Squidex.Config.Domain
{ {
@ -31,20 +27,6 @@ namespace Squidex.Config.Domain
receiver?.Subscribe(catchConsumer); receiver?.Subscribe(catchConsumer);
} }
var appProvider = app.ApplicationServices.GetRequiredService<IAppProvider>();
app.ApplicationServices.GetRequiredService<IAppRepository>().AppSaved += appId =>
{
appProvider.Remove(appId);
};
var schemaProvider = app.ApplicationServices.GetRequiredService<ISchemaProvider>();
app.ApplicationServices.GetRequiredService<ISchemaRepository>().SchemaSaved += (appId, schemaId) =>
{
schemaProvider.Remove(appId, schemaId);
};
return app; return app;
} }

71
tests/Squidex.Infrastructure.Tests/CQRS/Events/CompoundEventConsumerTests.cs

@ -0,0 +1,71 @@
// ==========================================================================
// CompoundEventConsumerTests.cs
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex Group
// All rights reserved.
// ==========================================================================
using System.Threading.Tasks;
using Moq;
using Squidex.Infrastructure.Tasks;
using Xunit;
namespace Squidex.Infrastructure.CQRS.Events
{
public class CompoundEventConsumerTests
{
private readonly Mock<IEventConsumer> consumer1 = new Mock<IEventConsumer>();
private readonly Mock<IEventConsumer> consumer2 = new Mock<IEventConsumer>();
private sealed class MyEvent : IEvent
{
}
[Fact]
public void Should_return_given_name()
{
var sut = new CompoundEventConsumer("consumer-name");
Assert.Equal("consumer-name", sut.Name);
}
[Fact]
public void Should_return_first_inner_name()
{
var sut = new CompoundEventConsumer(consumer1.Object, consumer2.Object);
Assert.Equal(consumer1.Object.GetType().Name, sut.Name);
}
[Fact]
public async Task Should_clear_all_consumers()
{
consumer1.Setup(x => x.ClearAsync()).Returns(TaskHelper.Done).Verifiable();
consumer2.Setup(x => x.ClearAsync()).Returns(TaskHelper.Done).Verifiable();
var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object);
await sut.ClearAsync();
consumer1.VerifyAll();
consumer2.VerifyAll();
}
[Fact]
public async Task Should_invoke_all_consumers()
{
var @event = Envelope.Create(new MyEvent());
consumer1.Setup(x => x.On(@event)).Returns(TaskHelper.Done).Verifiable();
consumer2.Setup(x => x.On(@event)).Returns(TaskHelper.Done).Verifiable();
var sut = new CompoundEventConsumer("consumer-name", consumer1.Object, consumer2.Object);
await sut.On(@event);
consumer1.VerifyAll();
consumer2.VerifyAll();
}
}
}

1
tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs

@ -90,6 +90,7 @@ namespace Squidex.Infrastructure.CQRS.Events
eventStore.Setup(x => x.GetEventsAsync(2)).Returns(events.ToObservable()); eventStore.Setup(x => x.GetEventsAsync(2)).Returns(events.ToObservable());
eventConsumer.Setup(x => x.Name).Returns(consumerName);
eventConsumerInfoRepository.Setup(x => x.FindAsync(consumerName)).Returns(Task.FromResult<IEventConsumerInfo>(consumerInfo)); eventConsumerInfoRepository.Setup(x => x.FindAsync(consumerName)).Returns(Task.FromResult<IEventConsumerInfo>(consumerInfo));
formatter.Setup(x => x.Parse(eventData1)).Returns(envelope1); formatter.Setup(x => x.Parse(eventData1)).Returns(envelope1);

6
tests/Squidex.Read.Tests/Apps/CachingAppProviderTests.cs

@ -11,7 +11,9 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
using Squidex.Events.Apps;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Apps.Repositories; using Squidex.Read.Apps.Repositories;
using Squidex.Read.Apps.Services.Implementations; using Squidex.Read.Apps.Services.Implementations;
using Xunit; using Xunit;
@ -80,7 +82,7 @@ namespace Squidex.Read.Apps
await ProvideAppById(appV1); await ProvideAppById(appV1);
sut.Remove(appId); sut.On(Envelope.Create(new AppLanguageAdded {AppId = appId }).To<IEvent>()).Wait();
await ProvideAppById(appV2); await ProvideAppById(appV2);
@ -96,7 +98,7 @@ namespace Squidex.Read.Apps
await ProvideAppByName(appV1); await ProvideAppByName(appV1);
sut.Remove(appId); sut.On(Envelope.Create(new AppLanguageAdded { AppId = appId }).To<IEvent>()).Wait();
await ProvideAppByName(appV2); await ProvideAppByName(appV2);

6
tests/Squidex.Read.Tests/Schemas/CachingSchemaProviderTests.cs

@ -11,7 +11,9 @@ using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moq; using Moq;
using Squidex.Events.Schemas;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.CQRS.Events;
using Squidex.Read.Schemas.Repositories; using Squidex.Read.Schemas.Repositories;
using Squidex.Read.Schemas.Services.Implementations; using Squidex.Read.Schemas.Services.Implementations;
using Xunit; using Xunit;
@ -83,7 +85,7 @@ namespace Squidex.Read.Schemas
await ProvideSchemaById(schemaV1); await ProvideSchemaById(schemaV1);
sut.Remove(appId, schemaId); sut.On(Envelope.Create(new FieldAdded { AppId = appId, SchemaId = schemaId })).Wait();
await ProvideSchemaById(schemaV2); await ProvideSchemaById(schemaV2);
@ -99,7 +101,7 @@ namespace Squidex.Read.Schemas
await ProvideSchemaByName(schemaV1); await ProvideSchemaByName(schemaV1);
sut.Remove(appId, schemaId); sut.On(Envelope.Create(new SchemaUpdated { AppId = appId, SchemaId = schemaId })).Wait();
await ProvideSchemaByName(schemaV2); await ProvideSchemaByName(schemaV2);

Loading…
Cancel
Save