diff --git a/Documentation/Jobs.md b/Documentation/Jobs.md new file mode 100644 index 000000000..a938a42d5 --- /dev/null +++ b/Documentation/Jobs.md @@ -0,0 +1,108 @@ +# Jobs + +A job is basically a task that you either don't want to execute in the current +context, on the current server or execute at a later time. EventFlow provides +basic functionality for jobs. + +There are areas where you might find jobs very useful, here are some examples + + * Publish a command at a specific time in the future + * Transient error handling + +```csharp +var jobScheduler = resolver.Resolve(); +var job = PublishCommandJob.Create(new SendEmailCommand(id), resolver); +await jobScheduler.ScheduleAsync( + job, + TimeSpan.FromDays(7), + CancellationToken.None) + .ConfigureAwait(false); +``` + +In the above example the `SendEmailCommand` command will be published in seven +days. + +## Be careful when using jobs + +When working with jobs, you should be aware of the following + + * The default implementation does executes the job _now_, i.e., in the + current context. To get another behavior, install e.g. `EventFlow.Hangfire` + to get support for scheduled jobs. Read below for details on how to + configure Hangfire + * Your jobs should serialize to JSON properly, see the section on + [value objects](./ValueObjects.md) for more information + * If you use the provided `PublishCommandJob`, make sure that your commands + serialize properly as well + +## Create your own jobs + +To create your own jobs, your job merely needs to implement the `IJob` +interface and be registered in EventFlow. + +Here's an example of a job implementing `IJob` + +```csharp +[JobVersion("LogMessage", 1)] +public class LogMessageJob : IJob +{ + public LogMessageJob(string message) + { + Message = message; + } + + public string Message { get; } + + public Task ExecuteAsync( + IResolver resolver, + CancellationToken cancellationToken) + { + var log = resolver.Resolve(); + log.Debug(Message); + } +} +``` + +Note that the `JobVersion` attribute specifies the job name and version to +EventFlow and this is how EventFlow distinguishes between the different job +types. This makes it possible for you to reorder your code, even rename the +job type, as long as you keep the same attribute values its considered the +same job in EventFlow. If the attribute is omitted, the name will be the +type name and version will be `1`. + +Here's how the job is registered in EventFlow. + +```csharp +var resolver = EventFlowOptions.new + .AddJobs(typeof(LogMessageJob)) + ... + .CreateResolver(); +``` + +Then to schedule the job + +```csharp +var jobScheduler = resolver.Resolve(); +var job = new LogMessageJob("Great log message"); +await jobScheduler.ScheduleAsync( + job, + TimeSpan.FromDays(7), + CancellationToken.None) + .ConfigureAwait(false); +``` + +## Hangfire + +To use [Hangfire](http://hangfire.io/) as the job scheduler, install the NuGet +package `EventFlow.Hangfire` and configure EventFlow to use the scheduler +like this. + +```csharp +var resolver = EventFlowOptions.new + .UseHandfireJobScheduler() // This line + ... + .CreateResolver(); +``` + +Note that the `UseHandfireJobScheduler()` does do any Hangfire configuration, +but merely registers the proper scheduler in EventFlow. diff --git a/EventFlow.sln b/EventFlow.sln index af74ac383..310cb75db 100644 --- a/EventFlow.sln +++ b/EventFlow.sln @@ -43,6 +43,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.Autofac.Tests", " EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Autofac", "Autofac", "{980EEDAA-1FEF-4D7C-8811-5EF1D9729773}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Hangfire", "Hangfire", "{4741A405-DA64-40CC-A726-A2C48CA49DA3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.Hangfire", "Source\EventFlow.Hangfire\EventFlow.Hangfire.csproj", "{FB079985-722A-43C9-9F14-C7D2AFBE8826}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventFlow.Hangfire.Tests", "Source\EventFlow.Hangfire.Tests\EventFlow.Hangfire.Tests.csproj", "{35180FC7-9135-4E79-8643-0FB825B40871}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -109,6 +115,14 @@ Global {EDCD8854-6224-4329-87C2-9ADD7D153070}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDCD8854-6224-4329-87C2-9ADD7D153070}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDCD8854-6224-4329-87C2-9ADD7D153070}.Release|Any CPU.Build.0 = Release|Any CPU + {FB079985-722A-43C9-9F14-C7D2AFBE8826}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FB079985-722A-43C9-9F14-C7D2AFBE8826}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FB079985-722A-43C9-9F14-C7D2AFBE8826}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FB079985-722A-43C9-9F14-C7D2AFBE8826}.Release|Any CPU.Build.0 = Release|Any CPU + {35180FC7-9135-4E79-8643-0FB825B40871}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {35180FC7-9135-4E79-8643-0FB825B40871}.Debug|Any CPU.Build.0 = Debug|Any CPU + {35180FC7-9135-4E79-8643-0FB825B40871}.Release|Any CPU.ActiveCfg = Release|Any CPU + {35180FC7-9135-4E79-8643-0FB825B40871}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -126,5 +140,7 @@ Global {4B06F01F-ACE6-489D-A92A-012F533EFA3C} = {7951DC73-5DAF-4322-9AF0-099BF5C90837} {BC96BEAE-E84E-4C51-B66D-DA1F43EAD54A} = {7951DC73-5DAF-4322-9AF0-099BF5C90837} {EDCD8854-6224-4329-87C2-9ADD7D153070} = {980EEDAA-1FEF-4D7C-8811-5EF1D9729773} + {FB079985-722A-43C9-9F14-C7D2AFBE8826} = {4741A405-DA64-40CC-A726-A2C48CA49DA3} + {35180FC7-9135-4E79-8643-0FB825B40871} = {4741A405-DA64-40CC-A726-A2C48CA49DA3} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 070c0d0cb..0633624ee 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,9 @@ to the documentation. * Microsoft SQL Server * [**Queries:**](./Documentation/Queries.md) Value objects that represent a query without specifying how its executed, that is let to a query handler +* [**Jobs:**](./Documentation/Jobs.md) Perform scheduled tasks at a later time, + e.g. publish a command. EventFlow provides support for these job schedulers + * [Hangfire](./Documentation/Jobs.md#hangfire) - [home page](http://hangfire.io/) * [**Event upgrade:**](./Documentation/EventUpgrade.md) As events committed to the event store is never changed, EventFlow uses the concept of event upgraders to deprecate events and replace them with new during aggregate load. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7d27199fc..b9bc306ae 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,4 +1,29 @@ -### New in 0.13 (not released yet) +### New in 0.14 (not released yet) + +* Breaking: All `EventFlowOptions` extensions are now `IEventFlowOptions` + instead and `EventFlowOptions` implements this interface. If you have made + your own extensions, you will need to use the newly created interface + instead. Changed in order to make testing of extensions and classes + dependent on the EventFlow options easier to test +* New: You can now bundle your configuration of EventFlow into modules that + implement `IModule` and register these by calling + `EventFlowOptions.RegisterModule(...)` +* New: EventFlow now supports scheduled job execution via e.g. Hangfire. You + can create your own scheduler or install the new `EventFlow.Hangfire` NuGet + package. Read the jobs documentation for more details +* New: Created the OWIN `CommandPublishMiddleware` middleware that can + handle publishing of commands by posting a JSON serialized command to + e.g. `/commands/ping/1` in which `ping` is the command name and `1` its + version. Remember to add authentication +* New: Created a new interface `ICommand` + to allow developers to control the type of `ICommand.SourceId`. Using the + `ICommand` (or Command) + will still yield the same result as before, i.e., `ICommand.SourceId` being + of type `ISourceId` +* New: The `AddDefaults(...)` now also adds the command type definition to the + new `ICommandDefinitonService` + +### New in 0.13.962 (released 2015-09-13) * Breaking: `EventFlowOptions.AddDefaults(...)` now also adds query handlers * New: Added an optional `Predicate` to the following option extension diff --git a/Source/EventFlow.Autofac/Extensions/EventFlowOptionsAutofacExtensions.cs b/Source/EventFlow.Autofac/Extensions/EventFlowOptionsAutofacExtensions.cs index a01176fa5..b1b7848e8 100644 --- a/Source/EventFlow.Autofac/Extensions/EventFlowOptionsAutofacExtensions.cs +++ b/Source/EventFlow.Autofac/Extensions/EventFlowOptionsAutofacExtensions.cs @@ -31,30 +31,30 @@ namespace EventFlow.Autofac.Extensions { public static class EventFlowOptionsAutofacExtensions { - public static EventFlowOptions UseAutofacContainerBuilder( - this EventFlowOptions eventFlowOptions) + public static IEventFlowOptions UseAutofacContainerBuilder( + this IEventFlowOptions eventFlowOptions) { return eventFlowOptions .UseAutofacContainerBuilder(new ContainerBuilder()); } - public static EventFlowOptions UseAutofacContainerBuilder( - this EventFlowOptions eventFlowOptions, + public static IEventFlowOptions UseAutofacContainerBuilder( + this IEventFlowOptions eventFlowOptions, ContainerBuilder containerBuilder) { return eventFlowOptions .UseServiceRegistration(new AutofacServiceRegistration(containerBuilder)); } - public static EventFlowOptions UseAutofacAggregateRootFactory( - this EventFlowOptions eventFlowOptions) + public static IEventFlowOptions UseAutofacAggregateRootFactory( + this IEventFlowOptions eventFlowOptions) { return eventFlowOptions .RegisterServices(f => f.Register(Lifetime.Singleton)); } public static IContainer CreateContainer( - this EventFlowOptions eventFlowOptions, + this IEventFlowOptions eventFlowOptions, bool validateRegistrations = true) { var rootResolver = eventFlowOptions.CreateResolver(validateRegistrations); diff --git a/Source/EventFlow.EventStores.EventStore.Tests/IntegrationTests/EventStoreEventStoreTestConfiguration.cs b/Source/EventFlow.EventStores.EventStore.Tests/IntegrationTests/EventStoreEventStoreTestConfiguration.cs index a4f0979db..b21f7093e 100644 --- a/Source/EventFlow.EventStores.EventStore.Tests/IntegrationTests/EventStoreEventStoreTestConfiguration.cs +++ b/Source/EventFlow.EventStores.EventStore.Tests/IntegrationTests/EventStoreEventStoreTestConfiguration.cs @@ -43,7 +43,7 @@ public class EventStoreEventStoreTestConfiguration : IntegrationTestConfiguratio private IQueryProcessor _queryProcessor; private IReadModelPopulator _readModelPopulator; - public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) + public override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) { var connectionSettings = ConnectionSettings.Create() .EnableVerboseLogging() diff --git a/Source/EventFlow.EventStores.EventStore/Extensions/EventFlowOptionsExtensions.cs b/Source/EventFlow.EventStores.EventStore/Extensions/EventFlowOptionsExtensions.cs index f0f51101c..4dd520aa7 100644 --- a/Source/EventFlow.EventStores.EventStore/Extensions/EventFlowOptionsExtensions.cs +++ b/Source/EventFlow.EventStores.EventStore/Extensions/EventFlowOptionsExtensions.cs @@ -31,22 +31,22 @@ namespace EventFlow.EventStores.EventStore.Extensions { public static class EventFlowOptionsExtensions { - public static EventFlowOptions UseEventStoreEventStore( - this EventFlowOptions eventFlowOptions) + public static IEventFlowOptions UseEventStoreEventStore( + this IEventFlowOptions eventFlowOptions) { return eventFlowOptions.UseEventStore(); } - public static EventFlowOptions UseEventStoreEventStore( - this EventFlowOptions eventFlowOptions, + public static IEventFlowOptions UseEventStoreEventStore( + this IEventFlowOptions eventFlowOptions, IPEndPoint ipEndPoint) { return eventFlowOptions .UseEventStoreEventStore(ipEndPoint, ConnectionSettings.Default); } - public static EventFlowOptions UseEventStoreEventStore( - this EventFlowOptions eventFlowOptions, + public static IEventFlowOptions UseEventStoreEventStore( + this IEventFlowOptions eventFlowOptions, IPEndPoint ipEndPoint, ConnectionSettings connectionSettings) { diff --git a/Source/EventFlow.EventStores.MsSql/Extensions/EventFlowOptionsExtensions.cs b/Source/EventFlow.EventStores.MsSql/Extensions/EventFlowOptionsExtensions.cs index 56524d77f..123eabd38 100644 --- a/Source/EventFlow.EventStores.MsSql/Extensions/EventFlowOptionsExtensions.cs +++ b/Source/EventFlow.EventStores.MsSql/Extensions/EventFlowOptionsExtensions.cs @@ -26,7 +26,7 @@ namespace EventFlow.EventStores.MsSql.Extensions { public static class EventFlowOptionsExtensions { - public static EventFlowOptions UseMssqlEventStore(this EventFlowOptions eventFlowOptions) + public static IEventFlowOptions UseMssqlEventStore(this IEventFlowOptions eventFlowOptions) { return eventFlowOptions.UseEventStore(); } diff --git a/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj new file mode 100644 index 000000000..72da62d11 --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/EventFlow.Hangfire.Tests.csproj @@ -0,0 +1,106 @@ + + + + + Debug + AnyCPU + {35180FC7-9135-4E79-8643-0FB825B40871} + Library + Properties + EventFlow.Hangfire.Tests + EventFlow.Hangfire.Tests + v4.5.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.dll + True + + + ..\..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.Core.dll + True + + + ..\..\packages\Hangfire.Core.1.4.6\lib\net45\Hangfire.Core.dll + True + + + ..\..\packages\Hangfire.SqlServer.1.4.6\lib\net45\Hangfire.SqlServer.dll + True + + + ..\..\packages\Helpz.0.1.8\lib\net451\Helpz.dll + True + + + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + True + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + + + + + + + + + + + + + + + {fb079985-722a-43c9-9f14-c7d2afbe8826} + EventFlow.Hangfire + + + {571d291c-5e4c-43af-855f-7c4e2f318f4c} + EventFlow.TestHelpers + + + {11131251-778d-4d2e-bdd1-4844a789bca9} + EventFlow + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobFlow.cs b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobFlow.cs new file mode 100644 index 000000000..2adb0d489 --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/Integration/HangfireJobFlow.cs @@ -0,0 +1,98 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.EventStores; +using EventFlow.Extensions; +using EventFlow.Hangfire.Extensions; +using EventFlow.Hangfire.Integration; +using EventFlow.Jobs; +using EventFlow.Provided.Jobs; +using EventFlow.TestHelpers; +using EventFlow.TestHelpers.Aggregates.Test; +using EventFlow.TestHelpers.Aggregates.Test.Commands; +using EventFlow.TestHelpers.Aggregates.Test.ValueObjects; +using Hangfire; +using Helpz.MsSql; +using NUnit.Framework; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.Hangfire.Tests.Integration +{ + public class HangfireJobFlow : Test + { + private IMsSqlDatabase _msSqlDatabase; + + [SetUp] + public void SetUp() + { + _msSqlDatabase = MsSqlHelpz.CreateDatabase("hangfire"); + } + + [TearDown] + public void TearDown() + { + _msSqlDatabase.Dispose(); + } + + [Test] + public async Task Flow() + { + using (var resolver = EventFlowOptions.New + .AddDefaults(EventFlowTestHelpers.Assembly) + .UseHandfireJobScheduler() + .CreateResolver(false)) + { + GlobalConfiguration.Configuration + .UseSqlServerStorage(_msSqlDatabase.ConnectionString.Value) + .UseActivator(new EventFlowResolverActivator(resolver)); + + using (new BackgroundJobServer()) + { + // Arrange + var testId = TestId.New; + var pingId = PingId.New; + var jobScheduler = resolver.Resolve(); + var eventStore = resolver.Resolve(); + var executeCommandJob = PublishCommandJob.Create(new PingCommand(testId, pingId), resolver); + + // Act + await jobScheduler.ScheduleNowAsync(executeCommandJob, CancellationToken.None).ConfigureAwait(false); + + // Assert + var start = DateTimeOffset.Now; + while (DateTimeOffset.Now < start + TimeSpan.FromSeconds(20)) + { + var testAggregate = await eventStore.LoadAggregateAsync(testId, CancellationToken.None).ConfigureAwait(false); + if (!testAggregate.IsNew) + { + Assert.Pass(); + } + Thread.Sleep(TimeSpan.FromSeconds(0.2)); + } + Assert.Fail(); + } + } + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Hangfire.Tests/Properties/AssemblyInfo.cs b/Source/EventFlow.Hangfire.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..0168c4090 --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EventFlow.Hangfire.Tests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EventFlow.Hangfire.Tests")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("35180fc7-9135-4e79-8643-0fb825b40871")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Source/EventFlow.Hangfire.Tests/app.config b/Source/EventFlow.Hangfire.Tests/app.config new file mode 100644 index 000000000..73b58e512 --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Hangfire.Tests/packages.config b/Source/EventFlow.Hangfire.Tests/packages.config new file mode 100644 index 000000000..747ac930b --- /dev/null +++ b/Source/EventFlow.Hangfire.Tests/packages.config @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj b/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj new file mode 100644 index 000000000..424e598ad --- /dev/null +++ b/Source/EventFlow.Hangfire/EventFlow.Hangfire.csproj @@ -0,0 +1,84 @@ + + + + + Debug + AnyCPU + {FB079985-722A-43C9-9F14-C7D2AFBE8826} + Library + Properties + EventFlow.Hangfire + EventFlow.Hangfire + v4.5.1 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\..\packages\Hangfire.Core.1.4.6\lib\net45\Hangfire.Core.dll + True + + + ..\..\packages\Newtonsoft.Json.7.0.1\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + + + + + + + + + + + Properties\SolutionInfo.cs + + + + + + + + + + {11131251-778d-4d2e-bdd1-4844a789bca9} + EventFlow + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/EventFlow.Hangfire.nuspec b/Source/EventFlow.Hangfire/EventFlow.Hangfire.nuspec new file mode 100644 index 000000000..da8f9bb95 --- /dev/null +++ b/Source/EventFlow.Hangfire/EventFlow.Hangfire.nuspec @@ -0,0 +1,23 @@ + + + + EventFlow.Hangfire + EventFlow.Hangfire + 0.0.0 + rasmus + Hangfire job scheduling support for EventFlow + en-US + https://raw.githubusercontent.com/rasmus/EventFlow/master/icon-256.png + https://github.com/rasmus/EventFlow + https://raw.githubusercontent.com/rasmus/EventFlow/master/LICENSE + Copyright (c) 2015 Rasmus Mikkelsen + true + CQRS ES event sourceing eventstore hangfire job scheduling tasks + @releaseNotes@ + @dependencies@ + @references@ + + + + + diff --git a/Source/EventFlow.Hangfire/Extensions/EventFlowOptionsHangfireExtensions.cs b/Source/EventFlow.Hangfire/Extensions/EventFlowOptionsHangfireExtensions.cs new file mode 100644 index 000000000..7dc8f1f76 --- /dev/null +++ b/Source/EventFlow.Hangfire/Extensions/EventFlowOptionsHangfireExtensions.cs @@ -0,0 +1,41 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Hangfire.Integration; +using EventFlow.Jobs; +using Hangfire; + +namespace EventFlow.Hangfire.Extensions +{ + public static class EventFlowOptionsHangfireExtensions + { + public static IEventFlowOptions UseHandfireJobScheduler( + this IEventFlowOptions eventFlowOptions) + { + return eventFlowOptions.RegisterServices(sr => + { + sr.Register(); + sr.Register(r => new BackgroundJobClient()); + }); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/Integration/EventFlowResolverActivator.cs b/Source/EventFlow.Hangfire/Integration/EventFlowResolverActivator.cs new file mode 100644 index 000000000..a69354ea5 --- /dev/null +++ b/Source/EventFlow.Hangfire/Integration/EventFlowResolverActivator.cs @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Configuration; +using Hangfire; +using System; + +namespace EventFlow.Hangfire.Integration +{ + public class EventFlowResolverActivator : JobActivator + { + private readonly IResolver _resolver; + + public EventFlowResolverActivator(IResolver resolver) + { + _resolver = resolver; + } + + public override object ActivateJob(Type jobType) + { + return _resolver.Resolve(jobType); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobId.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobId.cs new file mode 100644 index 000000000..4062005a4 --- /dev/null +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobId.cs @@ -0,0 +1,36 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Jobs; +using EventFlow.ValueObjects; +using System; + +namespace EventFlow.Hangfire.Integration +{ + public class HangfireJobId : SingleValueObject, IJobId + { + public HangfireJobId(string value) : base(value) + { + if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value)); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs new file mode 100644 index 000000000..a1b0efc3b --- /dev/null +++ b/Source/EventFlow.Hangfire/Integration/HangfireJobScheduler.cs @@ -0,0 +1,79 @@ +// The MIT License (MIT) +// +// Copyright (c) 2015 Rasmus Mikkelsen +// https://github.com/rasmus/EventFlow +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using EventFlow.Core; +using EventFlow.Jobs; +using EventFlow.Logs; +using Hangfire; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EventFlow.Hangfire.Integration +{ + public class HangfireJobScheduler : IJobScheduler + { + private readonly IBackgroundJobClient _backgroundJobClient; + private readonly IJobDefinitionService _jobDefinitionService; + private readonly IJsonSerializer _jsonSerializer; + private readonly ILog _log; + + public HangfireJobScheduler( + ILog log, + IJsonSerializer jsonSerializer, + IBackgroundJobClient backgroundJobClient, + IJobDefinitionService jobDefinitionService) + { + _log = log; + _jsonSerializer = jsonSerializer; + _backgroundJobClient = backgroundJobClient; + _jobDefinitionService = jobDefinitionService; + } + + public Task ScheduleNowAsync(IJob job, CancellationToken cancellationToken) + { + return ScheduleAsync(job, (c, d, j) => _backgroundJobClient.Enqueue(r => r.Execute(d.Name, d.Version, j))); + } + + public Task ScheduleAsync(IJob job, DateTimeOffset runAt, CancellationToken cancellationToken) + { + return ScheduleAsync(job, (c, d, j) => _backgroundJobClient.Schedule(r => r.Execute(d.Name, d.Version, j), runAt)); + } + + public Task ScheduleAsync(IJob job, TimeSpan delay, CancellationToken cancellationToken) + { + return ScheduleAsync(job, (c, d, j) => _backgroundJobClient.Schedule(r => r.Execute(d.Name, d.Version, j), delay)); + } + + private Task ScheduleAsync(IJob job, Func schedule) + { + var jobDefinition = _jobDefinitionService.GetJobDefinition(job.GetType()); + var json = _jsonSerializer.Serialize(job); + + var id = schedule(_backgroundJobClient, jobDefinition, json); + + _log.Verbose($"Scheduled job '{id}' in Hangfire"); + + return Task.FromResult(new HangfireJobId(id)); + } + } +} \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/Properties/AssemblyInfo.cs b/Source/EventFlow.Hangfire/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..f0cef91f3 --- /dev/null +++ b/Source/EventFlow.Hangfire/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EventFlow.Hangfire")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EventFlow.Hangfire")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] diff --git a/Source/EventFlow.Hangfire/app.config b/Source/EventFlow.Hangfire/app.config new file mode 100644 index 000000000..195db1f42 --- /dev/null +++ b/Source/EventFlow.Hangfire/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.Hangfire/packages.config b/Source/EventFlow.Hangfire/packages.config new file mode 100644 index 000000000..3c8de0ec0 --- /dev/null +++ b/Source/EventFlow.Hangfire/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj b/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj index 3cb11aa60..a0f9ae33e 100644 --- a/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj +++ b/Source/EventFlow.MsSql.Tests/EventFlow.MsSql.Tests.csproj @@ -40,6 +40,10 @@ ..\..\packages\FluentAssertions.3.5.0\lib\net45\FluentAssertions.Core.dll True + + ..\..\packages\Helpz.0.1.8\lib\net451\Helpz.dll + True + ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll True @@ -70,8 +74,6 @@ - - @@ -105,7 +107,9 @@ EventFlow - + + + diff --git a/Source/EventFlow.MsSql.Tests/Helpers/MsSqlHelper.cs b/Source/EventFlow.MsSql.Tests/Helpers/MsSqlHelper.cs deleted file mode 100644 index c2c9406b7..000000000 --- a/Source/EventFlow.MsSql.Tests/Helpers/MsSqlHelper.cs +++ /dev/null @@ -1,83 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Collections.Generic; -using System.Data.SqlClient; -using EventFlow.MsSql.Tests.Extensions; - -namespace EventFlow.MsSql.Tests.Helpers -{ - public static class MsSqlHelper - { - public static ITestDatabase CreateDatabase(string partialDatabaseName) - { - var connectionstring = GetConnectionstring(partialDatabaseName); - var masterConnectionstring = connectionstring.ReplaceDatabaseInConnectionstring("master"); - var testDatabaseName = connectionstring.GetDatabaseInConnectionstring(); - - using (var sqlConnection = new SqlConnection(masterConnectionstring)) - { - sqlConnection.Open(); - Console.WriteLine("MssqlHelper: Creating database '{0}'", testDatabaseName); - var sql = $"CREATE DATABASE [{testDatabaseName}]"; - using (var sqlCommand = new SqlCommand(sql, sqlConnection)) - { - sqlCommand.ExecuteNonQuery(); - } - } - - return new TestDatabase(connectionstring); - } - - public static string GetConnectionstring(string partialDatabaseName) - { - var databaseName = string.Format( - "Test_{0}_{1}_{2}", - partialDatabaseName, - DateTime.Now.ToString("yyyy-MM-dd-HH-mm"), - Guid.NewGuid()); - - var connectionstringParts = new List - { - $"Database={databaseName}", - }; - - var environmentServer = Environment.GetEnvironmentVariable("MSSQL_SERVER"); - var environmentPassword = Environment.GetEnvironmentVariable("MSSQL_PASS"); - var envrionmentUsername = Environment.GetEnvironmentVariable("MSSQL_USER"); - - connectionstringParts.Add(string.IsNullOrEmpty(environmentServer) - ? @"Server=localhost\SQLEXPRESS" - : $"Server={environmentServer}"); - connectionstringParts.Add(string.IsNullOrEmpty(envrionmentUsername) - ? @"Integrated Security=True" - : $"User Id={envrionmentUsername}"); - if (!string.IsNullOrEmpty(environmentPassword)) - { - connectionstringParts.Add($"Password={environmentPassword}"); - } - - return string.Join(";", connectionstringParts); - } - } -} diff --git a/Source/EventFlow.MsSql.Tests/Helpers/TestDatabase.cs b/Source/EventFlow.MsSql.Tests/Helpers/TestDatabase.cs deleted file mode 100644 index 4136249b6..000000000 --- a/Source/EventFlow.MsSql.Tests/Helpers/TestDatabase.cs +++ /dev/null @@ -1,79 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2015 Rasmus Mikkelsen -// https://github.com/rasmus/EventFlow -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of -// this software and associated documentation files (the "Software"), to deal in -// the Software without restriction, including without limitation the rights to -// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -// the Software, and to permit persons to whom the Software is furnished to do so, -// subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -using System; -using System.Data.SqlClient; -using EventFlow.MsSql.Tests.Extensions; - -namespace EventFlow.MsSql.Tests.Helpers -{ - public interface ITestDatabase : IDisposable - { - string ConnectionString { get; } - void Execute(string sql); - } - - public class TestDatabase : ITestDatabase - { - public string ConnectionString { get; private set; } - public SqlConnection SqlConnection { get; private set; } - public string Name { get; private set; } - - public TestDatabase(string connectionString) - { - ConnectionString = connectionString; - Name = connectionString.GetDatabaseInConnectionstring(); - - SqlConnection = new SqlConnection(ConnectionString); - SqlConnection.Open(); - } - - public void Execute(string sql) - { - using (var sqlCommand = new SqlCommand(sql, SqlConnection)) - { - sqlCommand.ExecuteNonQuery(); - } - } - - public void Dispose() - { - Console.WriteLine("MssqlHelper: Deleting test database '{0}'", Name); - - var masterConnectionString = ConnectionString.ReplaceDatabaseInConnectionstring("master"); - var sql = string.Format( - "ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;DROP DATABASE [{0}];", - Name); - - using (var sqlConnection = new SqlConnection(masterConnectionString)) - { - sqlConnection.Open(); - using (var sqlCommand = new SqlCommand(sql, sqlConnection)) - { - sqlCommand.ExecuteNonQuery(); - } - } - - SqlConnection.Dispose(); - } - } -} diff --git a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs index 0b9ad9d9f..aa380db90 100644 --- a/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs +++ b/Source/EventFlow.MsSql.Tests/IntegrationTests/MsSqlIntegrationTestConfiguration.cs @@ -23,35 +23,34 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using EventFlow.Aggregates; using EventFlow.Configuration; using EventFlow.Core; using EventFlow.EventStores.MsSql; using EventFlow.Extensions; using EventFlow.MsSql.Extensions; -using EventFlow.MsSql.Tests.Helpers; using EventFlow.MsSql.Tests.ReadModels; using EventFlow.ReadStores; using EventFlow.ReadStores.MsSql; using EventFlow.ReadStores.MsSql.Extensions; using EventFlow.TestHelpers; using EventFlow.TestHelpers.Aggregates.Test.ReadModels; +using Helpz.MsSql; namespace EventFlow.MsSql.Tests.IntegrationTests { public class MsSqlIntegrationTestConfiguration : IntegrationTestConfiguration { - protected ITestDatabase TestDatabase { get; private set; } + protected IMsSqlDatabase TestDatabase { get; private set; } protected IMsSqlConnection MsSqlConnection { get; private set; } protected IReadModelSqlGenerator ReadModelSqlGenerator { get; private set; } protected IReadModelPopulator ReadModelPopulator { get; private set; } - public override IRootResolver CreateRootResolver(EventFlowOptions eventFlowOptions) + public override IRootResolver CreateRootResolver(IEventFlowOptions eventFlowOptions) { - TestDatabase = MsSqlHelper.CreateDatabase("eventflow"); + TestDatabase = MsSqlHelpz.CreateDatabase("eventflow"); var resolver = eventFlowOptions - .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(TestDatabase.ConnectionString)) + .ConfigureMsSql(MsSqlConfiguration.New.SetConnectionString(TestDatabase.ConnectionString.Value)) .UseEventStore() .UseMssqlReadModel() .CreateResolver(); diff --git a/Source/EventFlow.MsSql.Tests/packages.config b/Source/EventFlow.MsSql.Tests/packages.config index b7decb6c3..b5ee06bda 100644 --- a/Source/EventFlow.MsSql.Tests/packages.config +++ b/Source/EventFlow.MsSql.Tests/packages.config @@ -3,6 +3,7 @@ + diff --git a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsExtensions.cs b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsExtensions.cs index 86104fcc0..87147dd67 100644 --- a/Source/EventFlow.MsSql/Extensions/EventFlowOptionsExtensions.cs +++ b/Source/EventFlow.MsSql/Extensions/EventFlowOptionsExtensions.cs @@ -27,7 +27,7 @@ namespace EventFlow.MsSql.Extensions { public static class EventFlowOptionsExtensions { - public static EventFlowOptions ConfigureMsSql(this EventFlowOptions eventFlowOptions, IMsSqlConfiguration msSqlConfiguration) + public static IEventFlowOptions ConfigureMsSql(this IEventFlowOptions eventFlowOptions, IMsSqlConfiguration msSqlConfiguration) { return eventFlowOptions.RegisterServices(f => { diff --git a/Source/EventFlow.Owin.Tests/EventFlow.Owin.Tests.csproj b/Source/EventFlow.Owin.Tests/EventFlow.Owin.Tests.csproj index bf03385a7..98b512ae3 100644 --- a/Source/EventFlow.Owin.Tests/EventFlow.Owin.Tests.csproj +++ b/Source/EventFlow.Owin.Tests/EventFlow.Owin.Tests.csproj @@ -122,6 +122,9 @@ + + +