diff --git a/LiquidProjections.sln b/LiquidProjections.sln index 4b6baab..e71f894 100644 --- a/LiquidProjections.sln +++ b/LiquidProjections.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26430.13 +VisualStudioVersion = 15.0.26430.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{27C8B175-6555-4591-87B1-177A2874FEA9}" ProjectSection(SolutionItems) = preProject @@ -18,11 +18,13 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidProjections.Specs", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExampleHost", "Samples\ExampleHost\ExampleHost.csproj", "{1B10C576-5212-402E-AC19-8CA1547E3F34}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidProjections", "Src\LiquidProjections\LiquidProjections.csproj", "{7B47454D-0129-43CB-AED5-27AFAFDB5D7C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiquidProjections", "Src\LiquidProjections\LiquidProjections.csproj", "{7B47454D-0129-43CB-AED5-27AFAFDB5D7C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidProjections.Testing", "Src\LiquidProjections.Testing\LiquidProjections.Testing.csproj", "{B4EA7831-8282-4F3A-A057-2DEB59611A68}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiquidProjections.Testing", "Src\LiquidProjections.Testing\LiquidProjections.Testing.csproj", "{B4EA7831-8282-4F3A-A057-2DEB59611A68}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidProjections.Abstractions", "Src\LiquidProjections.Abstractions\LiquidProjections.Abstractions.csproj", "{8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiquidProjections.Abstractions", "Src\LiquidProjections.Abstractions\LiquidProjections.Abstractions.csproj", "{8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiquidProjections.Owin", "Src\LiquidProjections.Owin\LiquidProjections.Owin.csproj", "{43C0A9ED-8094-4646-85A4-6EAA03505A20}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -50,6 +52,10 @@ Global {8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D}.Debug|Any CPU.Build.0 = Debug|Any CPU {8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D}.Release|Any CPU.ActiveCfg = Release|Any CPU {8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D}.Release|Any CPU.Build.0 = Release|Any CPU + {43C0A9ED-8094-4646-85A4-6EAA03505A20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43C0A9ED-8094-4646-85A4-6EAA03505A20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43C0A9ED-8094-4646-85A4-6EAA03505A20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43C0A9ED-8094-4646-85A4-6EAA03505A20}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -60,5 +66,6 @@ Global {7B47454D-0129-43CB-AED5-27AFAFDB5D7C} = {AE89AB5E-BC68-4AA3-9183-A222AC81691C} {B4EA7831-8282-4F3A-A057-2DEB59611A68} = {AE89AB5E-BC68-4AA3-9183-A222AC81691C} {8E38C862-7DC9-4A7E-A2EE-921FFBC7EE2D} = {AE89AB5E-BC68-4AA3-9183-A222AC81691C} + {43C0A9ED-8094-4646-85A4-6EAA03505A20} = {AE89AB5E-BC68-4AA3-9183-A222AC81691C} EndGlobalSection EndGlobal diff --git a/Samples/ExampleHost/App.config b/Samples/ExampleHost/App.config index 7c164fe..13c296d 100644 --- a/Samples/ExampleHost/App.config +++ b/Samples/ExampleHost/App.config @@ -1,7 +1,7 @@  - + diff --git a/Samples/ExampleHost/CountsProjector.cs b/Samples/ExampleHost/CountsProjector.cs index 7da5c41..b873c04 100644 --- a/Samples/ExampleHost/CountsProjector.cs +++ b/Samples/ExampleHost/CountsProjector.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using LiquidProjections.ExampleHost.Events; +using LiquidProjections.Statistics; namespace LiquidProjections.ExampleHost { @@ -9,13 +10,15 @@ public class CountsProjector { private readonly Dispatcher dispatcher; private readonly InMemoryDatabase store; + private readonly ProjectionStats stats; private ExampleProjector documentCountProjector; private ExampleProjector countryLookupProjector; - public CountsProjector(Dispatcher dispatcher, InMemoryDatabase store) + public CountsProjector(Dispatcher dispatcher, InMemoryDatabase store, ProjectionStats stats) { this.dispatcher = dispatcher; this.store = store; + this.stats = stats; BuildCountryProjector(); BuildDocumentProjector(); @@ -210,8 +213,11 @@ private void BuildDocumentProjector() period.Status = "Canceled"; }); - documentCountProjector = - new ExampleProjector(documentMapBuilder, store, countryLookupProjector); + documentCountProjector = + new ExampleProjector(documentMapBuilder, store, stats, countryLookupProjector) + { + Id = "DocumentCount" + }; } private string GetCountryName(Guid countryCode) @@ -229,7 +235,10 @@ private void BuildCountryProjector() .AsCreateOf(anEvent => anEvent.Code) .Using((country, anEvent) => country.Name = anEvent.Name); - countryLookupProjector = new ExampleProjector(countryMapBuilder, store); + countryLookupProjector = new ExampleProjector(countryMapBuilder, store, stats) + { + Id = "CountryLookup" + }; } private static IEnumerable GetPreviousContiguousValidPeriods(List allPeriods, diff --git a/Samples/ExampleHost/ExampleHost.csproj b/Samples/ExampleHost/ExampleHost.csproj index c03f633..bcbebf8 100644 --- a/Samples/ExampleHost/ExampleHost.csproj +++ b/Samples/ExampleHost/ExampleHost.csproj @@ -9,7 +9,7 @@ Properties LiquidProjections.ExampleHost LiquidProjections.ExampleHost - v4.5 + v4.5.2 512 @@ -135,11 +135,18 @@ {8e38c862-7dc9-4a7e-a2ee-921ffbc7ee2d} LiquidProjections.Abstractions + + {43c0a9ed-8094-4646-85a4-6eaa03505a20} + LiquidProjections.Owin + {7b47454d-0129-43cb-aed5-27afafdb5d7c} LiquidProjections + + + diff --git a/Samples/ExampleHost/ExampleProjector.cs b/Samples/ExampleHost/ExampleProjector.cs index c58eb40..7b4855f 100644 --- a/Samples/ExampleHost/ExampleProjector.cs +++ b/Samples/ExampleHost/ExampleProjector.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using LiquidProjections.Statistics; namespace LiquidProjections.ExampleHost { @@ -13,14 +13,16 @@ public class ExampleProjector : IExampleProjector where TProjection : class, IEntity, new() { private readonly InMemoryDatabase store; - private readonly Projector innerProjector; + private readonly ProjectionStats stats; - public ExampleProjector(IEventMapBuilder mapBuilder, InMemoryDatabase store, params IExampleProjector[] childProjectors) + public ExampleProjector(IEventMapBuilder mapBuilder, InMemoryDatabase store, + ProjectionStats stats, params IExampleProjector[] childProjectors) { this.store = store; + this.stats = stats; var map = BuildMapFrom(mapBuilder); - innerProjector = new Projector(map, childProjectors.Select(p => p.InnerProjector)); + InnerProjector = new Projector(map, childProjectors.Select(p => p.InnerProjector)); } private IEventMap BuildMapFrom(IEventMapBuilder mapBuilder) @@ -53,16 +55,20 @@ private IEventMap BuildMapFrom(IEventMapBuilder transactions) + public async Task Handle(IReadOnlyList transactions) { - return innerProjector.Handle(transactions); + await InnerProjector.Handle(transactions); + + stats.TrackProgress(Id, transactions.Last().Checkpoint); } - public Projector InnerProjector => innerProjector; + public Projector InnerProjector { get; } + + public string Id { get; set; } } public interface IExampleProjector { Projector InnerProjector { get; } } -} +} \ No newline at end of file diff --git a/Samples/ExampleHost/Program.cs b/Samples/ExampleHost/Program.cs index 4d207f4..a0cc4f3 100644 --- a/Samples/ExampleHost/Program.cs +++ b/Samples/ExampleHost/Program.cs @@ -3,6 +3,8 @@ using System.IO; using System.Web.Http; using System.Web.Http.Dispatcher; +using LiquidProjections.Owin; +using LiquidProjections.Statistics; using Microsoft.Owin.Hosting; using Owin; using TinyIoC; @@ -22,10 +24,16 @@ public static void Main(string[] args) var dispatcher = new Dispatcher(eventStore.Subscribe); - var bootstrapper = new CountsProjector(dispatcher, projectionsStore); + var stats = new ProjectionStats(() => DateTime.UtcNow); + + var bootstrapper = new CountsProjector(dispatcher, projectionsStore, stats); var startOptions = new StartOptions($"http://localhost:9000"); - using (WebApp.Start(startOptions, builder => builder.UseControllers(container))) + using (WebApp.Start(startOptions, builder => + { + builder.UseControllers(container); + builder.UseLiquidProjections(stats); + })) { bootstrapper.Start(); diff --git a/Samples/ExampleHost/Properties/AssemblyInfo.cs b/Samples/ExampleHost/Properties/AssemblyInfo.cs index 9eb2d8a..648deb0 100644 --- a/Samples/ExampleHost/Properties/AssemblyInfo.cs +++ b/Samples/ExampleHost/Properties/AssemblyInfo.cs @@ -31,8 +31,8 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] +// [assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] -[assembly: AssemblyInformationalVersion("2.0.0+Branch.master.Sha.6e68f840c3303bac97b10c567815a3bfef3184d7")] +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] diff --git a/Src/LiquidProjections.Abstractions/AssemblyInfo.cs b/Src/LiquidProjections.Abstractions/AssemblyInfo.cs index 88280fa..ff6863c 100644 --- a/Src/LiquidProjections.Abstractions/AssemblyInfo.cs +++ b/Src/LiquidProjections.Abstractions/AssemblyInfo.cs @@ -7,6 +7,6 @@ [assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] -[assembly: AssemblyInformationalVersion("2.0.0+Branch.master.Sha.6e68f840c3303bac97b10c567815a3bfef3184d7")] \ No newline at end of file +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/.nuspec b/Src/LiquidProjections.Owin/.nuspec new file mode 100644 index 0000000..74814a5 --- /dev/null +++ b/Src/LiquidProjections.Owin/.nuspec @@ -0,0 +1,26 @@ + + + + LiquidProjections.Owin + 0.0.0.0 + Dennis Doomen + Dennis Doomen + https://github.com/liquidprojections/LiquidProjections + false + Provides OWIN Middleware to access LiquidProjection over HTTP. + event-sourcing; projections; ddd; owin; webapi + + + + + + + + + + + + + + + diff --git a/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs b/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs new file mode 100644 index 0000000..b2e1d90 --- /dev/null +++ b/Src/LiquidProjections.Owin/CustomNancyBootstrapper.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using LiquidProjections.Statistics; +using Nancy; +using Nancy.Bootstrapper; +using Nancy.Configuration; +using Nancy.Extensions; +using Nancy.Linker; +using Nancy.Metadata.Modules; +using Nancy.Routing; +using Nancy.Swagger; +using Nancy.Swagger.Modules; +using Nancy.Swagger.Services; +using Nancy.TinyIoc; + +namespace LiquidProjections.Owin +{ + internal class CustomNancyBootstrapper : DefaultNancyBootstrapper + { + private readonly ProjectionStats stats; + + public CustomNancyBootstrapper(ProjectionStats stats) + { + this.stats = stats; + } + + protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines) + { + base.ApplicationStartup(container, pipelines); + + SwaggerMetadataProvider.SetInfo( + "LiquidProjections", + Assembly.GetExecutingAssembly().GetName().Version.ToString(), + "Provides statistics about running projectors" + ); + } + +#if DEBUG + + public override void Configure(INancyEnvironment environment) + { + environment.Tracing(enabled: false, displayErrorTraces: true); + base.Configure(environment); + } +#endif + + protected override IAssemblyCatalog AssemblyCatalog => new StaticAssemblyCatalog(); + + protected override ITypeCatalog TypeCatalog => new InternalTypeCatalog(AssemblyCatalog); + + protected override void ConfigureApplicationContainer(TinyIoCContainer container) + { + container.Register((x, overloads) => + new ResourceLinker(x.Resolve(), + x.Resolve(), x.Resolve())); + + container.Register("LinkerRegistrations"); + container.Register("SwaggerRegistrations"); + container.Register(stats); + } + + protected override IEnumerable Modules => + new[] + { + new ModuleRegistration(typeof(SwaggerModule)), + new ModuleRegistration(typeof(StatisticsModule)) + }; + } + + internal class StaticAssemblyCatalog : IAssemblyCatalog + { + public IReadOnlyCollection GetAssemblies() + { + return new[] + { + typeof(MetadataModuleRegistrations).Assembly, + typeof(CustomNancyBootstrapper).Assembly, + typeof(DefaultNancyBootstrapper).Assembly + }.Distinct().ToArray(); + } + } + + /// + /// Implementation of the interface that will find internal types as well. + /// + internal class InternalTypeCatalog : ITypeCatalog + { + private readonly IAssemblyCatalog assemblyCatalog; + private readonly ConcurrentDictionary> cache; + + /// + /// Initializes a new instance of the class. + /// + /// An instanced, used to get the assemblies that types should be resolved from. + public InternalTypeCatalog(IAssemblyCatalog assemblyCatalog) + { + this.assemblyCatalog = assemblyCatalog; + cache = new ConcurrentDictionary>(); + } + + /// + /// Gets all types that are assignable to the provided . + /// + /// The that returned types should be assignable to. + /// A that should be used when retrieving types. + /// An of instances. + public IReadOnlyCollection GetTypesAssignableTo(Type type, TypeResolveStrategy strategy) + { + return cache.GetOrAdd(type, t => GetTypesAssignableTo(type)) + .Where(strategy.Invoke).ToArray(); + } + + private IReadOnlyCollection GetTypesAssignableTo(Type type) + { + return assemblyCatalog.GetAssemblies() + .SelectMany(assembly => assembly.SafeGetTypes()) + .Where(type.IsAssignableFrom) + .Where(t => !t.GetTypeInfo().IsAbstract).ToArray(); + } + } +} \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/LiquidProjections.Owin.csproj b/Src/LiquidProjections.Owin/LiquidProjections.Owin.csproj new file mode 100644 index 0000000..1e67875 --- /dev/null +++ b/Src/LiquidProjections.Owin/LiquidProjections.Owin.csproj @@ -0,0 +1,92 @@ + + + + + Debug + AnyCPU + {43C0A9ED-8094-4646-85A4-6EAA03505A20} + Library + Properties + LiquidProjections.Owin + LiquidProjections.Owin + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + bin\Debug\LiquidProjections.Owin.xml + 1701;1702;1705;1591 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\LiquidProjections.Owin.xml + 1701;1702;1705;1591 + true + + + + ..\..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Nancy.2.0.0-clinteastwood\lib\net452\Nancy.dll + + + ..\..\packages\Nancy.Linker.0.3.1\lib\net40-Client\Nancy.Linker.dll + + + ..\..\packages\Nancy.Metadata.Modules.2.0.0-barneyrubble\lib\net452\Nancy.Metadata.Modules.dll + + + ..\..\packages\Nancy.Owin.2.0.0-clinteastwood\lib\net452\Nancy.Owin.dll + + + ..\..\packages\Nancy.Swagger.2.2.12-alpha\lib\net452\Nancy.Swagger.dll + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + + + ..\..\packages\Swagger.ObjectModel.2.2.12-alpha\lib\net452\Swagger.ObjectModel.dll + + + + + + + + + + + + + + + + + + + + + + + + + + {7b47454d-0129-43cb-aed5-27afafdb5d7c} + LiquidProjections + + + + \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/LiquidProjections.Owin.v3.ncrunchproject b/Src/LiquidProjections.Owin/LiquidProjections.Owin.v3.ncrunchproject new file mode 100644 index 0000000..0e3a2cf --- /dev/null +++ b/Src/LiquidProjections.Owin/LiquidProjections.Owin.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/MiddlewareExtensions.cs b/Src/LiquidProjections.Owin/MiddlewareExtensions.cs new file mode 100644 index 0000000..a5aad4f --- /dev/null +++ b/Src/LiquidProjections.Owin/MiddlewareExtensions.cs @@ -0,0 +1,19 @@ +using LiquidProjections.Statistics; +using Nancy.Owin; +using Owin; + +namespace LiquidProjections.Owin +{ + public static class MiddlewareExtensions + { + public static IAppBuilder UseLiquidProjections(this IAppBuilder appBuilder, ProjectionStats stats) + { + appBuilder.Map("/projectionStats", a => a.UseNancy(new NancyOptions + { + Bootstrapper = new CustomNancyBootstrapper(stats) + })); + + return appBuilder; + } + } +} \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/Properties/AssemblyInfo.cs b/Src/LiquidProjections.Owin/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e6003d6 --- /dev/null +++ b/Src/LiquidProjections.Owin/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +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("LiquidProjections.Owin")] +[assembly: AssemblyDescription("OWIN middleware to access LiquidProjections over HTTP")] +[assembly: AssemblyProduct("LiquidProjections.Owin")] +[assembly: AssemblyCopyright("Copyright Dennis Doomen 2016-2017")] +[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("43c0a9ed-8094-4646-85a4-6eaa03505a20")] + +// 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("2.1.1.0")] +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] + +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/StatisticsModule.cs b/Src/LiquidProjections.Owin/StatisticsModule.cs new file mode 100644 index 0000000..d0e41e2 --- /dev/null +++ b/Src/LiquidProjections.Owin/StatisticsModule.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using LiquidProjections.Statistics; +using Nancy; +using Nancy.Linker; +using Nancy.Swagger; +using Nancy.Swagger.Modules; +using Nancy.Swagger.Services; +using Nancy.Swagger.Services.RouteUtils; +using Swagger.ObjectModel; + +// ReSharper disable VirtualMemberCallInConstructor + +namespace LiquidProjections.Owin +{ + internal class StatisticsModule : NancyModule + { + public StatisticsModule(ProjectionStats stats, IResourceLinker resourceLinker) + { + Get("/", args => + { + var results = stats.GetForAllProjectors().OrderBy(p => p.ProjectorId).Select(p => new ProjectorSummary + { + ProjectorId = p.ProjectorId, + LastCheckpoint = stats[p.ProjectorId].LastCheckpoint.Checkpoint, + LastCheckpointUpdatedUtc = stats[p.ProjectorId].LastCheckpoint.TimestampUtc, + Url = Context.Request.Url + $"/{p.ProjectorId}" + }); + + return results; + }, null, "GetAll"); + + Get("/{id}", args => + { + string id = args.Id; + + return new + { + ProjectorId = id, + LastCheckpoint = stats[id].LastCheckpoint.Checkpoint, + LastCheckpointUpdatedUtc = stats[id].LastCheckpoint.TimestampUtc, + Properties = stats[id].GetProperties().Select(p => new ProjectorProperty + { + Key = p.Key, + Value = p.Value.Value, + LastUpdatedUtc = p.Value.TimestampUtc + }), + EventsUrl = resourceLinker.BuildAbsoluteUri(Context, "GetEvents", new + { + args.id + }).ToString() + }; + }, null, "GetSpecific"); + + Get("/{id}/events", args => + { + string id = args.Id; + + return new ProjectorEventCollection + { + ProjectorId = id, + Events = stats[id].GetEvents().Select(@event => new ProjectorEvent + { + Body = @event.Body, + TimestampUtc = @event.TimestampUtc + }) + }; + }, null, "GetEvents"); + + Get("/{id}/eta/{targetCheckpoint}", args => + { + string id = args.Id; + + TimeSpan? eta = stats[id].GetTimeToReach(args.targetCheckpoint); + + return new + { + Eta = eta + }; + }, null, "GetEta"); + } + } + + internal class StatisticsMetadataModule : SwaggerMetadataModule + { + public StatisticsMetadataModule(ISwaggerModelCatalog modelCatalog, ISwaggerTagCatalog tagCatalog) + : base(modelCatalog, tagCatalog) + { + SwaggerTypeMapping.AddTypeMapping(typeof(DateTime), typeof(DateTime)); + + RouteDescriber.AddBaseTag(new Tag + { + Description = "Operations for getting projection statistics", + Name = "Statistics" + }); + + RouteDescriber.DescribeRoute>("GetAll", "", + "Returns a list of all known projectors and a summary of their status", new[] + { + new HttpResponseMetadata {Code = 200, Message = "OK"} + }); + + RouteDescriber + .DescribeRoute("GetSpecific", "", "Returns the details of a specific projector", new[] + { + new HttpResponseMetadata {Code = 200, Message = "OK"} + }) + .Parameter(p => p.Name("id").In(ParameterIn.Path).Description("Identifies the projector")); + + + RouteDescriber + .DescribeRoute("GetEvents", "", "Returns the events logged for a specific projector", new[] + { + new HttpResponseMetadata {Code = 200, Message = "OK"} + }) + .Parameter(p => p.Name("id").In(ParameterIn.Path).Description("Identifies the projector")); ; + + RouteDescriber + .DescribeRoute("GetEta", "", "Returns the ETA for a specific projector to reach a certain checkpoint", new[] + { + new HttpResponseMetadata {Code = 200, Message = "OK"} + }) + .Parameter(p => p.Name("id").In(ParameterIn.Path).Description("Identifies the projector")) + .Parameter(p => p.Name("targetCheckpoint").In(ParameterIn.Path).Description("The target checkpoint for which to calculate the ETA")); + + RouteDescriber.AddAdditionalModels( + typeof(ProjectorEvent), typeof(ProjectorProperty), typeof(ProjectorSummary)); + } + } + + internal class ProjectorEventCollection + { + public string ProjectorId { get; set; } + public IEnumerable Events { get; set; } + } + + internal class ProjectorProperty + { + public string Key { get; set; } + public string Value { get; set; } + public DateTime LastUpdatedUtc { get; set; } + } + + internal class ProjectorSummary + { + public string ProjectorId { get; set; } + public long LastCheckpoint { get; set; } + public DateTime LastCheckpointUpdatedUtc { get; set; } + public string Url { get; set; } + } + + internal class ProjectorEvent + { + public string Body { get; set; } + public DateTime TimestampUtc { get; set; } + } + + internal class ProjectorDetails + { + public string ProjectorId { get; set; } + public long LastCheckpoint { get; set; } + public DateTime LastCheckpointUpdatedUtc { get; set; } + public ProjectorProperty[] Properties { get; set; } + public string EventsUrl { get; set; } + } +} \ No newline at end of file diff --git a/Src/LiquidProjections.Owin/packages.config b/Src/LiquidProjections.Owin/packages.config new file mode 100644 index 0000000..037a9ee --- /dev/null +++ b/Src/LiquidProjections.Owin/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/Src/LiquidProjections.Testing/AssemblyInfo.cs b/Src/LiquidProjections.Testing/AssemblyInfo.cs index 9f29d43..10e4f57 100644 --- a/Src/LiquidProjections.Testing/AssemblyInfo.cs +++ b/Src/LiquidProjections.Testing/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.InteropServices; [assembly: AssemblyTitle("LiquidProjections.Testing")] @@ -7,6 +7,6 @@ [assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] -[assembly: AssemblyInformationalVersion("2.0.0+Branch.master.Sha.6e68f840c3303bac97b10c567815a3bfef3184d7")] \ No newline at end of file +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] \ No newline at end of file diff --git a/Src/LiquidProjections/AssemblyInfo.cs b/Src/LiquidProjections/AssemblyInfo.cs index 4eea4dc..8e09ed4 100644 --- a/Src/LiquidProjections/AssemblyInfo.cs +++ b/Src/LiquidProjections/AssemblyInfo.cs @@ -7,6 +7,6 @@ [assembly: ComVisible(false)] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] -[assembly: AssemblyInformationalVersion("2.0.0+Branch.master.Sha.6e68f840c3303bac97b10c567815a3bfef3184d7")] \ No newline at end of file +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] \ No newline at end of file diff --git a/Src/LiquidProjections/Statistics/ProjectionStats.cs b/Src/LiquidProjections/Statistics/ProjectionStats.cs index 19585ba..94a00e7 100644 --- a/Src/LiquidProjections/Statistics/ProjectionStats.cs +++ b/Src/LiquidProjections/Statistics/ProjectionStats.cs @@ -57,11 +57,14 @@ public void LogEvent(string projectorId, string body) return this[projectorId].GetTimeToReach(targetCheckpoint); } - private ProjectorStats this[string projectorId] + /// + /// Gets the statistics for a particular projector. + /// + public ProjectorStats this[string projectorId] { get { - return stats.GetOrAdd(projectorId, id => new ProjectorStats(id)); + return stats.GetOrAdd(projectorId, id => new ProjectorStats(id, nowUtc)); } } diff --git a/Src/LiquidProjections/Statistics/ProjectorStats.cs b/Src/LiquidProjections/Statistics/ProjectorStats.cs index 3a1307b..0f15d14 100644 --- a/Src/LiquidProjections/Statistics/ProjectorStats.cs +++ b/Src/LiquidProjections/Statistics/ProjectorStats.cs @@ -26,11 +26,12 @@ private readonly WeightedProjectionSpeedCalculator last10MinuteSamples private TimestampedCheckpoint lastCheckpoint; - public ProjectorStats(string projectorId) + public ProjectorStats(string projectorId, Func nowUtc) { properties = new ConcurrentDictionary(); events = new List(); ProjectorId = projectorId; + lastCheckpoint = new TimestampedCheckpoint(0, nowUtc()); } public ProjectorStats(string projectorId, IDictionary properties, IEnumerable events) diff --git a/Tests/LiquidProjections.Specs/LiquidProjections.Specs.csproj b/Tests/LiquidProjections.Specs/LiquidProjections.Specs.csproj index 59bb899..09e40ed 100644 --- a/Tests/LiquidProjections.Specs/LiquidProjections.Specs.csproj +++ b/Tests/LiquidProjections.Specs/LiquidProjections.Specs.csproj @@ -43,6 +43,21 @@ ..\..\packages\FluentAssertions.4.19.2\lib\net45\FluentAssertions.Core.dll + + ..\..\packages\FluentAssertions.Json.4.19.0\lib\net45\FluentAssertions.Json.dll + + + ..\..\packages\Microsoft.Owin.3.1.0\lib\net45\Microsoft.Owin.dll + + + ..\..\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll + + + ..\..\packages\Owin.1.0\lib\net40\Owin.dll + + + ..\..\packages\OwinHttpMessageHandler.1.3.8\lib\net45\OwinHttpMessageHandler.dll + @@ -71,6 +86,7 @@ + @@ -79,6 +95,10 @@ {8e38c862-7dc9-4a7e-a2ee-921ffbc7ee2d} LiquidProjections.Abstractions + + {43c0a9ed-8094-4646-85a4-6eaa03505a20} + LiquidProjections.Owin + {b4ea7831-8282-4f3a-a057-2deb59611a68} LiquidProjections.Testing @@ -92,6 +112,7 @@ + diff --git a/Tests/LiquidProjections.Specs/Properties/AssemblyInfo.cs b/Tests/LiquidProjections.Specs/Properties/AssemblyInfo.cs index a02fa55..6a88976 100644 --- a/Tests/LiquidProjections.Specs/Properties/AssemblyInfo.cs +++ b/Tests/LiquidProjections.Specs/Properties/AssemblyInfo.cs @@ -31,8 +31,8 @@ // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: -// [assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyVersion("2.0.0.0")] -[assembly: AssemblyFileVersion("2.0.0.0")] +// [assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyVersion("2.1.1.0")] +[assembly: AssemblyFileVersion("2.1.1.0")] -[assembly: AssemblyInformationalVersion("2.0.0+Branch.master.Sha.6e68f840c3303bac97b10c567815a3bfef3184d7")] +[assembly: AssemblyInformationalVersion("2.1.1-Statistics.1+2.Branch.Statistics.Sha.d204fd580c5bb1f85bdcdd4819af6123c7315da8")] diff --git a/Tests/LiquidProjections.Specs/StatisticsHttpApiSpecs.cs b/Tests/LiquidProjections.Specs/StatisticsHttpApiSpecs.cs new file mode 100644 index 0000000..3564927 --- /dev/null +++ b/Tests/LiquidProjections.Specs/StatisticsHttpApiSpecs.cs @@ -0,0 +1,245 @@ +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Chill; +using FluentAssertions; +using FluentAssertions.Json; +using LiquidProjections.Owin; +using LiquidProjections.Statistics; +using Microsoft.Owin.Builder; +using Newtonsoft.Json.Linq; +using Xunit; + +// ReSharper disable ConvertToLambdaExpression + +namespace LiquidProjections.Specs +{ + namespace StatisticsHttpApiSpecs + { + public class When_no_specific_projector_is_requested : GivenSubject + { + public When_no_specific_projector_is_requested() + { + Given(() => + { + var nowUtc = 10.July(2017).At(10, 39).AsUtc(); + var stats = new ProjectionStats(() => nowUtc); + stats.TrackProgress("id1", 1000); + stats.TrackProgress("id2", 1000); + + var appBuilder = new AppBuilder(); + appBuilder.UseLiquidProjections(stats); + + var httpClient = new HttpClient(new OwinHttpMessageHandler(appBuilder.Build())); + httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + WithSubject(ct => httpClient); + + }); + + When(async () => + { + return await Subject.GetAsync("http://localhost/projectionStats/"); + }); + } + + [Fact] + public async Task Then_it_should_provide_a_list_of_all_projectors() + { + var jtoken = JToken.Parse(await Result.Content.ReadAsStringAsync()); + + jtoken.Should().Be(JToken.Parse(@" + [ + { + ""projectorId"": ""id1"", + ""lastCheckpoint"": 1000, + ""lastCheckpointUpdatedUtc"": ""2017-07-10T10:39:00Z"", + ""url"": ""http://localhost/projectionStats/id1"" + }, + { + ""projectorId"": ""id2"", + ""lastCheckpoint"": 1000, + ""lastCheckpointUpdatedUtc"": ""2017-07-10T10:39:00Z"", + ""url"": ""http://localhost/projectionStats/id2"" + } + ]")); + } + } + public class When_a_specific_projector_is_requested : GivenSubject + { + public When_a_specific_projector_is_requested() + { + Given(() => + { + var nowUtc = 10.July(2017).At(10, 39).AsUtc(); + var stats = new ProjectionStats(() => nowUtc); + + stats.TrackProgress("id1", 1000); + stats.StoreProperty("id1", "property1", "value1"); + + var appBuilder = new AppBuilder(); + appBuilder.UseLiquidProjections(stats); + + var httpClient = new HttpClient(new OwinHttpMessageHandler(appBuilder.Build())); + httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + WithSubject(ct => httpClient); + }); + + When(async () => + { + + return await Subject.GetAsync("http://localhost/projectionStats/id1"); + }); + } + + [Fact] + public async Task Then_it_should_return_the_last_checkpoint_and_properties() + { + var jtoken = JToken.Parse(await Result.Content.ReadAsStringAsync()); + + jtoken.Should().Be(JToken.Parse(@" + { + ""projectorId"": ""id1"", + ""lastCheckpoint"": 1000, + ""lastCheckpointUpdatedUtc"": ""2017-07-10T10:39:00Z"", + ""properties"": [{ + ""key"": ""property1"", + ""value"": ""value1"", + ""lastUpdatedUtc"": ""2017-07-10T10:39:00Z"" + }], + ""eventsUrl"": ""http://localhost/projectionStats/id1/events"" + }")); + } + } + public class When_an_unknown_projector_is_requested : GivenSubject + { + public When_an_unknown_projector_is_requested() + { + Given(() => + { + var nowUtc = 10.July(2017).At(10, 39).AsUtc(); + var stats = new ProjectionStats(() => nowUtc); + + var appBuilder = new AppBuilder(); + appBuilder.UseLiquidProjections(stats); + + var httpClient = new HttpClient(new OwinHttpMessageHandler(appBuilder.Build())); + httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + WithSubject(ct => httpClient); + }); + + When(async () => + { + return await Subject.GetAsync("http://localhost/projectionStats/unknown"); + }); + } + + [Fact] + public async Task Then_it_should_return_some_default_information() + { + var jtoken = JToken.Parse(await Result.Content.ReadAsStringAsync()); + + jtoken.Should().Be(JToken.Parse(@" + { + ""projectorId"": ""unknown"", + ""lastCheckpoint"": 0, + ""lastCheckpointUpdatedUtc"": ""2017-07-10T10:39:00Z"", + ""properties"": [], + ""eventsUrl"": ""http://localhost/projectionStats/unknown/events"" + }")); + } + } + public class When_the_events_of_a_specific_projector_are_requested : GivenSubject + { + public When_the_events_of_a_specific_projector_are_requested() + { + Given(() => + { + var nowUtc = 10.July(2017).At(10, 39).AsUtc(); + var stats = new ProjectionStats(() => nowUtc); + stats.LogEvent("id1", "someevent"); + + var appBuilder = new AppBuilder(); + appBuilder.UseLiquidProjections(stats); + + var httpClient = new HttpClient(new OwinHttpMessageHandler(appBuilder.Build())); + httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + WithSubject(ct => httpClient); + }); + + When(async () => + { + + return await Subject.GetAsync("http://localhost/projectionStats/id1/events"); + }); + } + + [Fact] + public async Task Then_it_should_return_the_last_checkpoint_and_properties() + { + var jtoken = JToken.Parse(await Result.Content.ReadAsStringAsync()); + + jtoken.Should().Be(JToken.Parse(@" + { + ""projectorId"": ""id1"", + ""events"": [{ + ""body"": ""someevent"", + ""timestampUtc"": ""2017-07-10T10:39:00Z"" + }], + }")); + } + } + public class When_the_eta_to_a_checkpoint_is_requested : GivenSubject + { + public When_the_eta_to_a_checkpoint_is_requested() + { + Given(() => + { + var nowUtc = 10.July(2017).At(10, 39).AsUtc(); + UseThe(new ProjectionStats(() => nowUtc)); + The().TrackProgress("id1", 10); + + nowUtc = nowUtc.Add(1.Minutes()); + The().TrackProgress("id1", 1000); + + var appBuilder = new AppBuilder(); + appBuilder.UseLiquidProjections(The()); + + var httpClient = new HttpClient(new OwinHttpMessageHandler(appBuilder.Build())); + httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + WithSubject(ct => httpClient); + }); + + When(async () => + { + return await Subject.GetAsync("http://localhost/projectionStats/id1/eta/2000"); + }); + } + + [Fact] + public async Task Then_it_should_return_the_last_checkpoint_and_properties() + { + TimeSpan eta = The().GetTimeToReach("id1", 2000).Value; + + var jtoken = JToken.Parse(await Result.Content.ReadAsStringAsync()); + JToken element = jtoken.Should().HaveElement("eta").Which; + + element.Should().HaveElement("days").Which.Value().Should().Be(eta.Days); + element.Should().HaveElement("hours").Which.Value().Should().Be(eta.Hours); + element.Should().HaveElement("minutes").Which.Value().Should().Be(eta.Minutes); + element.Should().HaveElement("seconds").Which.Value().Should().Be(eta.Seconds); + element.Should().HaveElement("milliseconds").Which.Value().Should().Be(eta.Milliseconds); + } + } + } +} \ No newline at end of file diff --git a/Tests/LiquidProjections.Specs/app.config b/Tests/LiquidProjections.Specs/app.config new file mode 100644 index 0000000..09cf5f6 --- /dev/null +++ b/Tests/LiquidProjections.Specs/app.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tests/LiquidProjections.Specs/packages.config b/Tests/LiquidProjections.Specs/packages.config index 99ec5eb..60ae98d 100644 --- a/Tests/LiquidProjections.Specs/packages.config +++ b/Tests/LiquidProjections.Specs/packages.config @@ -2,6 +2,11 @@ + + + + + diff --git a/build.cake b/build.cake index 0a021f5..5f295f5 100644 --- a/build.cake +++ b/build.cake @@ -1,5 +1,7 @@ #tool "nuget:?package=xunit.runner.console" #tool "nuget:?package=GitVersion.CommandLine" +#tool "nuget:?package=ILRepack" +#addin "Cake.Incubator" ////////////////////////////////////////////////////////////////////// // ARGUMENTS @@ -33,10 +35,6 @@ Task("GitVersion").Does(() => { }); }); -Task("SyncNugetDependencies").Does(() => { - -}); - Task("Restore-NuGet-Packages") .IsDependentOn("Clean") .Does(() => @@ -50,7 +48,7 @@ Task("Restore-NuGet-Packages") }); Task("Build") - .IsDependentOn("Restore-NuGet-Packages") + .IsDependentOn("GitVersion") .Does(() => { if(IsRunningOnWindows()) @@ -71,6 +69,31 @@ Task("Build") } }); +Task("Merge") + .IsDependentOn("Build") + .Does(() => + { + ILRepack( + "./Artifacts/LiquidProjections.Owin.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/LiquidProjections.Owin.dll", + new FilePath[] { + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Microsoft.Owin.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Nancy.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Nancy.Metadata.Modules.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Nancy.Owin.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Nancy.Linker.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Nancy.Swagger.dll", + "./Src/LiquidProjections.Owin/bin/" + configuration + "/Swagger.ObjectModel.dll" + }, + new ILRepackSettings + { + Internalize = true, + XmlDocs = true + }); + + CopyFile("./Artifacts/LiquidProjections.Owin.dll", "./Tests/LiquidProjections.Specs/bin/" + configuration +"/LiquidProjections.Owin.dll"); + }); + Task("Run-Unit-Tests") .Does(() => { @@ -80,7 +103,7 @@ Task("Run-Unit-Tests") Task("Pack") .IsDependentOn("GitVersion") - .IsDependentOn("Build") + .IsDependentOn("Merge") .Does(() => { NuGetPack("./src/LiquidProjections.Abstractions/.nuspec", new NuGetPackSettings { @@ -98,7 +121,15 @@ Task("Pack") { "nugetversion", gitVersion.NuGetVersionV2 } } }); - + + NuGetPack("./src/LiquidProjections.Owin/.nuspec", new NuGetPackSettings { + OutputDirectory = "./Artifacts", + Version = gitVersion.NuGetVersionV2, + Properties = new Dictionary { + { "nugetversion", gitVersion.NuGetVersionV2 } + } + }); + NuGetPack("./src/LiquidProjections.Testing/.nuspec", new NuGetPackSettings { OutputDirectory = "./Artifacts", Version = gitVersion.NuGetVersionV2, @@ -113,8 +144,8 @@ Task("Pack") ////////////////////////////////////////////////////////////////////// Task("Default") - .IsDependentOn("GitVersion") - .IsDependentOn("Build") + .IsDependentOn("Restore-NuGet-Packages") + .IsDependentOn("Merge") .IsDependentOn("Run-Unit-Tests") .IsDependentOn("Pack");