mirror of https://github.com/Squidex/squidex.git
42 changed files with 774 additions and 581 deletions
@ -1,21 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// IRuleDequeuerGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Orleans.Concurrency; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.Rules.Orleans.Grains |
|
||||
{ |
|
||||
public interface IRuleDequeuerGrain : IGrainWithStringKey |
|
||||
{ |
|
||||
Task ActivateAsync(); |
|
||||
|
|
||||
Task HandleAsync(Immutable<IRuleEventEntity> @event); |
|
||||
} |
|
||||
} |
|
||||
@ -1,32 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// RuleDequeuerBootstrap.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans.Providers; |
|
||||
using Squidex.Domain.Apps.Read.Rules.Orleans.Grains; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.Rules.Orleans |
|
||||
{ |
|
||||
public sealed class RuleDequeuerBootstrap : IBootstrapProvider |
|
||||
{ |
|
||||
public string Name { get; private set; } |
|
||||
|
|
||||
public Task Init(string name, IProviderRuntime providerRuntime, IProviderConfiguration config) |
|
||||
{ |
|
||||
var grain = providerRuntime.GrainFactory.GetGrain<IRuleDequeuerGrain>("Default"); |
|
||||
|
|
||||
return grain.ActivateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task Close() |
|
||||
{ |
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,89 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppProvider.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.Domain.Apps.Read.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Rules; |
||||
|
using Squidex.Domain.Apps.Read.Schemas; |
||||
|
using Squidex.Domain.Apps.Read.State.Orleans.Grains; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.State.Orleans |
||||
|
{ |
||||
|
public sealed class AppProvider : IAppProvider |
||||
|
{ |
||||
|
private readonly IStateFactory factory; |
||||
|
|
||||
|
public AppProvider(IStateFactory factory) |
||||
|
{ |
||||
|
Guard.NotNull(factory, nameof(factory)); |
||||
|
|
||||
|
this.factory = factory; |
||||
|
} |
||||
|
|
||||
|
public async Task<IAppEntity> GetAppAsync(string appName) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetAppAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetAppWithSchemaAsync(id); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<IRuleEntity>> GetRulesAsync(string appName) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetRulesAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetSchemaAsync(id, provideDeleted); |
||||
|
} |
||||
|
|
||||
|
public async Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetSchemaAsync(name, provideDeleted); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<ISchemaEntity>> GetSchemasAsync(string appName) |
||||
|
{ |
||||
|
var app = await factory.GetAsync<AppStateGrain, AppStateGrainState>(appName); |
||||
|
|
||||
|
return await app.GetSchemasAsync(); |
||||
|
} |
||||
|
|
||||
|
public async Task<List<IAppEntity>> GetUserApps(string userId) |
||||
|
{ |
||||
|
var appUser = await factory.GetAsync<AppUserGrain, AppUserGrainState>(userId); |
||||
|
var appNames = await appUser.GetAppNamesAsync(); |
||||
|
|
||||
|
var tasks = |
||||
|
appNames |
||||
|
.Select(x => GetAppAsync(x)); |
||||
|
|
||||
|
var apps = await Task.WhenAll(tasks); |
||||
|
|
||||
|
return apps.Where(a => a != null).ToList(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppStateGrain.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Domain.Apps.Core.Schemas; |
||||
|
using Squidex.Domain.Apps.Events.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Apps; |
||||
|
using Squidex.Domain.Apps.Read.Rules; |
||||
|
using Squidex.Domain.Apps.Read.Schemas; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.CQRS.Events; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains |
||||
|
{ |
||||
|
public class AppStateGrain : StatefulObject<AppStateGrainState> |
||||
|
{ |
||||
|
private readonly FieldRegistry fieldRegistry; |
||||
|
private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); |
||||
|
private Exception exception; |
||||
|
|
||||
|
public AppStateGrain(FieldRegistry fieldRegistry) |
||||
|
{ |
||||
|
Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); |
||||
|
|
||||
|
this.fieldRegistry = fieldRegistry; |
||||
|
} |
||||
|
|
||||
|
public override async Task ReadStateAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
await base.ReadStateAsync(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
exception = ex; |
||||
|
|
||||
|
State = new AppStateGrainState(); |
||||
|
} |
||||
|
|
||||
|
State.SetRegistry(fieldRegistry); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid id) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var schema = State.FindSchema(x => x.Id == id && !x.IsDeleted); |
||||
|
|
||||
|
return Task.FromResult((State.GetApp(), schema)); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<IAppEntity> GetAppAsync() |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var value = State.GetApp(); |
||||
|
|
||||
|
return Task.FromResult(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<List<IRuleEntity>> GetRulesAsync() |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var value = State.FindRules(); |
||||
|
|
||||
|
return Task.FromResult(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<List<ISchemaEntity>> GetSchemasAsync() |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var value = State.FindSchemas(x => !x.IsDeleted); |
||||
|
|
||||
|
return Task.FromResult(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<ISchemaEntity> GetSchemaAsync(Guid id, bool provideDeleted = false) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var value = State.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); |
||||
|
|
||||
|
return Task.FromResult(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task<ISchemaEntity> GetSchemaAsync(string name, bool provideDeleted = false) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
var value = State.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); |
||||
|
|
||||
|
return Task.FromResult(value); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public virtual Task HandleAsync(Envelope<IEvent> message) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
if (exception != null) |
||||
|
{ |
||||
|
if (message.Payload is AppCreated) |
||||
|
{ |
||||
|
exception = null; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw exception; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
State.Apply(message); |
||||
|
|
||||
|
return WriteStateAsync(); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
// ==========================================================================
|
||||
|
// AppUserGrain.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using System.Linq; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains |
||||
|
{ |
||||
|
public sealed class AppUserGrain : StatefulObject<AppUserGrainState> |
||||
|
{ |
||||
|
private readonly SingleThreadedDispatcher dispatcher = new SingleThreadedDispatcher(); |
||||
|
|
||||
|
public Task AddAppAsync(string appName) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
State.AppNames.Add(appName); |
||||
|
|
||||
|
return WriteStateAsync(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public Task RemoveAppAsync(string appName) |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
State.AppNames.Remove(appName); |
||||
|
|
||||
|
return WriteStateAsync(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public Task<List<string>> GetAppNamesAsync() |
||||
|
{ |
||||
|
return dispatcher.DispatchAndUnwrapAsync(() => |
||||
|
{ |
||||
|
return Task.FromResult(State.AppNames.ToList()); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,37 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// IAppStateGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Squidex.Domain.Apps.Read.Apps; |
|
||||
using Squidex.Domain.Apps.Read.Rules; |
|
||||
using Squidex.Domain.Apps.Read.Schemas; |
|
||||
using Squidex.Infrastructure.CQRS.Events; |
|
||||
using Squidex.Infrastructure.Json.Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains |
|
||||
{ |
|
||||
public interface IAppStateGrain : IGrainWithStringKey |
|
||||
{ |
|
||||
Task<J<(IAppEntity, ISchemaEntity)>> GetAppWithSchemaAsync(Guid id); |
|
||||
|
|
||||
Task<J<IAppEntity>> GetAppAsync(); |
|
||||
|
|
||||
Task<J<ISchemaEntity>> GetSchemaAsync(Guid id, bool provideDeleted = false); |
|
||||
|
|
||||
Task<J<ISchemaEntity>> GetSchemaAsync(string name, bool provideDeleted = false); |
|
||||
|
|
||||
Task<J<List<ISchemaEntity>>> GetSchemasAsync(); |
|
||||
|
|
||||
Task<J<List<IRuleEntity>>> GetRulesAsync(); |
|
||||
|
|
||||
Task HandleAsync(J<Envelope<IEvent>> message); |
|
||||
} |
|
||||
} |
|
||||
@ -1,23 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// IAppUserGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains |
|
||||
{ |
|
||||
public interface IAppUserGrain : IGrainWithStringKey |
|
||||
{ |
|
||||
Task<List<string>> GetSchemaNamesAsync(); |
|
||||
|
|
||||
Task AddAppAsync(string appName); |
|
||||
|
|
||||
Task RemoveAppAsync(string appName); |
|
||||
} |
|
||||
} |
|
||||
@ -1,121 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// AppStateGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans.Runtime; |
|
||||
using Squidex.Domain.Apps.Core.Schemas; |
|
||||
using Squidex.Domain.Apps.Events.Apps; |
|
||||
using Squidex.Domain.Apps.Read.Apps; |
|
||||
using Squidex.Domain.Apps.Read.Rules; |
|
||||
using Squidex.Domain.Apps.Read.Schemas; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.CQRS.Events; |
|
||||
using Squidex.Infrastructure.Json.Orleans; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations |
|
||||
{ |
|
||||
public sealed class AppStateGrain : GrainV2<AppStateGrainState>, IAppStateGrain |
|
||||
{ |
|
||||
private readonly FieldRegistry fieldRegistry; |
|
||||
private Exception exception; |
|
||||
|
|
||||
public AppStateGrain(FieldRegistry fieldRegistry, IGrainRuntime runtime) |
|
||||
: base(runtime) |
|
||||
{ |
|
||||
Guard.NotNull(fieldRegistry, nameof(fieldRegistry)); |
|
||||
Guard.NotNull(runtime, nameof(runtime)); |
|
||||
|
|
||||
this.fieldRegistry = fieldRegistry; |
|
||||
} |
|
||||
|
|
||||
protected override async Task ReadStateAsync() |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
await base.ReadStateAsync(); |
|
||||
} |
|
||||
catch (Exception ex) |
|
||||
{ |
|
||||
exception = ex; |
|
||||
|
|
||||
State = new AppStateGrainState(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public override Task OnActivateAsync() |
|
||||
{ |
|
||||
State.SetRegistry(fieldRegistry); |
|
||||
|
|
||||
return base.OnActivateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task<J<(IAppEntity, ISchemaEntity)>> GetAppWithSchemaAsync(Guid id) |
|
||||
{ |
|
||||
var schema = State.FindSchema(x => x.Id == id && !x.IsDeleted); |
|
||||
|
|
||||
return Task.FromResult((State.GetApp(), schema).AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task<J<IAppEntity>> GetAppAsync() |
|
||||
{ |
|
||||
var value = State.GetApp(); |
|
||||
|
|
||||
return Task.FromResult(value.AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task<J<List<IRuleEntity>>> GetRulesAsync() |
|
||||
{ |
|
||||
var value = State.FindRules(); |
|
||||
|
|
||||
return Task.FromResult(value.AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task<J<List<ISchemaEntity>>> GetSchemasAsync() |
|
||||
{ |
|
||||
var value = State.FindSchemas(x => !x.IsDeleted); |
|
||||
|
|
||||
return Task.FromResult(value.AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task<J<ISchemaEntity>> GetSchemaAsync(Guid id, bool provideDeleted = false) |
|
||||
{ |
|
||||
var value = State.FindSchema(x => x.Id == id && (!x.IsDeleted || provideDeleted)); |
|
||||
|
|
||||
return Task.FromResult(value.AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task<J<ISchemaEntity>> GetSchemaAsync(string name, bool provideDeleted = false) |
|
||||
{ |
|
||||
var value = State.FindSchema(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase) && (!x.IsDeleted || provideDeleted)); |
|
||||
|
|
||||
return Task.FromResult(value.AsJ()); |
|
||||
} |
|
||||
|
|
||||
public Task HandleAsync(J<Envelope<IEvent>> message) |
|
||||
{ |
|
||||
if (exception != null) |
|
||||
{ |
|
||||
if (message.Value.Payload is AppCreated) |
|
||||
{ |
|
||||
exception = null; |
|
||||
} |
|
||||
else |
|
||||
{ |
|
||||
throw exception; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
State.Apply(message.Value); |
|
||||
|
|
||||
return WriteStateAsync(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,43 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// AppUserGrain.cs
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex Group
|
|
||||
// All rights reserved.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Collections.Generic; |
|
||||
using System.Linq; |
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans.Runtime; |
|
||||
using Squidex.Infrastructure.Orleans; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.State.Orleans.Grains.Implementations |
|
||||
{ |
|
||||
public sealed class AppUserGrain : GrainV2<AppUserGrainState>, IAppUserGrain |
|
||||
{ |
|
||||
public AppUserGrain(IGrainRuntime runtime) |
|
||||
: base(runtime) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public Task AddAppAsync(string appName) |
|
||||
{ |
|
||||
State.AppNames.Add(appName); |
|
||||
|
|
||||
return WriteStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task RemoveAppAsync(string appName) |
|
||||
{ |
|
||||
State.AppNames.Remove(appName); |
|
||||
|
|
||||
return WriteStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task<List<string>> GetSchemaNamesAsync() |
|
||||
{ |
|
||||
return Task.FromResult(State.AppNames.ToList()); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,129 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// OrleansAppProvider.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 Orleans; |
|
||||
using Squidex.Domain.Apps.Read.Apps; |
|
||||
using Squidex.Domain.Apps.Read.Rules; |
|
||||
using Squidex.Domain.Apps.Read.Schemas; |
|
||||
using Squidex.Domain.Apps.Read.State.Orleans.Grains; |
|
||||
using Squidex.Infrastructure; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Read.State.Orleans |
|
||||
{ |
|
||||
public sealed class OrleansAppProvider : IAppProvider |
|
||||
{ |
|
||||
private readonly IGrainFactory factory; |
|
||||
private readonly ISemanticLog log; |
|
||||
|
|
||||
public OrleansAppProvider(IGrainFactory factory, ISemanticLog log) |
|
||||
{ |
|
||||
Guard.NotNull(factory, nameof(factory)); |
|
||||
Guard.NotNull(log, nameof(log)); |
|
||||
|
|
||||
this.factory = factory; |
|
||||
|
|
||||
this.log = log; |
|
||||
} |
|
||||
|
|
||||
public async Task<IAppEntity> GetAppAsync(string appName) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetAppAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppAsync(); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(string appName, Guid id) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetAppWithSchemaAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetAppWithSchemaAsync(id); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<List<IRuleEntity>> GetRulesAsync(string appName) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetRulesAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetRulesAsync(); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<ISchemaEntity> GetSchemaAsync(string appName, Guid id, bool provideDeleted = false) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetSchemaAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(id, provideDeleted); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<ISchemaEntity> GetSchemaAsync(string appName, string name, bool provideDeleted = false) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetSchemaAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemaAsync(name, provideDeleted); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<List<ISchemaEntity>> GetSchemasAsync(string appName) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetSchemasAsync)))) |
|
||||
{ |
|
||||
var result = await factory.GetGrain<IAppStateGrain>(appName).GetSchemasAsync(); |
|
||||
|
|
||||
return result.Value; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public async Task<List<IAppEntity>> GetUserApps(string userId) |
|
||||
{ |
|
||||
using (log.MeasureTrace(w => w |
|
||||
.WriteProperty("module", nameof(OrleansAppProvider)) |
|
||||
.WriteProperty("method", nameof(GetUserApps)))) |
|
||||
{ |
|
||||
var schemaIds = await factory.GetGrain<IAppUserGrain>(userId).GetSchemaNamesAsync(); |
|
||||
|
|
||||
var tasks = |
|
||||
schemaIds |
|
||||
.Select(x => factory.GetGrain<IAppStateGrain>(x)) |
|
||||
.Select(x => x.GetAppAsync()); |
|
||||
|
|
||||
var apps = await Task.WhenAll(tasks); |
|
||||
|
|
||||
return apps.Select(a => a.Value).Where(a => a != null).ToList(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,113 @@ |
|||||
|
// ==========================================================================
|
||||
|
// MongoStateStore.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using MongoDB.Bson; |
||||
|
using MongoDB.Driver; |
||||
|
using Newtonsoft.Json; |
||||
|
using Newtonsoft.Json.Linq; |
||||
|
using Squidex.Infrastructure.MongoDb; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
public sealed class MongoStateStore : IStateStore, IExternalSystem |
||||
|
{ |
||||
|
private const string FieldId = "_id"; |
||||
|
private const string FieldDoc = "_doc"; |
||||
|
private const string FieldEtag = "_etag"; |
||||
|
private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; |
||||
|
private static readonly FilterDefinitionBuilder<BsonDocument> Filter = Builders<BsonDocument>.Filter; |
||||
|
private static readonly UpdateDefinitionBuilder<BsonDocument> Update = Builders<BsonDocument>.Update; |
||||
|
private static readonly ProjectionDefinitionBuilder<BsonDocument> Projection = Builders<BsonDocument>.Projection; |
||||
|
private readonly IMongoDatabase database; |
||||
|
private readonly JsonSerializer serializer; |
||||
|
|
||||
|
public MongoStateStore(IMongoDatabase database, JsonSerializer serializer) |
||||
|
{ |
||||
|
Guard.NotNull(database, nameof(database)); |
||||
|
Guard.NotNull(serializer, nameof(serializer)); |
||||
|
|
||||
|
this.database = database; |
||||
|
this.serializer = serializer; |
||||
|
} |
||||
|
|
||||
|
public void Connect() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
database.ListCollections(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
throw new ConfigurationException($"MongoDb connection failed to connect to database {database.DatabaseNamespace.DatabaseName}", ex); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public async Task<(T Value, string Etag)> ReadAsync<T>(string key) |
||||
|
{ |
||||
|
var collection = GetCollection<T>(); |
||||
|
|
||||
|
var existing = |
||||
|
await collection.Find(Filter.Eq(FieldId, key)) |
||||
|
.FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existing != null) |
||||
|
{ |
||||
|
var value = existing[FieldDoc].AsBsonDocument.ToJson().ToObject<T>(serializer); |
||||
|
|
||||
|
return (value, existing[FieldEtag].AsString); |
||||
|
} |
||||
|
|
||||
|
return (default(T), null); |
||||
|
} |
||||
|
|
||||
|
public async Task WriteAsync<T>(string key, T value, string oldEtag, string newEtag) |
||||
|
{ |
||||
|
var collection = GetCollection<T>(); |
||||
|
|
||||
|
var newData = JToken.FromObject(value, serializer).ToBson(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await collection.UpdateOneAsync( |
||||
|
Filter.And( |
||||
|
Filter.Eq(FieldId, key), |
||||
|
Filter.Eq(FieldEtag, oldEtag) |
||||
|
), |
||||
|
Update |
||||
|
.Set(FieldEtag, newEtag) |
||||
|
.Set(FieldDoc, newData), |
||||
|
Upsert); |
||||
|
} |
||||
|
catch (MongoWriteException ex) |
||||
|
{ |
||||
|
if (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) |
||||
|
{ |
||||
|
var existingEtag = |
||||
|
await collection.Find(Filter.Eq(FieldId, key)) |
||||
|
.Project<BsonDocument>(Projection.Exclude(FieldDoc)).FirstOrDefaultAsync(); |
||||
|
|
||||
|
if (existingEtag != null && existingEtag.Contains(FieldEtag)) |
||||
|
{ |
||||
|
throw new InconsistentStateException(existingEtag[FieldEtag].AsString, oldEtag, ex); |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private IMongoCollection<BsonDocument> GetCollection<T>() |
||||
|
{ |
||||
|
return database.GetCollection<BsonDocument>($"States_{typeof(T).Name}"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,41 @@ |
|||||
|
// ==========================================================================
|
||||
|
// DefaultEventNotifier.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events |
||||
|
{ |
||||
|
public sealed class DefaultEventNotifier : IEventNotifier |
||||
|
{ |
||||
|
private static readonly string ChannelName = typeof(DefaultEventNotifier).Name; |
||||
|
|
||||
|
private readonly IPubSub pubsub; |
||||
|
|
||||
|
public sealed class EventNotification |
||||
|
{ |
||||
|
public string StreamName { get; set; } |
||||
|
} |
||||
|
|
||||
|
public DefaultEventNotifier(IPubSub pubsub) |
||||
|
{ |
||||
|
Guard.NotNull(pubsub, nameof(pubsub)); |
||||
|
|
||||
|
this.pubsub = pubsub; |
||||
|
} |
||||
|
|
||||
|
public void NotifyEventsStored(string streamName) |
||||
|
{ |
||||
|
pubsub.Publish(new EventNotification { StreamName = streamName }, true); |
||||
|
} |
||||
|
|
||||
|
public IDisposable Subscribe(Action<string> handler) |
||||
|
{ |
||||
|
return pubsub.Subscribe<EventNotification>(x => handler?.Invoke(x.StreamName)); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
// ==========================================================================
|
||||
|
// InconsistentStateException.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Runtime.Serialization; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.States |
||||
|
{ |
||||
|
[Serializable] |
||||
|
public class InconsistentStateException : Exception |
||||
|
{ |
||||
|
private readonly string currentEtag; |
||||
|
private readonly string expectedEtag; |
||||
|
|
||||
|
public string CurrentEtag |
||||
|
{ |
||||
|
get { return currentEtag; } |
||||
|
} |
||||
|
|
||||
|
public string ExpectedEtag |
||||
|
{ |
||||
|
get { return expectedEtag; } |
||||
|
} |
||||
|
|
||||
|
public InconsistentStateException(string currentEtag, string expectedEtag, Exception ex) |
||||
|
: base(FormatMessage(currentEtag, expectedEtag), ex) |
||||
|
{ |
||||
|
this.currentEtag = currentEtag; |
||||
|
|
||||
|
this.expectedEtag = expectedEtag; |
||||
|
} |
||||
|
|
||||
|
protected InconsistentStateException(SerializationInfo info, StreamingContext context) |
||||
|
: base(info, context) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static string FormatMessage(string currentEtag, string expectedEtag) |
||||
|
{ |
||||
|
return $"Requested etag {expectedEtag}, but found {currentEtag}."; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
// ==========================================================================
|
||||
|
// DefaultEventNotifierTests.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Collections.Generic; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events |
||||
|
{ |
||||
|
public sealed class DefaultEventNotifierTests |
||||
|
{ |
||||
|
private readonly DefaultEventNotifier sut = new DefaultEventNotifier(new InMemoryPubSub()); |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_invalidate_all_actions() |
||||
|
{ |
||||
|
var handler1Handled = 0; |
||||
|
var handler2Handled = 0; |
||||
|
|
||||
|
var streamNames = new List<string>(); |
||||
|
|
||||
|
sut.Subscribe(x => |
||||
|
{ |
||||
|
streamNames.Add(x); |
||||
|
|
||||
|
handler1Handled++; |
||||
|
}); |
||||
|
|
||||
|
sut.NotifyEventsStored("a"); |
||||
|
|
||||
|
sut.Subscribe(x => |
||||
|
{ |
||||
|
streamNames.Add(x); |
||||
|
|
||||
|
handler2Handled++; |
||||
|
}); |
||||
|
|
||||
|
sut.NotifyEventsStored("b"); |
||||
|
|
||||
|
Assert.Equal(2, handler1Handled); |
||||
|
Assert.Equal(1, handler2Handled); |
||||
|
|
||||
|
Assert.Equal(streamNames.ToArray(), new[] { "a", "b", "b" }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue