├── .editorconfig
├── .gitignore
├── LICENSE
├── NuGet.Config
├── README.md
├── azure-pipelines.yml
├── icon
├── 128.png
├── 200.png
├── 32.png
├── 48.png
├── 64.png
├── Merq.ico
└── icon.png
├── lib
└── Microsoft.ComponentModel.Composition.Diagnostics.dll
├── msbuild.rsp
└── src
├── Core
├── Merq.Core.Tests
│ ├── CommandBusSpec.cs
│ ├── EventStreamSamples.cs
│ ├── EventStreamSpec.cs
│ └── Merq.Core.Tests.csproj
├── Merq.Core
│ ├── CommandBus.cs
│ ├── EventStream.cs
│ ├── Merq.Core.csproj
│ └── Properties
│ │ ├── Resources.Designer.cs
│ │ └── Resources.resx
└── Merq
│ ├── GitInfo.txt
│ ├── IAsyncCommand.cs
│ ├── IAsyncCommandHandler.cs
│ ├── ICanExecute.cs
│ ├── ICommand.cs
│ ├── ICommandBus.cs
│ ├── ICommandHandler.cs
│ ├── IEventStream.cs
│ ├── IEventStreamExtensions.cs
│ ├── IExecutable.cs
│ ├── IExecutableCommandHandler.cs
│ ├── Merq.csproj
│ └── Merq.targets
├── Directory.Build.props
├── Directory.Build.targets
├── GitInfo.txt
├── GlobalAssemblyInfo.cs
├── Installers
├── Merq.Installer.props
├── Merq.Installer.targets
├── Merq.Lib
│ ├── Directories.wxs
│ ├── Library.wxs
│ ├── Merq.Lib.wixproj
│ └── packages.config
├── Merq.Msi
│ ├── Merq.Msi.targets
│ ├── Merq.Msi.wixproj
│ ├── Merq.wxi
│ ├── Product.wxs
│ └── packages.config
├── SampleBundle
│ ├── Bundle.wxs
│ ├── Resources
│ │ ├── 1033
│ │ │ └── ClassicTheme.wxl
│ │ ├── 3082
│ │ │ └── ClassicTheme.wxl
│ │ ├── ClassicTheme.wxl
│ │ ├── ClassicTheme.xml
│ │ ├── banner.bmp
│ │ └── banner.pdn
│ ├── SampleBundle.wixproj
│ └── packages.config
├── SampleExtension
│ ├── Properties
│ │ └── AssemblyInfo.cs
│ ├── SampleExtension.csproj
│ ├── project.json
│ └── source.extension.vsixmanifest
└── build
│ ├── Merq.VisualStudio.WiX.targets
│ ├── Merq.VisualStudio.props
│ ├── Merq.VisualStudio.targets
│ └── Readme.txt
├── Merq.VisualStudio.proj
├── Merq.key
├── Merq.props
├── Merq.sln
├── Merq.snk
├── Merq.vssettings
├── StaFact.cs
├── Version.targets
├── Vsix
├── Merq.Vsix.IntegrationTests
│ ├── ComponentsSpec.cs
│ └── Merq.Vsix.IntegrationTests.csproj
├── Merq.Vsix.Tests
│ ├── CommandBusComponentSpec.cs
│ ├── EventStreamComponentSpec.cs
│ ├── GlobalSuppressions.cs
│ └── Merq.Vsix.Tests.csproj
└── Merq.Vsix
│ ├── Components
│ ├── CommandBusComponent.cs
│ ├── DefaultExportProvider.cs
│ └── EventStreamComponent.cs
│ ├── Merq.Vsix.csproj
│ ├── Merq.Vsix.props
│ ├── Merq.Vsix.targets
│ ├── MerqPackage.cs
│ ├── MerqPackage.resx
│ ├── Properties
│ ├── AssemblyInfo.cs
│ ├── Resources.Designer.cs
│ └── Resources.resx
│ └── source.extension.vsixmanifest
└── _._
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; EditorConfig to support per-solution formatting.
2 | ; Use the EditorConfig VS add-in to make this work.
3 | ; http://editorconfig.org/
4 |
5 | ; This is the default for the codeline.
6 | root = true
7 |
8 | [*]
9 | end_of_line = CRLF
10 |
11 | [*.{cs,txt,md}]
12 | indent_style = tab
13 | indent_size = 4
14 |
15 | [*.{sln,proj,props,targets,xml,config,nuspec}]
16 | indent_style = tab
17 | indent_size = 4
18 |
19 | [*.{csproj,resx}]
20 | indent_style = space
21 | indent_size = 2
22 |
23 | [*.yaml]
24 | indent_style = space
25 | indent_size = 2
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .nuget
2 | out
3 | bin
4 | obj
5 | packages
6 | *.nuget.targets
7 | *.nuget.props
8 | *.lock.json
9 | *.suo
10 | *.user
11 | *.cache
12 | .vs
13 | .*
14 | log.txt
15 | *.binlog
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Mobile Essentials
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |  Merq
2 | ================
3 |
4 | > **Mercury:** messenger of the Roman gods
5 |
6 | > *Mercury* > *Merq-ry* > **Merq**
7 |
8 | Internal application architecture based on commands and events represented as
9 | messages in a command bus and an event stream respectively, with support for
10 | asynchronously executing commands in a main thread deadlock-free way.
11 |
12 | [](http://build.devdiv.io/8887)
13 | [](https://coveralls.io/github/MobileEssentials/Merq?branch=master)
14 | [](https://www.nuget.org/packages/Merq)
15 | [](https://gitter.im/MobileEssentials?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
16 | [](https://github.com/MobileEssentials/Merq/blob/master/LICENSE)
17 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | name: merq
2 |
3 | trigger:
4 | batch: false
5 | branches:
6 | include:
7 | - master
8 | - dev/*
9 | - feature/*
10 | - rel/*
11 | paths:
12 | exclude:
13 | - docs
14 | - icon
15 |
16 | variables:
17 | - group: Xamarin Release
18 | - group: Xamarin-Secrets
19 | - name: Configuration
20 | value: Release
21 | - name: DotNetVersion
22 | value: 3.1.x
23 | - name: PackageOutputPath
24 | value: $(Build.ArtifactStagingDirectory)/package
25 | - name: System.Debug
26 | value: true
27 |
28 | resources:
29 | repositories:
30 | - repository: templates
31 | type: github
32 | name: xamarin/yaml-templates
33 | ref: refs/heads/master
34 | endpoint: xamarin
35 |
36 | stages:
37 | - stage: Windows
38 | jobs:
39 | - job: Build
40 | pool: VSEng-MicroBuildVS2019
41 | steps:
42 | - checkout: self
43 | clean: true
44 | submodules: recursive
45 | - task: UseDotNet@2
46 | inputs:
47 | version: $(DotNetVersion)
48 | performMultiLevelLookup: true
49 | - script: 'dotnet tool update -g dotnet-format && dotnet format -f $(Build.SourcesDirectory)\src --dry-run --check -v:diag'
50 | displayName: Check .editorconfig compliance
51 | - template: dump-environment.yml@templates
52 |
53 | - task: MSBuild@1
54 | displayName: Build
55 | inputs:
56 | solution: src\Merq.sln
57 | msbuildArguments: '-r -bl:$(Build.ArtifactStagingDirectory)/logs/build.binlog'
58 |
59 | - task: VSTest@2
60 | displayName: 'Test'
61 | timeoutInMinutes: 5
62 | inputs:
63 | testAssemblyVer2: |
64 | **\*Tests.dll
65 | !**\*IntegrationTests.dll
66 | !**\*TestAdapter.dll
67 | !**\obj\**
68 | codeCoverageEnabled: true
69 | runInParallel: false
70 | rerunFailedTests: true
71 | rerunMaxAttempts: 5
72 |
73 | - task: PublishBuildArtifacts@1
74 | displayName: 'Logs'
75 | condition: always()
76 | inputs:
77 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/logs'
78 | ArtifactName: logs
79 |
80 | - task: PublishBuildArtifacts@1
81 | displayName: 'Artifacts'
82 | inputs:
83 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/package'
84 | ArtifactName: package
85 |
86 | - task: PublishBuildArtifacts@1
87 | displayName: 'Symbols'
88 | inputs:
89 | PathtoPublish: '$(Build.ArtifactStagingDirectory)/artifacts'
90 | ArtifactName: symbols
91 |
92 | - stage: Upload
93 | jobs:
94 | - job: Upload
95 | pool: VSEng-MicroBuildVS2019
96 | steps:
97 | - checkout: self
98 |
99 | - task: UseDotNet@2
100 | inputs:
101 | packageType: runtime
102 | version: $(DotNetVersion)
103 | performMultiLevelLookup: true
104 | - script: 'dotnet tool update -g --version 7.0.0 PowerShell >nul || dotnet tool list -g'
105 | displayName: UsePowerShell
106 |
107 | - template: fix-source-version/v2.yml@templates
108 | # This is only needed while we teach the build-tools tasks how to receive overriden variables.
109 | - script: git reset --hard $(GitHub.Commit)
110 | displayName: Align checkout with GitHub.Commit
111 | condition: ne(variables['GitHub.Commit'], variables['Build.SourceVersion'])
112 |
113 | - task: DownloadBuildArtifacts@0
114 | inputs:
115 | artifactName: package
116 |
117 | - template: dump-environment.yml@templates
118 | - template: upload-to-storage/win/v1.yml@templates
119 | parameters:
120 | ArtifactsDirectory: '$(Build.ArtifactStagingDirectory)/package'
121 | Azure.ContainerName: 'xvs-merq'
122 | GitHub.Context: 'artifacts'
123 |
124 | - task: NuGetCommand@2
125 | displayName: Push Packages
126 | continueOnError: true
127 | condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['PushPackages'], 'true')))
128 | inputs:
129 | command: push
130 | packagesToPush: $(Build.ArtifactStagingDirectory)/package/*.nupkg
131 | nuGetFeedType: external
132 | publishFeedCredentials: 'xamarin-impl public feed'
133 |
--------------------------------------------------------------------------------
/icon/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/128.png
--------------------------------------------------------------------------------
/icon/200.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/200.png
--------------------------------------------------------------------------------
/icon/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/32.png
--------------------------------------------------------------------------------
/icon/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/48.png
--------------------------------------------------------------------------------
/icon/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/64.png
--------------------------------------------------------------------------------
/icon/Merq.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/Merq.ico
--------------------------------------------------------------------------------
/icon/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/icon/icon.png
--------------------------------------------------------------------------------
/lib/Microsoft.ComponentModel.Composition.Diagnostics.dll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MobileEssentials/Merq/34d6af216bf5c72f0bc3fa796607c83ae9a42fae/lib/Microsoft.ComponentModel.Composition.Diagnostics.dll
--------------------------------------------------------------------------------
/msbuild.rsp:
--------------------------------------------------------------------------------
1 | /consoleloggerparameters:Verbosity=minimal
2 | /bl
3 | /v:m
4 | /nr:false
5 | /m
--------------------------------------------------------------------------------
/src/Core/Merq.Core.Tests/CommandBusSpec.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using System.Linq;
6 | using Moq;
7 | using Xunit;
8 |
9 | namespace Merq
10 | {
11 | public class CommandBusSpec
12 | {
13 | [Fact]
14 | public void when_registering_non_generic_handler_then_throws()
15 | {
16 | Assert.Throws(() => new CommandBus(Mock.Of()));
17 | }
18 |
19 | [Fact]
20 | public void when_registering_duplicate_handlers_then_throws()
21 | {
22 | Assert.Throws(() => new CommandBus(
23 | Mock.Of>(),
24 | Mock.Of>()));
25 | }
26 |
27 | [Fact]
28 | public void when_executing_command_without_handler_then_throws()
29 | {
30 | var bus = new CommandBus();
31 |
32 | Assert.Throws(() => bus.Execute(new Command()));
33 | }
34 |
35 | [Fact]
36 | public void when_executing_command_with_result_without_handler_then_throws()
37 | {
38 | var bus = new CommandBus();
39 |
40 | Assert.Throws(() => bus.Execute(new CommandWithResult()));
41 | }
42 |
43 | [Fact]
44 | public async void when_executing_async_command_without_handler_then_throws()
45 | {
46 | var bus = new CommandBus();
47 |
48 | await Assert.ThrowsAsync(() => bus.ExecuteAsync(new AsyncCommand(), CancellationToken.None));
49 | }
50 |
51 | [Fact]
52 | public async void when_executing_async_command_with_result_without_handler_then_throws()
53 | {
54 | var bus = new CommandBus();
55 |
56 | await Assert.ThrowsAsync(() => bus.ExecuteAsync(new AsyncCommandWithResult(), CancellationToken.None));
57 | }
58 |
59 | [Fact]
60 | public void when_can_handle_requested_for_non_registered_handler_then_returns_false()
61 | {
62 | var bus = new CommandBus();
63 |
64 | Assert.False(bus.CanHandle());
65 | }
66 |
67 | [Fact]
68 | public void when_can_handle_requested_for_registered_handler_type_then_returns_true()
69 | {
70 | var bus = new CommandBus(Mock.Of>());
71 |
72 | Assert.True(bus.CanHandle());
73 | }
74 |
75 | [Fact]
76 | public void when_can_handle_requested_for_registered_handler_instance_then_returns_true()
77 | {
78 | var bus = new CommandBus(Mock.Of>());
79 |
80 | Assert.True(bus.CanHandle(new Command()));
81 | }
82 |
83 | [Fact]
84 | public void when_can_handle_requested_for_null_command_then_returns_false()
85 | {
86 | var bus = new CommandBus(Mock.Of>());
87 |
88 | Assert.False(bus.CanHandle((Command)null));
89 | }
90 |
91 | [Fact]
92 | public void when_can_execute_requested_and_no_handler_registered_then_returns_false()
93 | {
94 | var bus = new CommandBus();
95 |
96 | Assert.False(bus.CanExecute(new Command()));
97 | }
98 |
99 | [Fact]
100 | public void when_can_execute_requested_then_invokes_sync_handler()
101 | {
102 | var command = new Command();
103 | var bus = new CommandBus(Mock.Of>(c => c.CanExecute(command) == true));
104 |
105 | Assert.True(bus.CanExecute(command));
106 | }
107 |
108 | [Fact]
109 | public void when_can_execute_requested_then_invokes_async_handler()
110 | {
111 | var command = new AsyncCommand();
112 | var bus = new CommandBus(Mock.Of>(c => c.CanExecute(command) == true));
113 |
114 | Assert.True(bus.CanExecute(command));
115 | }
116 |
117 | [Fact]
118 | public void when_executing_sync_command_then_invokes_sync_handler()
119 | {
120 | var handler = new Mock>();
121 | var command = new Command();
122 | var bus = new CommandBus(handler.Object);
123 |
124 | bus.Execute(command);
125 |
126 | handler.Verify(x => x.Execute(command));
127 | }
128 |
129 | [Fact]
130 | public void when_executing_sync_command_then_invokes_sync_handler_with_result()
131 | {
132 | var handler = new Mock>();
133 | var command = new CommandWithResult();
134 | var bus = new CommandBus(handler.Object);
135 |
136 | bus.Execute(command);
137 |
138 | handler.Verify(x => x.Execute(command));
139 | }
140 |
141 | [Fact]
142 | public void when_executing_sync_command_with_result_then_invokes_sync_handler_with_result()
143 | {
144 | var handler = new Mock>();
145 | var command = new CommandWithResult();
146 | var expected = new Result();
147 |
148 | handler.Setup(x => x.Execute(command)).Returns(expected);
149 | var bus = new CommandBus(handler.Object);
150 |
151 | var result = bus.Execute(command);
152 |
153 | Assert.Same(expected, result);
154 | }
155 |
156 | [Fact]
157 | public async void when_executing_async_command_then_invokes_async_handler()
158 | {
159 | var handler = new Mock>();
160 | var command = new AsyncCommand();
161 |
162 | handler.Setup(x => x.ExecuteAsync(command, CancellationToken.None)).Returns(Task.FromResult(true));
163 | var bus = new CommandBus(handler.Object);
164 |
165 | await bus.ExecuteAsync(command, CancellationToken.None);
166 |
167 | handler.Verify(x => x.ExecuteAsync(command, CancellationToken.None));
168 | }
169 |
170 | [Fact]
171 | public async void when_executing_async_command_then_invokes_async_handler_with_result()
172 | {
173 | var handler = new Mock>();
174 | var command = new AsyncCommandWithResult();
175 | var result = new Result();
176 |
177 | handler.Setup(x => x.ExecuteAsync(command, CancellationToken.None)).Returns(Task.FromResult(result));
178 | var bus = new CommandBus(handler.Object);
179 |
180 | await bus.ExecuteAsync(command, CancellationToken.None);
181 |
182 | handler.Verify(x => x.ExecuteAsync(command, CancellationToken.None));
183 | }
184 |
185 | [Fact]
186 | public void when_constructing_with_null_handlers_then_throws()
187 | {
188 | Assert.Throws(() => new CommandBus(default(IEnumerable)));
189 | }
190 |
191 | [Fact]
192 | public void when_can_handle_with_null_command_then_returns_false()
193 | {
194 | Assert.False(new CommandBus().CanHandle(null));
195 | }
196 |
197 | [Fact]
198 | public void when_can_execute_with_null_command_then_returns_false()
199 | {
200 | Assert.False(new CommandBus().CanExecute(null));
201 | }
202 |
203 | [Fact]
204 | public void when_execute_with_null_command_then_throws()
205 | {
206 | Assert.Throws(() => new CommandBus().Execute(default(Command)));
207 | }
208 |
209 | [Fact]
210 | public void when_execute_result_with_null_command_then_throws()
211 | {
212 | Assert.Throws(() => new CommandBus().Execute(default(CommandWithResult)));
213 | }
214 |
215 | [Fact]
216 | public async Task when_executeasync_with_null_command_then_throws()
217 | {
218 | await Assert.ThrowsAsync(() => new CommandBus().ExecuteAsync(default(AsyncCommand), CancellationToken.None));
219 | }
220 |
221 | [Fact]
222 | public async Task when_executeasync_result_with_null_command_then_throws()
223 | {
224 | await Assert.ThrowsAsync(() => new CommandBus().ExecuteAsync(default(AsyncCommandWithResult), CancellationToken.None));
225 | }
226 |
227 | [Fact]
228 | public void when_executing_non_public_command_handler_then_invokes_handler_with_result()
229 | {
230 | var handler = new NonPublicCommandHandlerWithResults(new Result());
231 | var bus = new CommandBus(handler);
232 |
233 | var results = bus.Execute(new CommandWithResults());
234 |
235 | Assert.Single(results);
236 | }
237 |
238 | [Fact]
239 | public void when_executing_command_as_explicit_ICommand_then_invokes_handler()
240 | {
241 | var handler = new Mock>();
242 | var command = new Command();
243 | var bus = new CommandBus(handler.Object);
244 |
245 | bus.Execute((ICommand)command);
246 |
247 | handler.Verify(x => x.Execute(command));
248 | }
249 |
250 | [Fact]
251 | public void when_command_execution_throws_then_throws_original_exception()
252 | {
253 | var handler = new Mock>();
254 | var command = new Command();
255 | var exception = new InvalidOperationException();
256 | handler.Setup(x => x.Execute(command)).Throws(exception);
257 | var bus = new CommandBus(handler.Object);
258 |
259 | var actual = Assert.Throws(() => bus.Execute((ICommand)command));
260 |
261 | Assert.Same(exception, actual);
262 | }
263 |
264 | public class AsyncCommand : IAsyncCommand { }
265 |
266 | public class AsyncCommandWithResult : IAsyncCommand { }
267 |
268 | public class Command : ICommand { }
269 |
270 | public class CommandWithResult : ICommand { }
271 |
272 | public class CommandWithResults : ICommand> { }
273 |
274 | public class Result { }
275 |
276 | class NonPublicCommandHandlerWithResults : ICommandHandler>
277 | {
278 | Result result;
279 |
280 | public NonPublicCommandHandlerWithResults(Result result)
281 | {
282 | this.result = result;
283 | }
284 |
285 | bool ICanExecute.CanExecute(CommandWithResults command)
286 | {
287 | return true;
288 | }
289 |
290 | IEnumerable ICommandHandler>.Execute(CommandWithResults command)
291 | {
292 | yield return result;
293 | }
294 | }
295 |
296 | // Ensure all test to be run using a derived command bus class
297 | class CommandBus : Merq.CommandBus
298 | {
299 | public CommandBus(IEnumerable handlers) : base(handlers) { }
300 |
301 | public CommandBus(params ICommandHandler[] handlers) : base(handlers) { }
302 | }
303 | }
304 | }
305 |
--------------------------------------------------------------------------------
/src/Core/Merq.Core.Tests/EventStreamSamples.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Reactive;
5 | using System.Reactive.Linq;
6 | using Microsoft.Reactive.Testing;
7 | using Xunit;
8 |
9 | namespace Merq
10 | {
11 | public class EventStreamSamples
12 | {
13 | [Fact]
14 | public void when_patient_readmitted_then_raises_alert()
15 | {
16 | var events = new EventStream();
17 | var query =
18 | from discharged in events.Of()
19 | from admitted in events.Of()
20 | where
21 | admitted.PatientId == discharged.PatientId &&
22 | (admitted.When - discharged.When).Days < 5
23 | select admitted;
24 |
25 |
26 | var readmitted = new List();
27 |
28 | using (var subscription = query.Subscribe(e => readmitted.Add(e.PatientId)))
29 | {
30 | // Two patients come in.
31 | events.Push(new PatientEnteredHospital { PatientId = 1, When = new DateTime(2011, 1, 1) });
32 | events.Push(new PatientEnteredHospital { PatientId = 2, When = new DateTime(2011, 1, 1) });
33 |
34 | // Both leave same day.
35 | events.Push(new PatientLeftHospital { PatientId = 1, When = new DateTime(2011, 1, 15) });
36 | events.Push(new PatientLeftHospital { PatientId = 2, When = new DateTime(2011, 1, 15) });
37 |
38 | // One comes back before 5 days passed.
39 | events.Push(new PatientEnteredHospital { PatientId = 1, When = new DateTime(2011, 1, 18) });
40 |
41 | // The other comes back after 10 days passed.
42 | events.Push(new PatientEnteredHospital { PatientId = 1, When = new DateTime(2011, 1, 25) });
43 | }
44 |
45 | // We should have an alert for patient 1 who came back before 5 days passed.
46 | Assert.Single(readmitted);
47 | Assert.Equal(1, readmitted[0]);
48 | }
49 |
50 | [Fact]
51 | public void when_user_login_fails_too_fast_then_locks_account()
52 | {
53 | var seconds = TimeSpan.FromSeconds(1).Ticks;
54 | var events = new EventStream();
55 |
56 | // Here we use the test scheduler to simulate time passing by
57 | // because we have a dependency on time because of the Buffer
58 | // method.
59 | var scheduler = new TestScheduler();
60 | var observable = scheduler.CreateColdObservable(
61 | // Two users attempt to log in, 4 times in a row
62 | new Recorded>(10 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
63 | new Recorded>(10 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
64 | new Recorded>(20 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
65 | new Recorded>(20 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
66 | new Recorded>(30 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
67 | new Recorded>(30 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
68 | new Recorded>(40 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 })),
69 | new Recorded>(40 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
70 |
71 | // User 2 attempts one more time within the 1' window
72 | new Recorded>(45 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 2 })),
73 |
74 | // User 1 pulls out the paper where he wrote his pwd ;), so he takes longer
75 | new Recorded>(75 * seconds, Notification.CreateOnNext(new LoginFailure { UserId = 1 }))
76 | );
77 |
78 | // This subscription bridges the scheduler-driven
79 | // observable with our event stream, causing us
80 | // to publish events as they are "raised" by the
81 | // test scheduler.
82 | observable.Subscribe(failure => events.Push(failure));
83 |
84 | var query = events.Of()
85 | // Sliding windows 1' long, every 10''
86 | .Buffer(TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(10), scheduler)
87 | // From all failure values
88 | .SelectMany(failures => failures
89 | // Group the failures by user
90 | .GroupBy(failure => failure.UserId)
91 | // Only grab those failures with more than 5 in the 1' window
92 | .Where(group => group.Count() >= 5)
93 | // Return the user id that failed to log in
94 | .Select(group => group.Key));
95 |
96 | var blocked = new List();
97 |
98 | using (var subscription = query.Subscribe(userId => blocked.Add(userId)))
99 | {
100 | // Here we could advance the scheduler half way and test intermediate
101 | // state if needed. We go all the way past the end of our login failures.
102 | scheduler.AdvanceTo(100 * seconds);
103 | }
104 |
105 | // We should have only user # 2 in the list.
106 | Assert.DoesNotContain(1, blocked);
107 | Assert.Contains(2, blocked);
108 | }
109 |
110 | public interface IBaseEvent { }
111 |
112 | public class BaseEvent : EventArgs, IBaseEvent
113 | {
114 | public override string ToString()
115 | {
116 | return "Base event";
117 | }
118 | }
119 |
120 | public class PatientEnteredHospital : BaseEvent
121 | {
122 | public int PatientId { get; set; }
123 | public DateTimeOffset When { get; set; }
124 |
125 | public override string ToString()
126 | {
127 | return string.Format("Patient {0} entered on {1}.", PatientId, When);
128 | }
129 | }
130 |
131 | public class PatientLeftHospital : BaseEvent
132 | {
133 | public int PatientId { get; set; }
134 | public DateTimeOffset When { get; set; }
135 |
136 | public override string ToString()
137 | {
138 | return string.Format("Patient {0} left on {1}.", PatientId, When);
139 | }
140 | }
141 |
142 | public class LoginFailure : BaseEvent
143 | {
144 | public int UserId { get; set; }
145 | public DateTimeOffset When { get; set; }
146 | }
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/src/Core/Merq.Core.Tests/EventStreamSpec.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reactive.Linq;
3 | using System.Reactive.Subjects;
4 | using Xunit;
5 |
6 | namespace Merq
7 | {
8 | public class EventStreamSpec
9 | {
10 | [Fact]
11 | public void when_pushing_null_event_then_throws()
12 | {
13 | var stream = new EventStream();
14 |
15 | Assert.Throws(() => stream.Push