From 6860645e403f2bf49a3328fb12ece4e0476be205 Mon Sep 17 00:00:00 2001 From: Sebastian Stehle Date: Tue, 1 Aug 2017 22:17:55 +0200 Subject: [PATCH] App plan handling changed. --- .../Apps/Services/IAppPlanBillingManager.cs | 2 +- .../Apps/Services/IChangePlanResult.cs | 14 +++++++ .../NoopAppPlanBillingManager.cs | 4 +- .../Apps/Services/PlanChangeAsyncResult.cs | 18 +++++++++ .../Apps/Services/PlanChangedResult.cs | 19 +++++++++ .../Apps/Services/RedirectToCheckoutResult.cs | 25 ++++++++++++ .../Apps/AppCommandHandler.cs | 16 +++++++- .../Apps/Commands/ChangePlan.cs | 2 + .../CQRS/Events/GetEventStoreSubscription.cs | 2 +- .../CQRS/Events/MongoEventStore.cs | 2 +- .../CQRS/Events/EventReceiver.cs | 23 ++++------- .../Timers/CompletionTimer.cs | 37 ++++++++++------- src/Squidex/Config/Domain/ReadModule.cs | 26 +++++++++--- .../Api/Plans/AppPlansController.cs | 14 ++++++- .../Api/Plans/Models/PlanChangedDto.cs | 18 +++++++++ .../pages/plans/plans-page.component.ts | 18 +++++---- .../app/shared/services/plans.service.spec.ts | 11 ++++- .../app/shared/services/plans.service.ts | 12 +++++- .../Apps/AppCommandHandlerTests.cs | 40 +++++++++++++++++++ .../CQRS/Events/EventReceiverTests.cs | 13 ++++-- .../Timers/CompletionTimerTests.cs | 20 ++++++++++ 21 files changed, 278 insertions(+), 58 deletions(-) create mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs create mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs create mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs create mode 100644 src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs create mode 100644 src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs index f2d32431b..722784540 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/IAppPlanBillingManager.cs @@ -15,7 +15,7 @@ namespace Squidex.Domain.Apps.Read.Apps.Services { bool HasPortal { get; } - Task ChangePlanAsync(string userId, Guid appId, string appName, string planId); + Task ChangePlanAsync(string userId, Guid appId, string appName, string planId); Task HasPaymentOptionsAsync(string userId); diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs new file mode 100644 index 000000000..5086dff24 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/IChangePlanResult.cs @@ -0,0 +1,14 @@ +// ========================================================================== +// IChangePlanResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Read.Apps.Services +{ + public interface IChangePlanResult + { + } +} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs index df97d581d..959889a93 100644 --- a/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/Implementations/NoopAppPlanBillingManager.cs @@ -19,9 +19,9 @@ namespace Squidex.Domain.Apps.Read.Apps.Services.Implementations get { return false; } } - public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) + public Task ChangePlanAsync(string userId, Guid appId, string appName, string planId) { - return TaskHelper.Done; + return Task.FromResult(PlanChangedResult.Instance); } public Task HasPaymentOptionsAsync(string userId) diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs new file mode 100644 index 000000000..3a4ec4f9e --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangeAsyncResult.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// PlanChangeAsyncResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== +namespace Squidex.Domain.Apps.Read.Apps.Services +{ + public sealed class PlanChangeAsyncResult : IChangePlanResult + { + public static readonly PlanChangeAsyncResult Instance = new PlanChangeAsyncResult(); + + private PlanChangeAsyncResult() + { + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs new file mode 100644 index 000000000..1456823c5 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/PlanChangedResult.cs @@ -0,0 +1,19 @@ +// ========================================================================== +// PlanChangedResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Domain.Apps.Read.Apps.Services +{ + public sealed class PlanChangedResult : IChangePlanResult + { + public static readonly PlanChangedResult Instance = new PlanChangedResult(); + + private PlanChangedResult() + { + } + } +} diff --git a/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs b/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs new file mode 100644 index 000000000..21b3043d1 --- /dev/null +++ b/src/Squidex.Domain.Apps.Read/Apps/Services/RedirectToCheckoutResult.cs @@ -0,0 +1,25 @@ +// ========================================================================== +// RedirectToCheckoutResult.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using Squidex.Infrastructure; + +namespace Squidex.Domain.Apps.Read.Apps.Services +{ + public sealed class RedirectToCheckoutResult : IChangePlanResult + { + public Uri Url { get; } + + public RedirectToCheckoutResult(Uri url) + { + Guard.NotNull(url, nameof(url)); + + Url = url; + } + } +} diff --git a/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs b/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs index 9ccdf4ff9..e87f82be7 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/AppCommandHandler.cs @@ -107,9 +107,21 @@ namespace Squidex.Domain.Apps.Write.Apps return handler.UpdateAsync(context, async a => { - a.ChangePlan(command); + if (command.FromCallback) + { + a.ChangePlan(command); + } + else + { + var result = await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId); - await appPlansBillingManager.ChangePlanAsync(command.Actor.Identifier, a.Id, a.Name, command.PlanId); + if (result is PlanChangedResult) + { + a.ChangePlan(command); + } + + context.Succeed(result); + } }); } diff --git a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs index 5bb4498df..b7c160acc 100644 --- a/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs +++ b/src/Squidex.Domain.Apps.Write/Apps/Commands/ChangePlan.cs @@ -13,6 +13,8 @@ namespace Squidex.Domain.Apps.Write.Apps.Commands { public sealed class ChangePlan : AppAggregateCommand, IValidatable { + public bool FromCallback { get; set; } + public string PlanId { get; set; } public void Validate(IList errors) diff --git a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs index ad9e07690..1ce470428 100644 --- a/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs +++ b/src/Squidex.Infrastructure.GetEventStore/CQRS/Events/GetEventStoreSubscription.cs @@ -189,7 +189,7 @@ namespace Squidex.Infrastructure.CQRS.Events } } - if (reason != SubscriptionDropReason.UserInitiated && reason != SubscriptionDropReason.EventHandlerException) + if (reason != SubscriptionDropReason.UserInitiated) { var exception = ex ?? new ConnectionClosedException($"Subscription closed with reason {reason}."); diff --git a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs index 56f09bc01..841693e4a 100644 --- a/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/CQRS/Events/MongoEventStore.cs @@ -83,7 +83,7 @@ namespace Squidex.Infrastructure.CQRS.Events var filter = CreateFilter(streamFilter, lastPosition); - await Collection.Find(filter).Sort(Sort.Ascending(EventStreamField)).ForEachAsync(async commit => + await Collection.Find(filter).Sort(Sort.Ascending(TimestampField)).ForEachAsync(async commit => { var eventStreamOffset = (int)commit.EventStreamOffset; diff --git a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs index ca1ea7947..4ab00201b 100644 --- a/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs +++ b/src/Squidex.Infrastructure/CQRS/Events/EventReceiver.cs @@ -140,26 +140,17 @@ namespace Squidex.Infrastructure.CQRS.Events var subscription = eventStore.CreateSubscription(eventConsumer.EventsFilter, position); - async Task StopSubscriptionAsync(Exception exception) + await subscription.SubscribeAsync(async storedEvent => { - await eventConsumerInfoRepository.StopAsync(consumerName, exception.ToString()); - - subscription.Dispose(); - } + await DispatchConsumer(ParseEvent(storedEvent), eventConsumer, eventConsumer.Name); - await subscription.SubscribeAsync(async storedEvent => + await eventConsumerInfoRepository.SetPositionAsync(eventConsumer.Name, storedEvent.EventPosition, false); + }, async exception => { - try - { - await DispatchConsumer(ParseEvent(storedEvent), eventConsumer, eventConsumer.Name); + await eventConsumerInfoRepository.StopAsync(consumerName, exception.ToString()); - await eventConsumerInfoRepository.SetPositionAsync(eventConsumer.Name, storedEvent.EventPosition, false); - } - catch (Exception ex) - { - await StopSubscriptionAsync(ex); - } - }, StopSubscriptionAsync); + subscription.Dispose(); + }); currentSubscription = subscription; } diff --git a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs index 62504ce59..d19c5249f 100644 --- a/src/Squidex.Infrastructure/Timers/CompletionTimer.cs +++ b/src/Squidex.Infrastructure/Timers/CompletionTimer.cs @@ -50,26 +50,33 @@ namespace Squidex.Infrastructure.Timers private async Task RunInternal(int delay, int initialDelay, Func callback) { - if (initialDelay > 0) - { - await WaitAsync(initialDelay).ConfigureAwait(false); - } - - while (requiresAtLeastOne == 2 || !disposeToken.IsCancellationRequested) + try { - try - { - await callback(disposeToken.Token).ConfigureAwait(false); - } - catch (OperationCanceledException) + if (initialDelay > 0) { + await WaitAsync(initialDelay).ConfigureAwait(false); } - finally + + while (requiresAtLeastOne == 2 || !disposeToken.IsCancellationRequested) { - requiresAtLeastOne = 1; - } + try + { + await callback(disposeToken.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + finally + { + requiresAtLeastOne = 1; + } - await WaitAsync(delay).ConfigureAwait(false); + await WaitAsync(delay).ConfigureAwait(false); + } + } + catch + { + } } diff --git a/src/Squidex/Config/Domain/ReadModule.cs b/src/Squidex/Config/Domain/ReadModule.cs index 306047798..dc30e8275 100644 --- a/src/Squidex/Config/Domain/ReadModule.cs +++ b/src/Squidex/Config/Domain/ReadModule.cs @@ -9,8 +9,10 @@ using System.Collections.Generic; using System.Linq; using Autofac; +using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; +using Squidex.Chargebee; using Squidex.Domain.Apps.Read.Apps; using Squidex.Domain.Apps.Read.Apps.Services; using Squidex.Domain.Apps.Read.Apps.Services.Implementations; @@ -22,8 +24,10 @@ using Squidex.Domain.Apps.Read.Schemas; using Squidex.Domain.Apps.Read.Schemas.Services; using Squidex.Domain.Apps.Read.Schemas.Services.Implementations; using Squidex.Domain.Users; +using Squidex.Infrastructure; using Squidex.Infrastructure.CQRS.Events; using Squidex.Pipeline; +using Squidex.Shared.Users; // ReSharper disable UnusedAutoPropertyAccessor.Local @@ -55,11 +59,6 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); - builder.RegisterType() - .As() - .AsSelf() - .SingleInstance(); - builder.RegisterType() .As() .AsSelf() @@ -98,6 +97,23 @@ namespace Squidex.Config.Domain .As() .AsSelf() .InstancePerDependency(); + + var chargebeeSiteName = Configuration.GetValue("chargebee:siteName"); + var chargebeeApiKey = Configuration.GetValue("chargebee:apiKey"); + + if (string.IsNullOrWhiteSpace(chargebeeSiteName)) + { + throw new ConfigurationException("Configure Chargebee SiteName type with 'chargebee:siteName'."); + } + if (string.IsNullOrWhiteSpace(chargebeeApiKey)) + { + throw new ConfigurationException("Configure Chargebee ApiKey with 'chargebee:apiKey'."); + } + + builder.Register(c => new ChargebeeAppPlanBillingManager(c.Resolve>(), chargebeeSiteName, chargebeeApiKey)) + .As() + .AsSelf() + .SingleInstance(); } } } diff --git a/src/Squidex/Controllers/Api/Plans/AppPlansController.cs b/src/Squidex/Controllers/Api/Plans/AppPlansController.cs index 9c4606383..b520b679d 100644 --- a/src/Squidex/Controllers/Api/Plans/AppPlansController.cs +++ b/src/Squidex/Controllers/Api/Plans/AppPlansController.cs @@ -19,6 +19,8 @@ using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; using Squidex.Pipeline; +// ReSharper disable RedundantIfElseBlock + namespace Squidex.Controllers.Api.Plans { /// @@ -81,6 +83,7 @@ namespace Squidex.Controllers.Api.Plans /// The name of the app. /// Plan object that needs to be changed. /// + /// 201 => Redirected to checkout page. /// 204 => Plan changed. /// 400 => Plan not owned by user. /// 404 => App not found. @@ -88,13 +91,20 @@ namespace Squidex.Controllers.Api.Plans [MustBeAppOwner] [HttpPut] [Route("apps/{app}/plan/")] + [ProducesResponseType(typeof(PlanChangedDto), 200)] [ProducesResponseType(typeof(ErrorDto), 400)] [ApiCosts(0.5)] public async Task ChangePlanAsync(string app, [FromBody] ChangePlanDto request) { - await CommandBus.PublishAsync(SimpleMapper.Map(request, new ChangePlan())); + var redirectUri = (string)null; + var context = await CommandBus.PublishAsync(SimpleMapper.Map(request, new ChangePlan())); + + if (context.Result() is RedirectToCheckoutResult result) + { + redirectUri = result.Url.ToString(); + } - return NoContent(); + return Ok(new PlanChangedDto { RedirectUri = redirectUri }); } } } diff --git a/src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs b/src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs new file mode 100644 index 000000000..3674351dc --- /dev/null +++ b/src/Squidex/Controllers/Api/Plans/Models/PlanChangedDto.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// PlanChangedDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Controllers.Api.Plans.Models +{ + public class PlanChangedDto + { + /// + /// Optional redirect uri. + /// + public string RedirectUri { get; set; } + } +} diff --git a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts b/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts index fdb019845..3da5735e5 100644 --- a/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts +++ b/src/Squidex/app/features/settings/pages/plans/plans-page.component.ts @@ -67,13 +67,17 @@ export class PlansPageComponent extends AppComponentBase implements OnInit { this.appNameOnce() .switchMap(app => this.plansService.putPlan(app, new ChangePlanDto(planId), this.version)) .subscribe(dto => { - this.plans = - new AppPlansDto(planId, - this.plans.planOwner, - this.plans.hasPortal, - this.plans.hasConfigured, - this.plans.plans); - this.isDisabled = false; + if (dto.redirectUri && dto.redirectUri.length > 0) { + window.location.replace(dto.redirectUri); + } else { + this.plans = + new AppPlansDto(planId, + this.plans.planOwner, + this.plans.hasPortal, + this.plans.hasConfigured, + this.plans.plans); + this.isDisabled = false; + } }, error => { this.notifyError(error); this.isDisabled = false; diff --git a/src/Squidex/app/shared/services/plans.service.spec.ts b/src/Squidex/app/shared/services/plans.service.spec.ts index 536b1ec7a..701387303 100644 --- a/src/Squidex/app/shared/services/plans.service.spec.ts +++ b/src/Squidex/app/shared/services/plans.service.spec.ts @@ -12,6 +12,7 @@ import { AppPlansDto, ApiUrlConfig, ChangePlanDto, + PlanChangedDto, PlanDto, PlansService, Version @@ -93,11 +94,19 @@ describe('PlansService', () => { const dto = new ChangePlanDto('enterprise'); - plansService.putPlan('my-app', dto, version).subscribe(); + let planChanged: PlanChangedDto | null = null; + + plansService.putPlan('my-app', dto, version).subscribe(result => { + planChanged = result; + }); const req = httpMock.expectOne('http://service/p/api/apps/my-app/plan'); + req.flush({ redirectUri: 'my-url' }); + expect(req.request.method).toEqual('PUT'); expect(req.request.headers.get('If-Match')).toBe(version.value); + + expect(planChanged).toBe(new PlanChangedDto('my-url')); })); }); \ No newline at end of file diff --git a/src/Squidex/app/shared/services/plans.service.ts b/src/Squidex/app/shared/services/plans.service.ts index 43da39c2c..6ba084f49 100644 --- a/src/Squidex/app/shared/services/plans.service.ts +++ b/src/Squidex/app/shared/services/plans.service.ts @@ -40,6 +40,13 @@ export class PlanDto { } } +export class PlanChangedDto { + constructor( + public readonly redirectUri: string + ) { + } +} + export class ChangePlanDto { constructor( public readonly planId: string @@ -80,10 +87,13 @@ export class PlansService { .pretifyError('Failed to load plans. Please reload.'); } - public putPlan(appName: string, dto: ChangePlanDto, version?: Version): Observable { + public putPlan(appName: string, dto: ChangePlanDto, version?: Version): Observable { const url = this.apiUrl.buildUrl(`api/apps/${appName}/plan`); return HTTP.putVersioned(this.http, url, dto, version) + .map(response => { + return new PlanChangedDto(response.redirectUri); + }) .pretifyError('Failed to change plan. Please reload.'); } } \ No newline at end of file diff --git a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs index be3f7a511..a4e842d81 100644 --- a/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs +++ b/tests/Squidex.Domain.Apps.Write.Tests/Apps/AppCommandHandlerTests.cs @@ -232,6 +232,41 @@ namespace Squidex.Domain.Apps.Write.Apps appPlansBillingManager.Verify(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan"), Times.Once()); } + [Fact] + public async Task ChangePlan_should_not_make_update_for_redirect_result() + { + appPlansProvider.Setup(x => x.IsConfiguredPlan("my-plan")).Returns(true); + appPlansBillingManager.Setup(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan")).Returns(CreateRedirectResult()); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan" }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + Assert.Null(app.PlanId); + } + + [Fact] + public async Task ChangePlan_should_not_call_billing_manager_for_callback() + { + appPlansProvider.Setup(x => x.IsConfiguredPlan("my-plan")).Returns(true); + + CreateApp(); + + var context = CreateContextForCommand(new ChangePlan { PlanId = "my-plan", FromCallback = true }); + + await TestUpdate(app, async _ => + { + await sut.HandleAsync(context); + }); + + appPlansBillingManager.Verify(x => x.ChangePlanAsync(User.Identifier, app.Id, app.Name, "my-plan"), Times.Never()); + } + [Fact] public async Task AddLanguage_should_update_domain_object() { @@ -279,5 +314,10 @@ namespace Squidex.Domain.Apps.Write.Apps return app; } + + private static Task CreateRedirectResult() + { + return Task.FromResult(new RedirectToCheckoutResult(new Uri("http://squidex.io"))); + } } } diff --git a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs index 7a8c0d473..58a074ed2 100644 --- a/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs +++ b/tests/Squidex.Infrastructure.Tests/CQRS/Events/EventReceiverTests.cs @@ -43,7 +43,7 @@ namespace Squidex.Infrastructure.CQRS.Events this.storedEvents = storedEvents; } - public Task SubscribeAsync(Func onNext, Func onError) + public async Task SubscribeAsync(Func onNext, Func onError) { foreach (var storedEvent in storedEvents) { @@ -52,10 +52,15 @@ namespace Squidex.Infrastructure.CQRS.Events break; } - onNext(storedEvent).Wait(); + try + { + await onNext(storedEvent); + } + catch (Exception ex) + { + await onError(ex); + } } - - return TaskHelper.Done; } public void Dispose() diff --git a/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs b/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs index 01f7c35e5..e445ff387 100644 --- a/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/Timers/CompletionTimerTests.cs @@ -6,9 +6,12 @@ // All rights reserved. // ========================================================================== +using System.Threading; using Squidex.Infrastructure.Tasks; using Xunit; +// ReSharper disable AccessToModifiedClosure + namespace Squidex.Infrastructure.Timers { public class CompletionTimerTests @@ -30,5 +33,22 @@ namespace Squidex.Infrastructure.Timers Assert.True(called); } + + public void Should_invoke_dispose_within_timer() + { + CompletionTimer timer = null; + + timer = new CompletionTimer(10, ct => + { + timer?.Dispose(); + + return TaskHelper.Done; + }, 10); + + Thread.Sleep(1000); + + timer.Wakeup(); + timer.Dispose(); + } } }