├── .gitignore ├── .nuke ├── Directory.Build.props ├── Eventfully.sln ├── LICENSE ├── README.md ├── analyzers └── Eventfully.Core.Analyzers │ ├── Eventfully.Core.Analyzers.Vsix │ ├── Eventfully.Core.Analyzers.Vsix.csproj │ └── source.extension.vsixmanifest │ └── Eventfully.Core.Analyzers │ ├── Eventfully.Core.Analyzers.csproj │ ├── MissingHandlerForMapIdForCodeFixProvider.cs │ ├── MissingMapIdForAnalyzer.cs │ ├── MissingMapIdForCodeFixProvider.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ └── tools │ ├── install.ps1 │ └── uninstall.ps1 ├── appveyor.yml ├── build.ps1 ├── build.sh ├── build ├── .editorconfig ├── Build.cs ├── _build.csproj └── _build.csproj.DotSettings ├── samples └── EFCore │ ├── Eventfully.Samples.AzureServiceBus.ConsoleApp │ ├── ApplicationDbContext.cs │ ├── Entities │ │ └── Order.cs │ ├── Eventfully.Samples.AzureServiceBus.ConsoleApp.csproj │ ├── MessagingProfile.cs │ ├── Migrations │ │ ├── 20191203191923_Initial.Designer.cs │ │ ├── 20191203191923_Initial.cs │ │ ├── 20200123164022_Orders.Designer.cs │ │ ├── 20200123164022_Orders.cs │ │ └── ApplicationDbContextModelSnapshot.cs │ ├── OrderCreated │ │ ├── OrderCreated.cs │ │ └── OrderCreatedHandler.cs │ ├── PaymentMethodCreated │ │ ├── PaymentMethodCreated.cs │ │ └── PaymentMethodCreatedHandler.cs │ ├── PizzaProcess.cs │ ├── Program.cs │ ├── Undependable │ │ └── FailingHandler.cs │ └── appsettings.json │ └── Eventfully.Samples.ConsoleApp │ ├── ApplicationDbContext.cs │ ├── Entities │ └── Order.cs │ ├── Eventfully.Samples.ConsoleApp.csproj │ ├── MessagingProfile.cs │ ├── Migrations │ ├── 20191203191923_Initial.Designer.cs │ ├── 20191203191923_Initial.cs │ ├── 20200123164022_Orders.Designer.cs │ ├── 20200123164022_Orders.cs │ └── ApplicationDbContextModelSnapshot.cs │ ├── OrderCreated │ ├── OrderCreated.cs │ └── OrderCreatedHandler.cs │ ├── PaymentMethodCreated │ ├── PaymentMethodCreated.cs │ └── PaymentMethodCreatedHandler.cs │ ├── Program.cs │ └── appsettings.json ├── src ├── Eventfully.AzureKeyVault │ ├── AzureKeyVaultKeyProvider.cs │ ├── Eventfully.AzureKeyVault.csproj │ └── README.txt ├── Eventfully.Core │ ├── Config │ │ ├── EndpointBindings.cs │ │ ├── IEndpointFluent.cs │ │ ├── MessagingConfiguration.cs │ │ └── MessagingProfile.cs │ ├── DependencyInjection │ │ ├── IRegistrar.cs │ │ └── IServiceFactory.cs │ ├── Endpoints │ │ ├── Endpoint.cs │ │ ├── EndpointSettings.cs │ │ └── IEndpoint.cs │ ├── Eventfully.Core.csproj │ ├── Filters │ │ ├── AesTransportEncpryptionFilter.cs │ │ ├── ExpirationCheckStep.cs │ │ ├── IEncryptionKeyProvider.cs │ │ ├── IIntegrationMessageFilter.cs │ │ ├── ITransportMessageFilter.cs │ │ ├── InboundMessagePipeline.cs │ │ ├── MessageExtractionStep.cs │ │ ├── MessageSerializationStep.cs │ │ ├── MessageTypeIntegrationFilter.cs │ │ ├── MessageTypeTransportFilter.cs │ │ ├── OutboundMessagePipeline.cs │ │ └── Pipeline.cs │ ├── Handling │ │ ├── ICustomMessageHandler.cs │ │ ├── IMessageDispatcher.cs │ │ ├── IMessageHandler.cs │ │ ├── MessageContext.cs │ │ └── MessageDispatcher.cs │ ├── Messages │ │ ├── IIntegrationMessage.cs │ │ ├── IMessageExtractor.cs │ │ ├── IntegrationCommand.cs │ │ ├── IntegrationEvent.cs │ │ ├── IntegrationMessage.cs │ │ ├── IntegrationReply.cs │ │ ├── MessageMetaData.cs │ │ └── WrappedMessage.cs │ ├── MessagingClient.cs │ ├── MessagingMap.cs │ ├── MessagingService.cs │ ├── Outboxing │ │ ├── IOutbox.cs │ │ ├── IOutboxManager.cs │ │ ├── IOutboxSession.cs │ │ ├── OutboxDispatchOptions.cs │ │ ├── OutboxDispatchResult.cs │ │ └── OutboxManager.cs │ ├── README.md │ ├── Saga │ │ ├── ISaga.cs │ │ ├── ISagaPersistence.cs │ │ ├── ITriggeredBy.cs │ │ └── ProcessManagerMachine.cs │ ├── Transports │ │ ├── ITransport.cs │ │ ├── ITransportFactory.cs │ │ ├── LocalTransport.cs │ │ ├── TestTransport.cs │ │ ├── Transport.cs │ │ ├── TransportConfigurationExtenstions.cs │ │ ├── TransportMessage.cs │ │ └── TransportSettings.cs │ └── Util │ │ ├── ConstantRetryStrategy.cs │ │ ├── DefaultExponentialRetryStrategy.cs │ │ ├── Enumeration.cs │ │ ├── Exceptions │ │ ├── EndpointNotFoundException.cs │ │ ├── InvalidMessageTypeException.cs │ │ ├── MessageExpiredException.cs │ │ ├── NonTransientException.cs │ │ └── UnknownMessageTypeException.cs │ │ ├── ICountingSemaphore.cs │ │ ├── IRetryIntervalStrategy.cs │ │ └── MessagingLogging.cs ├── Eventfully.EFCoreOutbox │ ├── Eventfully.EFCoreOutbox.csproj │ ├── Outbox.cs │ ├── OutboxMessage.cs │ ├── OutboxSession.cs │ ├── README.txt │ ├── RegistrationExtensions.cs │ └── SqlConnectionFactory.cs ├── Eventfully.MicrosoftDependencyInjection │ ├── Eventfully.Extensions.Microsoft.DependencyInjection.csproj │ ├── Extensions.cs │ ├── ServiceFactory.cs │ └── ServiceRegistrar.cs ├── Eventfully.Semaphore.SqlServer │ ├── CreateSqlSemaphoreTable.sql │ ├── Eventfully.Semaphore.SqlServer.csproj │ ├── README.txt │ ├── SqlConnectionFactory.cs │ └── SqlServerSemaphore.cs └── Eventfully.Transports.AzureServiceBus │ ├── AzureServiceBusClientCache.cs │ ├── AzureServiceBusConfigurationExtensions.cs │ ├── AzureServiceBusMessagePump.cs │ ├── AzureServiceBusMetaDataMapper.cs │ ├── AzureServiceBusTransport.cs │ ├── AzureServiceBusTransportFactory.cs │ ├── Eventfully.Transports.AzureServiceBus.csproj │ └── Recoverability │ ├── AzureServiceBusCloneRecoverabilityProvider.cs │ ├── AzureServiceBusDeferRecoverabilityProvider.cs │ ├── IAzureServiceBusRecoverabilityProvider.cs │ └── RecoverabilityControlMessage.cs └── tests ├── Eventfully.Core.Analyzers.Test ├── Eventfully.Core.Analyzers.Test.csproj ├── Helpers │ ├── CodeFixVerifier.Helper.cs │ ├── DiagnosticResult.cs │ └── DiagnosticVerifier.Helper.cs ├── MissingMapIdForAnalyzerTests.cs ├── PizzaProcess.cs └── Verifiers │ ├── CodeFixVerifier.cs │ └── DiagnosticVerifier.cs ├── Eventfully.Core.UnitTests ├── Eventfully.Core.UnitTests.csproj ├── MessageDispatcherTests.cs ├── MessagingServiceTests.cs ├── MetaDataTests.cs ├── ProcessManagerStateMachineCustomNaming.cs ├── ProcessManagerStateMachineTests.cs └── Util │ └── UnitTestFixture.cs ├── Eventfully.EFCoreOutbox.IntegrationTests ├── Eventfully.EFCoreOutbox.IntegrationTests.csproj ├── Migrations │ ├── 20200106191125_Initial.Designer.cs │ ├── 20200106191125_Initial.cs │ └── ApplicationDbContextModelSnapshot.cs ├── OutboxConcurrencyTests.cs ├── OutboxDequeueTests.cs ├── OutboxEnqueueTests.cs ├── OutboxManagementTests.cs ├── OutboxRetryTests.cs ├── TransientDispatchTests.cs ├── Util │ ├── ApplicationDbContext.cs │ ├── IntegrationTestBase.cs │ ├── IntegrationTestFixture.cs │ └── TestMessage.cs └── appsettings.json ├── Eventfully.EFCoreOutbox.UnitTests ├── Eventfully.EFCoreOutbox.UnitTests.csproj ├── OutboxMessageTests.cs └── TestMessage.cs ├── Eventfully.Semaphore.SqlServer.IntegrationTests ├── Eventfully.Semaphore.SqlServer.IntegrationTests.csproj ├── SemaphoreTests.cs ├── Util │ ├── IntegrationTestBase.cs │ └── IntegrationTestFixture.cs └── appsettings.json ├── Eventfully.Transports.AzureServiceBus.IntegrationTests ├── DeferRecoverabilityProviderTests.cs ├── DispatchTests.cs ├── Eventfully.Transports.AzureServiceBus.IntegrationTests.csproj ├── HandleTests.cs ├── Util │ ├── FakeInterfaces.cs │ ├── IntegrationTestBase.cs │ ├── IntegrationTestFixture.cs │ └── TestMessage.cs └── appsettings.json └── Eventfully.Transports.AzureServiceBus.UnitTests.UnitTests ├── Eventfully.Transports.AzureServiceBus.UnitTests.csproj ├── MetaDataMapperApplyTests.cs └── MetaDataMapperExtractTests.cs /.nuke: -------------------------------------------------------------------------------- 1 | Eventfully.sln -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrenzel/Eventfully/bf32d3665898fb11b9bc5c25a63ffe6abe4b467b/Directory.Build.props -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Camron Frenzel 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 | -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers.Vsix/Eventfully.Core.Analyzers.Vsix.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 14.0 6 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 7 | 8 | 9 | 10 | Debug 11 | AnyCPU 12 | AnyCPU 13 | 2.0 14 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 15 | {8DE2E3AD-9F5D-454E-86A9-2D68519737A0} 16 | Library 17 | Properties 18 | Eventfully.Core.Analyzers.Vsix 19 | Eventfully.Core.Analyzers 20 | v4.6.1 21 | false 22 | false 23 | false 24 | false 25 | false 26 | false 27 | Roslyn 28 | 29 | 30 | true 31 | full 32 | false 33 | bin\Debug\ 34 | DEBUG;TRACE 35 | prompt 36 | 4 37 | 38 | 39 | pdbonly 40 | true 41 | bin\Release\ 42 | TRACE 43 | prompt 44 | 4 45 | 46 | 47 | Program 48 | $(DevEnvDir)devenv.exe 49 | /rootsuffix Roslyn 50 | 51 | 52 | 53 | Designer 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers.Vsix/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Eventfully.Core.Analyzers 6 | This is a sample diagnostic extension for the .NET Compiler Platform ("Roslyn"). 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.3 5 | false 6 | True 7 | 8 | 9 | 10 | Eventfully.Core.Analyzers 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Eventfully.Core.Analyzers { 12 | using System; 13 | using System.Reflection; 14 | 15 | 16 | /// 17 | /// A strongly-typed resource class, for looking up localized strings, etc. 18 | /// 19 | // This class was auto-generated by the StronglyTypedResourceBuilder 20 | // class via a tool like ResGen or Visual Studio. 21 | // To add or remove a member, edit your .ResX file then rerun ResGen 22 | // with the /str option, or rebuild your VS project. 23 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] 24 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 25 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 26 | internal class Resources { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() { 34 | } 35 | 36 | /// 37 | /// Returns the cached ResourceManager instance used by this class. 38 | /// 39 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 40 | internal static global::System.Resources.ResourceManager ResourceManager { 41 | get { 42 | if (object.ReferenceEquals(resourceMan, null)) { 43 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Eventfully.Core.Analyzers.Resources", typeof(Resources).GetTypeInfo().Assembly); 44 | resourceMan = temp; 45 | } 46 | return resourceMan; 47 | } 48 | } 49 | 50 | /// 51 | /// Overrides the current thread's CurrentUICulture property for all 52 | /// resource lookups using this strongly typed resource class. 53 | /// 54 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 55 | internal static global::System.Globalization.CultureInfo Culture { 56 | get { 57 | return resourceCulture; 58 | } 59 | set { 60 | resourceCulture = value; 61 | } 62 | } 63 | 64 | /// 65 | /// Looks up a localized string similar to Should have MapIdFor for each event handled. 66 | /// 67 | internal static string AnalyzerDescription { 68 | get { 69 | return ResourceManager.GetString("AnalyzerDescription", resourceCulture); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized string similar to {0}. 75 | /// 76 | internal static string AnalyzerMessageFormat { 77 | get { 78 | return ResourceManager.GetString("AnalyzerMessageFormat", resourceCulture); 79 | } 80 | } 81 | 82 | /// 83 | /// Looks up a localized string similar to Missing MapIdFor. 84 | /// 85 | internal static string AnalyzerTitle { 86 | get { 87 | return ResourceManager.GetString("AnalyzerTitle", resourceCulture); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /analyzers/Eventfully.Core.Analyzers/Eventfully.Core.Analyzers/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cfrenzel/Eventfully/bf32d3665898fb11b9bc5c25a63ffe6abe4b467b/appveyor.yml -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] 2 | Param( 3 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 4 | [string[]]$BuildArguments 5 | ) 6 | 7 | Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" 8 | 9 | Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } 10 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 11 | 12 | ########################################################################### 13 | # CONFIGURATION 14 | ########################################################################### 15 | 16 | $BuildProjectFile = "$PSScriptRoot\build\_build.csproj" 17 | $TempDirectory = "$PSScriptRoot\\.tmp" 18 | 19 | $DotNetGlobalFile = "$PSScriptRoot\\global.json" 20 | $DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" 21 | $DotNetChannel = "Current" 22 | 23 | $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 24 | $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 25 | 26 | ########################################################################### 27 | # EXECUTION 28 | ########################################################################### 29 | 30 | function ExecSafe([scriptblock] $cmd) { 31 | & $cmd 32 | if ($LASTEXITCODE) { exit $LASTEXITCODE } 33 | } 34 | 35 | # If global.json exists, load expected version 36 | if (Test-Path $DotNetGlobalFile) { 37 | $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) 38 | if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { 39 | $DotNetVersion = $DotNetGlobal.sdk.version 40 | } 41 | } 42 | 43 | # If dotnet is installed locally, and expected version is not set or installation matches the expected version 44 | if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` 45 | (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { 46 | $env:DOTNET_EXE = (Get-Command "dotnet").Path 47 | } 48 | else { 49 | $DotNetDirectory = "$TempDirectory\dotnet-win" 50 | $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" 51 | 52 | # Download install script 53 | $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" 54 | New-Item -ItemType Directory -Path $TempDirectory | Out-Null 55 | (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) 56 | 57 | # Install by channel or version 58 | if (!(Test-Path variable:DotNetVersion)) { 59 | ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } 60 | } else { 61 | ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } 62 | } 63 | } 64 | 65 | Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" 66 | 67 | ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false } 68 | ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } 69 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo $(bash --version 2>&1 | head -n 1) 4 | 5 | set -eo pipefail 6 | SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 7 | 8 | ########################################################################### 9 | # CONFIGURATION 10 | ########################################################################### 11 | 12 | BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj" 13 | TEMP_DIRECTORY="$SCRIPT_DIR//.tmp" 14 | 15 | DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" 16 | DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh" 17 | DOTNET_CHANNEL="Current" 18 | 19 | export DOTNET_CLI_TELEMETRY_OPTOUT=1 20 | export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 21 | 22 | ########################################################################### 23 | # EXECUTION 24 | ########################################################################### 25 | 26 | function FirstJsonValue { 27 | perl -nle 'print $1 if m{"'$1'": "([^"]+)",?}' <<< ${@:2} 28 | } 29 | 30 | # If global.json exists, load expected version 31 | if [ -f "$DOTNET_GLOBAL_FILE" ]; then 32 | DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE")) 33 | if [ "$DOTNET_VERSION" == "" ]; then 34 | unset DOTNET_VERSION 35 | fi 36 | fi 37 | 38 | # If dotnet is installed locally, and expected version is not set or installation matches the expected version 39 | if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version 2>&1) == "$DOTNET_VERSION") ]]; then 40 | export DOTNET_EXE="$(command -v dotnet)" 41 | else 42 | DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" 43 | export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" 44 | 45 | # Download install script 46 | DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" 47 | mkdir -p "$TEMP_DIRECTORY" 48 | curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" 49 | chmod +x "$DOTNET_INSTALL_FILE" 50 | 51 | # Install by channel or version 52 | if [ -z ${DOTNET_VERSION+x} ]; then 53 | "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path 54 | else 55 | "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path 56 | fi 57 | fi 58 | 59 | echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" 60 | 61 | "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false 62 | "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" 63 | -------------------------------------------------------------------------------- /build/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | dotnet_style_qualification_for_field = false:warning 3 | dotnet_style_qualification_for_property = false:warning 4 | dotnet_style_qualification_for_method = false:warning 5 | dotnet_style_qualification_for_event = false:warning 6 | dotnet_style_require_accessibility_modifiers = never:warning 7 | 8 | csharp_style_expression_bodied_properties = true:warning 9 | csharp_style_expression_bodied_indexers = true:warning 10 | csharp_style_expression_bodied_accessors = true:warning 11 | -------------------------------------------------------------------------------- /build/_build.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | false 7 | 8 | False 9 | CS0649;CS0169 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Migrations; 9 | using Eventfully; 10 | using Eventfully.EFCoreOutbox; 11 | using Microsoft.EntityFrameworkCore.Design; 12 | using Microsoft.Extensions.Configuration; 13 | using System.IO; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using System.Reflection; 16 | 17 | namespace Eventfully.Samples.ConsoleApp 18 | { 19 | public class ApplicationDbContext : DbContext, ISupportTransientDispatch 20 | { 21 | public event EventHandler ChangesPersisted; 22 | 23 | public ApplicationDbContext(DbContextOptions options) 24 | : base(options) 25 | {} 26 | 27 | public DbSet Orders { get; set; } 28 | 29 | 30 | 31 | protected override void OnModelCreating(ModelBuilder builder) 32 | { 33 | base.OnModelCreating(builder); 34 | 35 | /*** OUTBOX Message Entities (see Messaging.EFCoreOutbox) ***/ 36 | builder.AddEFCoreOutbox(); 37 | } 38 | 39 | public override int SaveChanges() 40 | { 41 | _preSaveChanges(); 42 | var res = base.SaveChanges(); 43 | _postSaveChanges(); 44 | return res; 45 | } 46 | 47 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) 48 | { 49 | _preSaveChanges(); 50 | var res = await base.SaveChangesAsync(cancellationToken); 51 | _postSaveChanges(); 52 | return res; 53 | } 54 | 55 | private void _preSaveChanges() 56 | { 57 | _addDateTimeStamps(); 58 | } 59 | private void _postSaveChanges() 60 | { 61 | this.ChangesPersisted?.Invoke(this, null); 62 | } 63 | 64 | 65 | private void _addDateTimeStamps() 66 | { 67 | foreach (var item in ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)) 68 | { 69 | var now = DateTime.UtcNow; 70 | 71 | if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAt") != null) 72 | item.Property("CreatedAt").CurrentValue = now; 73 | else if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAtUtc") != null) 74 | item.Property("CreatedAtUtc").CurrentValue = now; 75 | 76 | if (item.Metadata.FindProperty("UpdatedAt") != null) 77 | item.Property("UpdatedAt").CurrentValue = now; 78 | else if (item.Metadata.FindProperty("UpdatedAtUtc") != null) 79 | item.Property("UpdatedAtUtc").CurrentValue = now; 80 | } 81 | } 82 | 83 | } 84 | 85 | 86 | 87 | 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Entities/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | 7 | namespace Eventfully.Samples.ConsoleApp 8 | { 9 | public class Order 10 | { 11 | public Guid Id { get; set; } 12 | 13 | [Required, StringLength(500)] 14 | public string Number { get; set; } 15 | 16 | [Column(TypeName = "decimal(18,4)")] 17 | public Decimal Total { get; set; } 18 | 19 | [Required, StringLength(3)] 20 | public string CurrencyCode { get; set; } 21 | 22 | public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Eventfully.Samples.AzureServiceBus.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | b8751a4b-9e77-466f-9aab-1599e62b5ea1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | true 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | false 40 | Analyzer 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/MessagingProfile.cs: -------------------------------------------------------------------------------- 1 | using Eventfully.Transports.AzureServiceBus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Eventfully.Samples.ConsoleApp 8 | { 9 | public class MessagingProfile : Profile 10 | { 11 | 12 | public MessagingProfile(Microsoft.Extensions.Configuration.IConfiguration config) 13 | { 14 | ConfigureEndpoint("Events") 15 | .AsOutbound() 16 | .AsEventDefault() 17 | .BindEvent() 18 | //see AuzureKeyVaultSample for a better way 19 | .UseAesEncryption(config.GetSection("SampleAESKey").Value) 20 | .UseAzureServiceBusTransport() 21 | ; 22 | ConfigureEndpoint("Events-Sub") 23 | .AsInbound() 24 | .BindEvent() 25 | //see AuzureKeyVaultSample for a better way 26 | .UseAesEncryption(config.GetSection("SampleAESKey").Value) 27 | .UseAzureServiceBusTransport() 28 | ; 29 | 30 | ConfigureEndpoint("Commands") 31 | .AsInboundOutbound() 32 | .UseAzureServiceBusTransport() 33 | ; 34 | 35 | ConfigureEndpoint("Replies") 36 | .AsInboundOutbound() 37 | .AsReplyDefault() 38 | .UseAzureServiceBusTransport() 39 | ; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Migrations/20191203191923_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Eventfully.Samples.ConsoleApp.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20191203191923_Initial")] 14 | partial class Initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage", b => 25 | { 26 | b.Property("Id"); 27 | 28 | b.Property("CreatedAtUtc"); 29 | 30 | b.Property("Endpoint") 31 | .HasMaxLength(500); 32 | 33 | b.Property("ExpiresAtUtc"); 34 | 35 | b.Property("PriorityDateUtc"); 36 | 37 | b.Property("Status"); 38 | 39 | b.Property("TryCount"); 40 | 41 | b.Property("Type") 42 | .IsRequired() 43 | .HasMaxLength(500); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("PriorityDateUtc", "Status") 48 | .HasName("IX_PriorityDateUtc") 49 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 50 | 51 | b.ToTable("OutboxMessages"); 52 | }); 53 | 54 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 55 | { 56 | b.Property("Id"); 57 | 58 | b.Property("Data") 59 | .IsRequired(); 60 | 61 | b.Property("MetaData"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("OutboxMessageData"); 66 | }); 67 | 68 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 69 | { 70 | b.HasOne("Messaging.EFCoreOutbox.OutboxMessage") 71 | .WithOne("MessageData") 72 | .HasForeignKey("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 73 | .OnDelete(DeleteBehavior.Cascade); 74 | }); 75 | #pragma warning restore 612, 618 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Migrations/20191203191923_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Eventfully.Samples.ConsoleApp.Migrations 5 | { 6 | public partial class Initial : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "OutboxMessages", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | PriorityDateUtc = table.Column(nullable: false), 16 | TryCount = table.Column(nullable: false), 17 | Type = table.Column(maxLength: 500, nullable: false), 18 | Endpoint = table.Column(maxLength: 500, nullable: true), 19 | Status = table.Column(nullable: false), 20 | CreatedAtUtc = table.Column(nullable: false), 21 | ExpiresAtUtc = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_OutboxMessages", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "OutboxMessageData", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false), 33 | Data = table.Column(nullable: false), 34 | MetaData = table.Column(nullable: true) 35 | }, 36 | constraints: table => 37 | { 38 | table.PrimaryKey("PK_OutboxMessageData", x => x.Id); 39 | table.ForeignKey( 40 | name: "FK_OutboxMessageData_OutboxMessages_Id", 41 | column: x => x.Id, 42 | principalTable: "OutboxMessages", 43 | principalColumn: "Id", 44 | onDelete: ReferentialAction.Cascade); 45 | }); 46 | 47 | migrationBuilder.CreateIndex( 48 | name: "IX_PriorityDateUtc", 49 | table: "OutboxMessages", 50 | columns: new[] { "PriorityDateUtc", "Status" }) 51 | .Annotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 52 | } 53 | 54 | protected override void Down(MigrationBuilder migrationBuilder) 55 | { 56 | migrationBuilder.DropTable( 57 | name: "OutboxMessageData"); 58 | 59 | migrationBuilder.DropTable( 60 | name: "OutboxMessages"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Migrations/20200123164022_Orders.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Eventfully.Samples.ConsoleApp.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20200123164022_Orders")] 14 | partial class Orders 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 25 | { 26 | b.Property("Id"); 27 | 28 | b.Property("CreatedAtUtc"); 29 | 30 | b.Property("Endpoint") 31 | .HasMaxLength(500); 32 | 33 | b.Property("ExpiresAtUtc"); 34 | 35 | b.Property("PriorityDateUtc"); 36 | 37 | b.Property("Status"); 38 | 39 | b.Property("TryCount"); 40 | 41 | b.Property("Type") 42 | .IsRequired() 43 | .HasMaxLength(500); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("PriorityDateUtc", "Status") 48 | .HasName("IX_PriorityDateUtc") 49 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 50 | 51 | b.ToTable("OutboxMessages"); 52 | }); 53 | 54 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 55 | { 56 | b.Property("Id"); 57 | 58 | b.Property("Data") 59 | .IsRequired(); 60 | 61 | b.Property("MetaData"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("OutboxMessageData"); 66 | }); 67 | 68 | modelBuilder.Entity("Eventfully.Samples.ConsoleApp.Order", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd(); 72 | 73 | b.Property("CreatedAtUtc"); 74 | 75 | b.Property("CurrencyCode") 76 | .IsRequired() 77 | .HasMaxLength(3); 78 | 79 | b.Property("Number") 80 | .IsRequired() 81 | .HasMaxLength(500); 82 | 83 | b.Property("Total") 84 | .HasColumnType("decimal(18,4)"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("Orders"); 89 | }); 90 | 91 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 92 | { 93 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 94 | .WithOne("MessageData") 95 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 96 | .OnDelete(DeleteBehavior.Cascade); 97 | }); 98 | #pragma warning restore 612, 618 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Migrations/20200123164022_Orders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Eventfully.Samples.ConsoleApp.Migrations 5 | { 6 | public partial class Orders : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Orders", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | Number = table.Column(maxLength: 500, nullable: false), 16 | Total = table.Column(type: "decimal(18,4)", nullable: false), 17 | CurrencyCode = table.Column(maxLength: 3, nullable: false), 18 | CreatedAtUtc = table.Column(nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Orders", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Orders"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Eventfully.Samples.ConsoleApp.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 23 | { 24 | b.Property("Id"); 25 | 26 | b.Property("CreatedAtUtc"); 27 | 28 | b.Property("Endpoint") 29 | .HasMaxLength(500); 30 | 31 | b.Property("ExpiresAtUtc"); 32 | 33 | b.Property("PriorityDateUtc"); 34 | 35 | b.Property("Status"); 36 | 37 | b.Property("TryCount"); 38 | 39 | b.Property("Type") 40 | .IsRequired() 41 | .HasMaxLength(500); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.HasIndex("PriorityDateUtc", "Status") 46 | .HasName("IX_PriorityDateUtc") 47 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 48 | 49 | b.ToTable("OutboxMessages"); 50 | }); 51 | 52 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 53 | { 54 | b.Property("Id"); 55 | 56 | b.Property("Data") 57 | .IsRequired(); 58 | 59 | b.Property("MetaData"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.ToTable("OutboxMessageData"); 64 | }); 65 | 66 | modelBuilder.Entity("Eventfully.Samples.ConsoleApp.Order", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd(); 70 | 71 | b.Property("CreatedAtUtc"); 72 | 73 | b.Property("CurrencyCode") 74 | .IsRequired() 75 | .HasMaxLength(3); 76 | 77 | b.Property("Number") 78 | .IsRequired() 79 | .HasMaxLength(500); 80 | 81 | b.Property("Total") 82 | .HasColumnType("decimal(18,4)"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.ToTable("Orders"); 87 | }); 88 | 89 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 90 | { 91 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 92 | .WithOne("MessageData") 93 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 94 | .OnDelete(DeleteBehavior.Cascade); 95 | }); 96 | #pragma warning restore 612, 618 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/OrderCreated/OrderCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Samples.ConsoleApp 6 | { 7 | 8 | public class OrderCreated : IntegrationEvent 9 | { 10 | public override string MessageType => "Sales.OrderCreated"; 11 | 12 | public Guid OrderId { get; private set; } 13 | public Decimal TotalDue { get; private set; } 14 | public string CurrencyCode { get; private set; } 15 | public Address ShippingAddress { get; private set; } 16 | 17 | private OrderCreated(){} 18 | 19 | public OrderCreated(Guid orderId, Decimal totalDue, string currencyCode, Address shippignAddr) 20 | { 21 | this.OrderId = orderId; 22 | this.TotalDue = totalDue; 23 | this.CurrencyCode = currencyCode; 24 | this.ShippingAddress = shippignAddr; 25 | } 26 | 27 | public class Address 28 | { 29 | public string Line1 { get; private set; } 30 | public string Line2 { get; private set; } 31 | public string City { get; private set; } 32 | public string StateCode { get; private set; } 33 | public string Zip { get; private set; } 34 | 35 | private Address(){} 36 | public Address(string line1, string line2, string city, string state, string zip) 37 | { 38 | this.Line1 = line1; 39 | this.Line2 = line2; 40 | this.City = city; 41 | this.StateCode = state; 42 | this.Zip = zip; 43 | } 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/OrderCreated/OrderCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class OrderCreatedHandler : IMessageHandler 9 | { 10 | public Task Handle(OrderCreated ev, MessageContext context) 11 | { 12 | Console.WriteLine($"Received OrderCreated Event"); 13 | Console.WriteLine($"\tOrderId: {ev.OrderId}"); 14 | Console.WriteLine($"\tTotal Due: {ev.TotalDue} {ev.CurrencyCode}"); 15 | Console.WriteLine($"\tShipping Address:"); 16 | Console.WriteLine($"\t\t{ev.ShippingAddress.Line1}"); 17 | if(!String.IsNullOrEmpty(ev.ShippingAddress.Line2)) 18 | Console.WriteLine($"\t\t{ev.ShippingAddress.Line2}"); 19 | Console.WriteLine($"\t\t{ev.ShippingAddress.City}, {ev.ShippingAddress.StateCode} {ev.ShippingAddress.Zip}"); 20 | Console.WriteLine(); 21 | 22 | return Task.CompletedTask; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/PaymentMethodCreated/PaymentMethodCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Samples.ConsoleApp 6 | { 7 | 8 | public class PaymentMethodCreated : IntegrationEvent 9 | { 10 | public override string MessageType => "Sales.PaymentMethodCreated"; 11 | 12 | public Guid PaymentMethodId { get; private set; } 13 | 14 | public CardInfo MethodInfo { get; private set; } 15 | 16 | private PaymentMethodCreated(){} 17 | 18 | public PaymentMethodCreated(Guid methodId, CardInfo methodInfo) 19 | { 20 | this.PaymentMethodId = methodId; 21 | this.MethodInfo = methodInfo; 22 | } 23 | 24 | public class CardInfo 25 | { 26 | public string Number { get; private set; } 27 | public string NameOnCard { get; set; } 28 | public string ExpirationYear { get; private set; } 29 | public string ExpirationMonth { get; private set; } 30 | 31 | private CardInfo(){} 32 | public CardInfo(string number, string nameOn, string expirationYear, string expirationMonth) 33 | { 34 | this.Number = number; 35 | this.NameOnCard = nameOn; 36 | this.ExpirationYear = expirationYear; 37 | this.ExpirationMonth = expirationMonth; 38 | } 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/PaymentMethodCreated/PaymentMethodCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class PaymentMethodCreatedHandler : IMessageHandler 9 | { 10 | public Task Handle(PaymentMethodCreated ev, MessageContext context) 11 | { 12 | Console.WriteLine($"Received PaymentMethodCreated Event"); 13 | Console.WriteLine($"\tId: {ev.PaymentMethodId}"); 14 | Console.WriteLine($"\tPayment Method Info:"); 15 | Console.WriteLine($"\t\t Name: {ev.MethodInfo.NameOnCard}"); 16 | Console.WriteLine($"\t\t Number: {ev.MethodInfo.Number}"); 17 | Console.WriteLine($"\t\t Expires: {ev.MethodInfo.ExpirationMonth} / {ev.MethodInfo.ExpirationYear}"); 18 | Console.WriteLine(); 19 | 20 | return Task.CompletedTask; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/Undependable/FailingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class FailingHandler 9 | { 10 | 11 | public class Event : IntegrationEvent 12 | { 13 | public override string MessageType => "FailingHandler.Created"; 14 | public DateTime FailUntilUtc { get; set; } 15 | public Guid Id { get; set; } = Guid.NewGuid(); 16 | 17 | private Event() { } 18 | public Event(DateTime failUntilUtc) => FailUntilUtc = failUntilUtc; 19 | 20 | } 21 | 22 | public class Handler : IMessageHandler 23 | { 24 | public Task Handle(Event ev, MessageContext context) 25 | { 26 | Console.WriteLine($"Received FailingHandler.Created Event"); 27 | Console.WriteLine($"\tId: {ev.Id}"); 28 | if (DateTime.UtcNow < ev.FailUntilUtc) 29 | { 30 | Console.WriteLine($"\tFailing Until: {ev.FailUntilUtc} - {(ev.FailUntilUtc - DateTime.UtcNow).TotalMinutes :N1} minutes remaining"); 31 | Console.WriteLine(); 32 | throw new ApplicationException($"Failing until {ev.FailUntilUtc}"); 33 | } 34 | Console.WriteLine($"\tSucceeded After: {ev.FailUntilUtc}"); 35 | Console.WriteLine(); 36 | return Task.CompletedTask; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.AzureServiceBus.ConsoleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "ConnectionStrings": { 4 | }, 5 | "SampleAESKey": "nZr4u7x!A%D*F-JaNdRgUkXp2s5v8y/B", 6 | 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Warning", 10 | "System": "Warning", 11 | "Microsoft": "Warning", 12 | "Eventfully": "Information" 13 | } 14 | } 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Migrations; 9 | using Eventfully; 10 | using Eventfully.EFCoreOutbox; 11 | using Microsoft.EntityFrameworkCore.Design; 12 | using Microsoft.Extensions.Configuration; 13 | using System.IO; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using System.Reflection; 16 | 17 | namespace Eventfully.Samples.ConsoleApp 18 | { 19 | public class ApplicationDbContext : DbContext, ISupportTransientDispatch 20 | { 21 | public event EventHandler ChangesPersisted; 22 | 23 | public ApplicationDbContext(DbContextOptions options) 24 | : base(options) 25 | {} 26 | 27 | public DbSet Orders { get; set; } 28 | 29 | 30 | 31 | protected override void OnModelCreating(ModelBuilder builder) 32 | { 33 | base.OnModelCreating(builder); 34 | 35 | /*** OUTBOX Message Entities (see Messaging.EFCoreOutbox) ***/ 36 | builder.AddEFCoreOutbox(); 37 | } 38 | 39 | public override int SaveChanges() 40 | { 41 | _preSaveChanges(); 42 | var res = base.SaveChanges(); 43 | _postSaveChanges(); 44 | return res; 45 | } 46 | 47 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) 48 | { 49 | _preSaveChanges(); 50 | var res = await base.SaveChangesAsync(cancellationToken); 51 | _postSaveChanges(); 52 | return res; 53 | } 54 | 55 | private void _preSaveChanges() 56 | { 57 | _addDateTimeStamps(); 58 | } 59 | private void _postSaveChanges() 60 | { 61 | this.ChangesPersisted?.Invoke(this, null); 62 | } 63 | 64 | 65 | private void _addDateTimeStamps() 66 | { 67 | foreach (var item in ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)) 68 | { 69 | var now = DateTime.UtcNow; 70 | 71 | if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAt") != null) 72 | item.Property("CreatedAt").CurrentValue = now; 73 | else if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAtUtc") != null) 74 | item.Property("CreatedAtUtc").CurrentValue = now; 75 | 76 | if (item.Metadata.FindProperty("UpdatedAt") != null) 77 | item.Property("UpdatedAt").CurrentValue = now; 78 | else if (item.Metadata.FindProperty("UpdatedAtUtc") != null) 79 | item.Property("UpdatedAtUtc").CurrentValue = now; 80 | } 81 | } 82 | 83 | } 84 | 85 | 86 | 87 | 88 | } 89 | 90 | 91 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Entities/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | using System.Text; 6 | 7 | namespace Eventfully.Samples.ConsoleApp 8 | { 9 | public class Order 10 | { 11 | public Guid Id { get; set; } 12 | 13 | [Required, StringLength(500)] 14 | public string Number { get; set; } 15 | 16 | [Column(TypeName = "decimal(18,4)")] 17 | public Decimal Total { get; set; } 18 | 19 | [Required, StringLength(3)] 20 | public string CurrencyCode { get; set; } 21 | 22 | public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Eventfully.Samples.ConsoleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp3.0 6 | b8751a4b-9e77-466f-9aab-1599e62b5ea1 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | true 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/MessagingProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class MessagingProfile : Profile 9 | { 10 | 11 | public MessagingProfile(Microsoft.Extensions.Configuration.IConfiguration config) 12 | { 13 | ConfigureEndpoint("Events") 14 | .AsInboundOutbound() 15 | .BindEvent() 16 | //see AuzureKeyVaultSample for a better way 17 | .UseAesEncryption(config.GetSection("SampleAESKey").Value) 18 | .UseLocalTransport() 19 | ; 20 | 21 | ConfigureEndpoint("Commands") 22 | .AsInboundOutbound() 23 | .UseLocalTransport() 24 | ; 25 | 26 | ConfigureEndpoint("Replies") 27 | .AsInboundOutbound() 28 | .AsReplyDefault() 29 | .UseLocalTransport() 30 | ; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Migrations/20191203191923_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Eventfully.Samples.ConsoleApp.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20191203191923_Initial")] 14 | partial class Initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage", b => 25 | { 26 | b.Property("Id"); 27 | 28 | b.Property("CreatedAtUtc"); 29 | 30 | b.Property("Endpoint") 31 | .HasMaxLength(500); 32 | 33 | b.Property("ExpiresAtUtc"); 34 | 35 | b.Property("PriorityDateUtc"); 36 | 37 | b.Property("Status"); 38 | 39 | b.Property("TryCount"); 40 | 41 | b.Property("Type") 42 | .IsRequired() 43 | .HasMaxLength(500); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("PriorityDateUtc", "Status") 48 | .HasName("IX_PriorityDateUtc") 49 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 50 | 51 | b.ToTable("OutboxMessages"); 52 | }); 53 | 54 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 55 | { 56 | b.Property("Id"); 57 | 58 | b.Property("Data") 59 | .IsRequired(); 60 | 61 | b.Property("MetaData"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("OutboxMessageData"); 66 | }); 67 | 68 | modelBuilder.Entity("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 69 | { 70 | b.HasOne("Messaging.EFCoreOutbox.OutboxMessage") 71 | .WithOne("MessageData") 72 | .HasForeignKey("Messaging.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 73 | .OnDelete(DeleteBehavior.Cascade); 74 | }); 75 | #pragma warning restore 612, 618 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Migrations/20191203191923_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Eventfully.Samples.ConsoleApp.Migrations 5 | { 6 | public partial class Initial : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "OutboxMessages", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | PriorityDateUtc = table.Column(nullable: false), 16 | TryCount = table.Column(nullable: false), 17 | Type = table.Column(maxLength: 500, nullable: false), 18 | Endpoint = table.Column(maxLength: 500, nullable: true), 19 | Status = table.Column(nullable: false), 20 | CreatedAtUtc = table.Column(nullable: false), 21 | ExpiresAtUtc = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_OutboxMessages", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "OutboxMessageData", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false), 33 | Data = table.Column(nullable: false), 34 | MetaData = table.Column(nullable: true) 35 | }, 36 | constraints: table => 37 | { 38 | table.PrimaryKey("PK_OutboxMessageData", x => x.Id); 39 | table.ForeignKey( 40 | name: "FK_OutboxMessageData_OutboxMessages_Id", 41 | column: x => x.Id, 42 | principalTable: "OutboxMessages", 43 | principalColumn: "Id", 44 | onDelete: ReferentialAction.Cascade); 45 | }); 46 | 47 | migrationBuilder.CreateIndex( 48 | name: "IX_PriorityDateUtc", 49 | table: "OutboxMessages", 50 | columns: new[] { "PriorityDateUtc", "Status" }) 51 | .Annotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 52 | } 53 | 54 | protected override void Down(MigrationBuilder migrationBuilder) 55 | { 56 | migrationBuilder.DropTable( 57 | name: "OutboxMessageData"); 58 | 59 | migrationBuilder.DropTable( 60 | name: "OutboxMessages"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Migrations/20200123164022_Orders.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Eventfully.Samples.ConsoleApp.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20200123164022_Orders")] 14 | partial class Orders 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 25 | { 26 | b.Property("Id"); 27 | 28 | b.Property("CreatedAtUtc"); 29 | 30 | b.Property("Endpoint") 31 | .HasMaxLength(500); 32 | 33 | b.Property("ExpiresAtUtc"); 34 | 35 | b.Property("PriorityDateUtc"); 36 | 37 | b.Property("Status"); 38 | 39 | b.Property("TryCount"); 40 | 41 | b.Property("Type") 42 | .IsRequired() 43 | .HasMaxLength(500); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("PriorityDateUtc", "Status") 48 | .HasName("IX_PriorityDateUtc") 49 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 50 | 51 | b.ToTable("OutboxMessages"); 52 | }); 53 | 54 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 55 | { 56 | b.Property("Id"); 57 | 58 | b.Property("Data") 59 | .IsRequired(); 60 | 61 | b.Property("MetaData"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("OutboxMessageData"); 66 | }); 67 | 68 | modelBuilder.Entity("Eventfully.Samples.ConsoleApp.Order", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd(); 72 | 73 | b.Property("CreatedAtUtc"); 74 | 75 | b.Property("CurrencyCode") 76 | .IsRequired() 77 | .HasMaxLength(3); 78 | 79 | b.Property("Number") 80 | .IsRequired() 81 | .HasMaxLength(500); 82 | 83 | b.Property("Total") 84 | .HasColumnType("decimal(18,4)"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("Orders"); 89 | }); 90 | 91 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 92 | { 93 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 94 | .WithOne("MessageData") 95 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 96 | .OnDelete(DeleteBehavior.Cascade); 97 | }); 98 | #pragma warning restore 612, 618 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Migrations/20200123164022_Orders.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Eventfully.Samples.ConsoleApp.Migrations 5 | { 6 | public partial class Orders : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "Orders", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | Number = table.Column(maxLength: 500, nullable: false), 16 | Total = table.Column(type: "decimal(18,4)", nullable: false), 17 | CurrencyCode = table.Column(maxLength: 3, nullable: false), 18 | CreatedAtUtc = table.Column(nullable: false) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Orders", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Orders"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.Samples.ConsoleApp; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Eventfully.Samples.ConsoleApp.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 23 | { 24 | b.Property("Id"); 25 | 26 | b.Property("CreatedAtUtc"); 27 | 28 | b.Property("Endpoint") 29 | .HasMaxLength(500); 30 | 31 | b.Property("ExpiresAtUtc"); 32 | 33 | b.Property("PriorityDateUtc"); 34 | 35 | b.Property("Status"); 36 | 37 | b.Property("TryCount"); 38 | 39 | b.Property("Type") 40 | .IsRequired() 41 | .HasMaxLength(500); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.HasIndex("PriorityDateUtc", "Status") 46 | .HasName("IX_PriorityDateUtc") 47 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 48 | 49 | b.ToTable("OutboxMessages"); 50 | }); 51 | 52 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 53 | { 54 | b.Property("Id"); 55 | 56 | b.Property("Data") 57 | .IsRequired(); 58 | 59 | b.Property("MetaData"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.ToTable("OutboxMessageData"); 64 | }); 65 | 66 | modelBuilder.Entity("Eventfully.Samples.ConsoleApp.Order", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd(); 70 | 71 | b.Property("CreatedAtUtc"); 72 | 73 | b.Property("CurrencyCode") 74 | .IsRequired() 75 | .HasMaxLength(3); 76 | 77 | b.Property("Number") 78 | .IsRequired() 79 | .HasMaxLength(500); 80 | 81 | b.Property("Total") 82 | .HasColumnType("decimal(18,4)"); 83 | 84 | b.HasKey("Id"); 85 | 86 | b.ToTable("Orders"); 87 | }); 88 | 89 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 90 | { 91 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 92 | .WithOne("MessageData") 93 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 94 | .OnDelete(DeleteBehavior.Cascade); 95 | }); 96 | #pragma warning restore 612, 618 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/OrderCreated/OrderCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Samples.ConsoleApp 6 | { 7 | 8 | public class OrderCreated : IntegrationEvent 9 | { 10 | public override string MessageType => "Sales.OrderCreated"; 11 | 12 | public Guid OrderId { get; private set; } 13 | public Decimal TotalDue { get; private set; } 14 | public string CurrencyCode { get; private set; } 15 | public Address ShippingAddress { get; private set; } 16 | 17 | private OrderCreated(){} 18 | 19 | public OrderCreated(Guid orderId, Decimal totalDue, string currencyCode, Address shippignAddr) 20 | { 21 | this.OrderId = orderId; 22 | this.TotalDue = totalDue; 23 | this.CurrencyCode = currencyCode; 24 | this.ShippingAddress = shippignAddr; 25 | } 26 | 27 | public class Address 28 | { 29 | public string Line1 { get; private set; } 30 | public string Line2 { get; private set; } 31 | public string City { get; private set; } 32 | public string StateCode { get; private set; } 33 | public string Zip { get; private set; } 34 | 35 | private Address(){} 36 | public Address(string line1, string line2, string city, string state, string zip) 37 | { 38 | this.Line1 = line1; 39 | this.Line2 = line2; 40 | this.City = city; 41 | this.StateCode = state; 42 | this.Zip = zip; 43 | } 44 | 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/OrderCreated/OrderCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class OrderCreatedHandler : IMessageHandler 9 | { 10 | public Task Handle(OrderCreated ev, MessageContext context) 11 | { 12 | Console.WriteLine($"Received OrderCreated Event"); 13 | Console.WriteLine($"\tOrderId: {ev.OrderId}"); 14 | Console.WriteLine($"\tTotal Due: {ev.TotalDue} {ev.CurrencyCode}"); 15 | Console.WriteLine($"\tShipping Address:"); 16 | Console.WriteLine($"\t\t{ev.ShippingAddress.Line1}"); 17 | if(!String.IsNullOrEmpty(ev.ShippingAddress.Line2)) 18 | Console.WriteLine($"\t\t{ev.ShippingAddress.Line2}"); 19 | Console.WriteLine($"\t\t{ev.ShippingAddress.City}, {ev.ShippingAddress.StateCode} {ev.ShippingAddress.Zip}"); 20 | Console.WriteLine(); 21 | 22 | return Task.CompletedTask; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/PaymentMethodCreated/PaymentMethodCreated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Samples.ConsoleApp 6 | { 7 | 8 | public class PaymentMethodCreated : IntegrationEvent 9 | { 10 | public override string MessageType => "Sales.PaymentMethodCreated"; 11 | 12 | public Guid PaymentMethodId { get; private set; } 13 | 14 | public CardInfo MethodInfo { get; private set; } 15 | 16 | private PaymentMethodCreated(){} 17 | 18 | public PaymentMethodCreated(Guid methodId, CardInfo methodInfo) 19 | { 20 | this.PaymentMethodId = methodId; 21 | this.MethodInfo = methodInfo; 22 | } 23 | 24 | public class CardInfo 25 | { 26 | public string Number { get; private set; } 27 | public string NameOnCard { get; set; } 28 | public string ExpirationYear { get; private set; } 29 | public string ExpirationMonth { get; private set; } 30 | 31 | private CardInfo(){} 32 | public CardInfo(string number, string nameOn, string expirationYear, string expirationMonth) 33 | { 34 | this.Number = number; 35 | this.NameOnCard = nameOn; 36 | this.ExpirationYear = expirationYear; 37 | this.ExpirationMonth = expirationMonth; 38 | } 39 | 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/PaymentMethodCreated/PaymentMethodCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Samples.ConsoleApp 7 | { 8 | public class PaymentMethodCreatedHandler : IMessageHandler 9 | { 10 | public Task Handle(PaymentMethodCreated ev, MessageContext context) 11 | { 12 | Console.WriteLine($"Received PaymentMethodCreated Event"); 13 | Console.WriteLine($"\tId: {ev.PaymentMethodId}"); 14 | Console.WriteLine($"\tPayment Method Info:"); 15 | Console.WriteLine($"\t\t Name: {ev.MethodInfo.NameOnCard}"); 16 | Console.WriteLine($"\t\t Number: {ev.MethodInfo.Number}"); 17 | Console.WriteLine($"\t\t Expires: {ev.MethodInfo.ExpirationMonth} / {ev.MethodInfo.ExpirationYear}"); 18 | Console.WriteLine(); 19 | 20 | return Task.CompletedTask; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/EFCore/Eventfully.Samples.ConsoleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationConnection": "Server=(localdb)\\mssqllocaldb;Database=eventfully-dev;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | 6 | "SampleAESKey": "nZr4u7x!A%D*F-JaNdRgUkXp2s5v8y/B", 7 | 8 | "Logging": { 9 | "LogLevel": { 10 | "Default": "Warning", 11 | "System": "Warning", 12 | "Microsoft": "Warning", 13 | "Eventfully": "Information" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Eventfully.AzureKeyVault/AzureKeyVaultKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using Azure.Identity; 2 | using Azure.Security.KeyVault.Secrets; 3 | using Microsoft.Azure.KeyVault; 4 | using Microsoft.Azure.Services.AppAuthentication; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace Eventfully 9 | { 10 | 11 | public class AzureKeyVaultKeyProvider : IEncryptionKeyProvider 12 | { 13 | private readonly string _keyVaultUrl; 14 | private static readonly Dictionary _clients = new Dictionary(); 15 | 16 | public AzureKeyVaultKeyProvider(string keyVaultuUrl) 17 | { 18 | _keyVaultUrl = keyVaultuUrl; 19 | } 20 | 21 | public string GetKey(string secretName = null) 22 | { 23 | var client = _getClient(); 24 | //var secret = client.GetSecret(secretName); 25 | //return secret.Value.Value; 26 | var secret = client.GetSecretAsync(_keyVaultUrl, secretName).GetAwaiter().GetResult(); 27 | return secret.Value; 28 | } 29 | 30 | private KeyVaultClient _getClient() 31 | { 32 | if (_clients.ContainsKey(this._keyVaultUrl)) 33 | return _clients[_keyVaultUrl]; 34 | else 35 | { 36 | 37 | var azureServiceTokenProvider = new AzureServiceTokenProvider(); 38 | var client = new KeyVaultClient( 39 | new KeyVaultClient.AuthenticationCallback( 40 | azureServiceTokenProvider.KeyVaultTokenCallback)); 41 | return client; 42 | } 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/Eventfully.AzureKeyVault/Eventfully.AzureKeyVault.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Eventfully.AzureKeyVault/README.txt: -------------------------------------------------------------------------------- 1 |  2 | Azure Key Vault Secrets - Managed Service Identity based key provider 3 | https://docs.microsoft.com/en-us/samples/azure-samples/app-service-msi-keyvault-dotnet/keyvault-msi-appservice-sample/ 4 | 5 | Configuring Azure Key Vault for use as EncryptionKeyProvider 6 | 7 | 1) Generate/Use existing AES key of length: 128, 192 or 256 8 | - ex: openssl rand 128 > sym_keyfile.key 9 | 10 | 2) Import Key into Key Vault 11 | - Create a New Secret in the KeyVault 12 | - Name: 13 | - Value: 14 | - ContentType: application/octet-stream 15 | 16 | 3) Authenticating with the KeyVault 17 | - By default the provider will Managed Service Identity to transparently authenticate ( make sure you app has permissions to acces the vault). 18 | - This works great while the app is running in azure, but when developing in VS you need to be logged into Azure with an account that has access 19 | 20 | 21 | 4) Using the provider 22 | 23 | ***** Using the provider for a single message type ********* 24 | 25 | ConfigureEndpoint("TestEndpoint") 26 | .AsInboundOutbound() 27 | .BindEvent() 28 | .UseEncryption( 29 | "yourkeyname", 30 | new AzureKeyVaultKeyProvider("yourkeyvaulturl") 31 | ) 32 | .UseAzureServiceBusTransport() 33 | ; 34 | 35 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Config/EndpointBindings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public class EndpointBindings 8 | { 9 | 10 | public List Endpoints { get; set; } 11 | public class EndpointBinding 12 | { 13 | public string Name { get; set; } 14 | public string ConnectionString { get; set; } 15 | 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Config/IEndpointFluent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Eventfully.Filters; 5 | using Eventfully.Transports; 6 | 7 | namespace Eventfully 8 | { 9 | public interface IEndpointFluent 10 | { 11 | TransportSettings TransportSettings { get; set; } 12 | 13 | EndpointSettings AsEventDefault(); 14 | EndpointSettings AsInbound(); 15 | EndpointSettings AsInboundOutbound(); 16 | EndpointSettings AsOutbound(); 17 | EndpointSettings AsReplyDefault(); 18 | 19 | BindMessageSettings BindCommand() where T : IIntegrationCommand; 20 | BindMessageSettings BindEvent() where T : IIntegrationEvent; 21 | EndpointSettings BindCommand(string messageTypeIdentifier); 22 | 23 | EndpointSettings WithFilter(params IIntegrationMessageFilter[] filters); 24 | EndpointSettings WithFilter(params ITransportMessageFilter[] filters); 25 | 26 | //EndpointSettings WithOutboundFilter(params IIntegrationMessageFilter[] filters); 27 | //EndpointSettings WithOutboundFilter(params ITransportMessageFilter[] filters); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Config/MessagingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public class MessagingConfiguration 8 | { 9 | public ICountingSemaphore OutboxConsumerSemaphore { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Config/MessagingProfile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Eventfully 7 | { 8 | public class Profile 9 | { 10 | public List EndpointSettings { get; private set; } = new List(); 11 | 12 | public EndpointSettings ConfigureEndpoint(string name) => ConfigureEndpoint(name, null); 13 | 14 | public EndpointSettings ConfigureEndpoint(string name, string connectionString) 15 | { 16 | var endpointSettings = new EndpointSettings(name, connectionString); 17 | EndpointSettings.Add(endpointSettings); 18 | return endpointSettings; 19 | } 20 | 21 | public Profile AddBindings(EndpointBindings bindings) 22 | { 23 | if (bindings == null || EndpointSettings.Count < 1) 24 | return this; 25 | 26 | foreach(var binding in bindings.Endpoints) 27 | { 28 | var endpoint = EndpointSettings.SingleOrDefault(x => x.Name.Equals(binding.Name, StringComparison.OrdinalIgnoreCase)); 29 | if (endpoint != null) 30 | { 31 | if (String.IsNullOrEmpty(endpoint.ConnectionString)) 32 | endpoint.ConnectionString = binding.ConnectionString; 33 | } 34 | } 35 | return this; 36 | } 37 | 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Eventfully.Core/DependencyInjection/IRegistrar.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public interface IServiceRegistrar 8 | { 9 | void AddTransient(); 10 | void AddTransient() where T: I; 11 | 12 | void AddSingleton(object o); 13 | void AddSingleton(object o); 14 | void AddSingleton(); 15 | void AddSingleton(bool asBoth=false) where T : I; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Eventfully.Core/DependencyInjection/IServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public interface IServiceFactory: IDisposable 8 | { 9 | object GetInstance(Type type); 10 | IServiceFactory CreateScope(); 11 | } 12 | 13 | public static class ServiceFactoryExtension 14 | { 15 | public static T GetInstance(this IServiceFactory factory) 16 | { 17 | return (T)factory.GetInstance(typeof(T)); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Endpoints/Endpoint.cs: -------------------------------------------------------------------------------- 1 | using Eventfully.Transports; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Eventfully 9 | { 10 | 11 | public class Endpoint : IEndpoint 12 | { 13 | protected readonly EndpointSettings _settings; 14 | protected readonly ITransport _transport; 15 | 16 | public string Name { get; protected set; } 17 | 18 | public EndpointSettings Settings => _settings; 19 | public ITransport Transport => _transport; 20 | 21 | public HashSet BoundMessageTypes { get; protected set; } = new HashSet(); 22 | public HashSet BoundMessageIdentifiers { get; protected set; } = new HashSet(); 23 | 24 | public bool IsReader { get; protected set; } 25 | public bool IsWriter { get; protected set; } 26 | 27 | public bool SupportsDelayedDispatch => _transport.SupportsDelayedDispatch; 28 | 29 | public Endpoint(EndpointSettings settings) 30 | :this(settings, null) 31 | {} 32 | public Endpoint(EndpointSettings settings, ITransport transport) 33 | { 34 | _settings = settings; 35 | this.Name = settings.Name; 36 | this.IsReader = settings.IsReader; 37 | this.IsWriter = settings.IsWriter; 38 | 39 | if (settings.MessageTypes != null) 40 | { 41 | foreach (Type messageType in settings.MessageTypes) 42 | this.BoundMessageTypes.Add(messageType); 43 | } 44 | 45 | if (settings.MessageTypeIdentifiers != null) 46 | { 47 | foreach (string messageTypeIdentifier in settings.MessageTypeIdentifiers) 48 | this.BoundMessageIdentifiers.Add(messageTypeIdentifier); 49 | } 50 | 51 | //create the transport from the supplied factory 52 | if (transport != null) 53 | _transport = transport; 54 | 55 | if(_transport == null) 56 | _transport = settings.TransportSettings.Create(); 57 | } 58 | 59 | public virtual Task StartAsync(Handler handler, CancellationToken cancellationToken = default(CancellationToken)) 60 | { 61 | return _transport.StartAsync(this, handler, cancellationToken); 62 | } 63 | 64 | public virtual Task StopAsync() 65 | { 66 | return _transport.StopAsync(); 67 | } 68 | 69 | public virtual Task Dispatch(string messageTypeIdenfifier, byte[] message, MessageMetaData metaData = null) 70 | { 71 | return Transport.Dispatch(messageTypeIdenfifier, message, this, metaData); 72 | } 73 | 74 | public virtual void SetReplyToForCommand(IIntegrationCommand command, MessageMetaData meta) 75 | { 76 | Transport.SetReplyToForCommand(this, command, meta); 77 | } 78 | 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Endpoints/IEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Eventfully.Transports; 6 | 7 | namespace Eventfully 8 | { 9 | public interface IEndpoint 10 | { 11 | HashSet BoundMessageIdentifiers { get; } 12 | HashSet BoundMessageTypes { get; } 13 | bool IsReader { get; } 14 | bool IsWriter { get; } 15 | string Name { get; } 16 | EndpointSettings Settings { get; } 17 | bool SupportsDelayedDispatch { get; } 18 | ITransport Transport { get; } 19 | 20 | Task Dispatch(string messageTypeIdenfifier, byte[] message, MessageMetaData metaData = null); 21 | void SetReplyToForCommand(IIntegrationCommand command, MessageMetaData meta); 22 | Task StartAsync(Handler handler, CancellationToken cancellationToken = default); 23 | Task StopAsync(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Eventfully.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Eventfully 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/ExpirationCheckStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Eventfully.Filters 5 | { 6 | public class ExpirationCheckStep : IPipelineStep 7 | { 8 | public IntegrationMessageFilterContext Process(IntegrationMessageFilterContext context) 9 | { 10 | if (context.Message != null) 11 | { 12 | if (context.MessageMetaData != null && context.MessageMetaData.IsExpired()) 13 | throw new MessageExpiredException(context.Message.MessageType); 14 | } 15 | return context; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/IEncryptionKeyProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | /// 9 | /// Provide a base64 encoded key for encryption 10 | /// 11 | public interface IEncryptionKeyProvider 12 | { 13 | /// 14 | /// Get an encryption key 15 | /// 16 | /// 17 | /// A base64 encoded key 18 | string GetKey(string keyName = null); 19 | } 20 | 21 | public class StringKeyProvider : IEncryptionKeyProvider 22 | { 23 | private readonly string _key; 24 | private readonly bool _isBase64Encoded; 25 | public StringKeyProvider(string key, bool isBase64Encoded = false) 26 | { 27 | _key = key; 28 | _isBase64Encoded = isBase64Encoded; 29 | } 30 | 31 | public string GetKey(string keyName = null) 32 | { 33 | if (!_isBase64Encoded) 34 | { 35 | var bytes = System.Text.Encoding.UTF8.GetBytes(_key); 36 | return System.Convert.ToBase64String(bytes); 37 | } 38 | return _key; 39 | } 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/IIntegrationMessageFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | 6 | namespace Eventfully.Filters 7 | { 8 | /// 9 | /// A filter that acts on messages and meta data in the IIntegrationMessage form 10 | /// Ex. modifying headers based on message content 11 | /// to act on messages while in the byte[] form 12 | /// 13 | public interface IIntegrationMessageFilter : IPipelineStep 14 | { 15 | FilterDirection SupportsDirection { get; } 16 | 17 | new IntegrationMessageFilterContext Process(IntegrationMessageFilterContext context); 18 | } 19 | 20 | public class IntegrationMessageFilterContext 21 | { 22 | public FilterDirection Direction { get; set; } 23 | 24 | public IIntegrationMessage Message { get; set; } 25 | public MessageMetaData MessageMetaData { get; set; } 26 | public IEndpoint Endpoint { get; set; } 27 | 28 | public MessageTypeProperties Props { get; set; } 29 | 30 | public IntegrationMessageFilterContext(IIntegrationMessage message, MessageMetaData meta, IEndpoint endpoint, FilterDirection direction, MessageTypeProperties props) 31 | { 32 | Message = message; 33 | MessageMetaData = meta; 34 | Endpoint = endpoint; 35 | Direction = direction; 36 | Props = props; 37 | } 38 | } 39 | 40 | 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/ITransportMessageFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Newtonsoft.Json; 5 | 6 | namespace Eventfully.Filters 7 | { 8 | [Flags] 9 | public enum FilterDirection 10 | { 11 | Inbound = 1, 12 | Outbound = 2, 13 | } 14 | /// 15 | /// A filter that acts on messages and meta data while the message content is in 16 | /// its byte[] form. Ex. Encrypting/Decrypting the message context 17 | /// to act on messages while in the IIntegrationMessage format 18 | /// 19 | public interface ITransportMessageFilter : IPipelineStep 20 | { 21 | FilterDirection SupportsDirection { get;} 22 | new TransportMessageFilterContext Process(TransportMessageFilterContext context); 23 | } 24 | 25 | public class TransportMessageFilterContext 26 | { 27 | public FilterDirection Direction { get; set; } 28 | public TransportMessage TransportMessage { get; set; } 29 | public IEndpoint Endpoint { get; set; } 30 | 31 | public TransportMessageFilterContext(TransportMessage message, IEndpoint endpoint, FilterDirection direction) 32 | { 33 | TransportMessage = message; 34 | Endpoint = endpoint; 35 | Direction = direction; 36 | } 37 | } 38 | 39 | 40 | 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/InboundMessagePipeline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Eventfully.Filters 6 | { 7 | public class InboundMessagePipeline : Pipeline 8 | { 9 | public InboundMessagePipeline(IEnumerable userTransportFilters, IEnumerable userIntegrationFilters) 10 | { 11 | Pipeline userPrePipeline = null; 12 | if (userTransportFilters != null && userTransportFilters.Count() > 0) 13 | userPrePipeline = new Pipeline(userTransportFilters); 14 | 15 | Pipeline userPostPipeline = null; 16 | if (userIntegrationFilters != null && userIntegrationFilters.Count() > 0) 17 | userPostPipeline = new Pipeline(userIntegrationFilters); 18 | 19 | this.Steps = input => 20 | { 21 | return input 22 | .Step(userPrePipeline) 23 | .Step(new MessageExtractionStep()) 24 | .Step(userPostPipeline) 25 | .Step(new ExpirationCheckStep()) 26 | ; 27 | }; 28 | } 29 | } 30 | 31 | 32 | 33 | } 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/MessageExtractionStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Serialization; 7 | 8 | namespace Eventfully.Filters 9 | { 10 | public class MessageExtractionStep : IPipelineStep 11 | { 12 | private static readonly Dictionary _messageExtractorCache = new Dictionary(); 13 | private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings() 14 | { 15 | ContractResolver = new PrivateSetterResolver() 16 | }; 17 | public IntegrationMessageFilterContext Process(TransportMessageFilterContext context) 18 | { 19 | string messageType = context.TransportMessage.MetaData.MessageType; 20 | if (String.IsNullOrEmpty(messageType)) 21 | throw new InvalidMessageTypeException(messageType); 22 | 23 | var metaData = context.TransportMessage.MetaData; 24 | var messageProps = MessagingMap.GetProps(messageType) 25 | ?? throw new UnknownMessageTypeException(messageType); 26 | 27 | IIntegrationMessage message = _extractMessage(context.TransportMessage.Data, messageProps); 28 | 29 | return new IntegrationMessageFilterContext(message, context.TransportMessage.MetaData, context.Endpoint, FilterDirection.Inbound, messageProps); 30 | } 31 | 32 | 33 | /// 34 | /// Convert message off the wire from byte[] into a concrete implementation of IIntegrationMessage 35 | /// delegates to custom extractors and defaults to Json deserialization 36 | /// 37 | private IIntegrationMessage _extractMessage(byte[] messageData, MessageTypeProperties props) 38 | { 39 | IIntegrationMessage message; 40 | if (props.HasCustomExtractor) 41 | { 42 | IMessageExtractor extractor = null; 43 | if (_messageExtractorCache.ContainsKey(props.MessageTypeIdentifier)) 44 | extractor = _messageExtractorCache[props.MessageTypeIdentifier]; 45 | else 46 | { 47 | extractor = (IMessageExtractor)Activator.CreateInstance(props.Type, true); 48 | _messageExtractorCache.Add(props.MessageTypeIdentifier, extractor); 49 | } 50 | message = extractor.Extract(messageData); 51 | } 52 | else 53 | { 54 | //by default the message is json and will be deserialized to the type with a matching MessageTypeIdentifier property:"MessageType" 55 | var json = Encoding.UTF8.GetString(messageData); 56 | message = (IIntegrationMessage)JsonConvert.DeserializeObject(json, props.Type, _serializerSettings); 57 | } 58 | return message; 59 | } 60 | } 61 | public class PrivateSetterResolver : DefaultContractResolver 62 | { 63 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 64 | { 65 | var prop = base.CreateProperty(member, memberSerialization); 66 | if (!prop.Writable) 67 | { 68 | var property = member as PropertyInfo; 69 | prop.Writable = property?.GetSetMethod(true) != null; 70 | } 71 | return prop; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/MessageSerializationStep.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Newtonsoft.Json; 5 | 6 | namespace Eventfully.Filters 7 | { 8 | public class MessageSerializationStep : IPipelineStep 9 | { 10 | private static readonly Dictionary _messageExtractorCache = new Dictionary(); 11 | 12 | public TransportMessageFilterContext Process(IntegrationMessageFilterContext context) 13 | { 14 | byte[] messageBytes = null; 15 | if (context.Message is IsSerialized) 16 | messageBytes = (context.Message as IsSerialized).Message; 17 | else 18 | { 19 | string messageBody = JsonConvert.SerializeObject(context.Message); 20 | messageBytes = Encoding.UTF8.GetBytes(messageBody); 21 | } 22 | 23 | return new TransportMessageFilterContext( 24 | new TransportMessage(context.Message.MessageType, messageBytes, context.MessageMetaData), 25 | context.Endpoint, 26 | FilterDirection.Outbound 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/MessageTypeIntegrationFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Filters 6 | { 7 | /// 8 | /// Filter wrapper for IntegrationFilters that should only be applied to 9 | /// Messages of a specific type 10 | /// 11 | /// 12 | public class MessageTypeIntegrationFilter : IIntegrationMessageFilter 13 | where T : IIntegrationMessage 14 | { 15 | private readonly IIntegrationMessageFilter _innerFilter; 16 | public FilterDirection SupportsDirection => _innerFilter.SupportsDirection; 17 | 18 | public MessageTypeIntegrationFilter(IIntegrationMessageFilter innerFilter) 19 | { 20 | _innerFilter = innerFilter; 21 | } 22 | 23 | 24 | public IntegrationMessageFilterContext Process(IntegrationMessageFilterContext context) 25 | { 26 | if (context.Message is T) 27 | return _innerFilter.Process(context); 28 | return context; 29 | } 30 | 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/MessageTypeTransportFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Filters 6 | { 7 | /// 8 | /// Filter wrapper for TransportFilters that should only be applied to 9 | /// Messages of a specific type 10 | /// 11 | /// 12 | public class MessageTypeTransportFilter : ITransportMessageFilter 13 | { 14 | private readonly string _messageTypeIdentifier; 15 | private readonly ITransportMessageFilter _innerFilter; 16 | public FilterDirection SupportsDirection => _innerFilter.SupportsDirection; 17 | 18 | public MessageTypeTransportFilter(string messageTypeIdentifier, ITransportMessageFilter innerFilter) 19 | { 20 | _messageTypeIdentifier = messageTypeIdentifier; 21 | _innerFilter = innerFilter; 22 | } 23 | 24 | public TransportMessageFilterContext Process(TransportMessageFilterContext context) 25 | { 26 | if (context.TransportMessage.MessageTypeIdentifier.Equals(_messageTypeIdentifier, StringComparison.OrdinalIgnoreCase)) 27 | return _innerFilter.Process(context); 28 | return context; 29 | } 30 | 31 | } 32 | 33 | public class MessageTypeTransportFilter : ITransportMessageFilter 34 | { 35 | private readonly ITransportMessageFilter _innerFilter; 36 | public FilterDirection SupportsDirection => _innerFilter.SupportsDirection; 37 | 38 | public MessageTypeTransportFilter(ITransportMessageFilter innerFilter) 39 | { 40 | _innerFilter = innerFilter; 41 | } 42 | 43 | 44 | public TransportMessageFilterContext Process(TransportMessageFilterContext context) 45 | { 46 | if (context.TransportMessage.MessageTypeIdentifier.Equals( 47 | MessagingMap.GetMessageTypeIdentifier(typeof(T)), 48 | StringComparison.OrdinalIgnoreCase)) 49 | return _innerFilter.Process(context); 50 | 51 | return context; 52 | } 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/OutboundMessagePipeline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using Newtonsoft.Json; 6 | 7 | namespace Eventfully.Filters 8 | { 9 | public class OutboundMessagePipeline : Pipeline 10 | { 11 | public OutboundMessagePipeline(IEnumerable userIntegrationFilters, IEnumerable userTransportFilters) 12 | { 13 | Pipeline userPrePipeline = null; 14 | if (userIntegrationFilters != null && userIntegrationFilters.Count() > 0) 15 | userPrePipeline = new Pipeline(userIntegrationFilters); 16 | 17 | Pipeline userPostPipeline = null; 18 | if (userTransportFilters != null && userTransportFilters.Count() > 0) 19 | userPostPipeline = new Pipeline(userTransportFilters); 20 | 21 | 22 | this.Steps = input => 23 | { 24 | return input 25 | .Step(userPrePipeline) 26 | .Step(new MessageSerializationStep()) 27 | .Step(userPostPipeline) 28 | ; 29 | }; 30 | } 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Filters/Pipeline.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Filters 6 | { 7 | /// 8 | /// Inspired by Jeremy Davis 9 | /// https://jermdavis.wordpress.com/2016/10/03/an-alternative-approach-to-pipelines/ 10 | /// 11 | /// 12 | /// 13 | public interface IPipeline : IPipelineStep 14 | { 15 | new OUTPUT Process(INPUT input); 16 | } 17 | 18 | public interface IPipeline : IPipeline, IPipelineStep 19 | { 20 | new INPUTOUTPUT Process(INPUTOUTPUT input); 21 | } 22 | 23 | public interface IPipelineStep 24 | { 25 | OUTPUT Process(INPUT input); 26 | } 27 | 28 | public interface IPipelineStep : IPipelineStep 29 | { 30 | new INPUTOUTPUT Process(INPUTOUTPUT input); 31 | } 32 | 33 | public class Pipeline : IPipeline 34 | { 35 | protected Func Steps { get; set; } 36 | 37 | public Pipeline() { } 38 | 39 | public OUTPUT Process(INPUT input) 40 | { 41 | return Steps.Invoke(input); 42 | } 43 | } 44 | 45 | public class Pipeline : Pipeline, IPipeline 46 | { 47 | public Pipeline() { } 48 | public Pipeline(IEnumerable> steps) 49 | { 50 | this.Steps = io => 51 | { 52 | foreach (var step in steps) 53 | io = io.Step(step); 54 | return io; 55 | }; 56 | } 57 | } 58 | 59 | public static class PipelineExtensions 60 | { 61 | public static OUTPUT Step(this INPUT input, IPipelineStep step) 62 | { 63 | return step.Process(input); 64 | } 65 | public static INPUTOUTPUT Step(this INPUTOUTPUT input, IPipelineStep step) 66 | { 67 | if (step == null) 68 | return input; 69 | return step.Process(input); 70 | } 71 | } 72 | 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Handling/ICustomMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully 7 | { 8 | /// 9 | /// Allows registering as a handler for a message type while 10 | /// processing each message type through a single Handle method 11 | /// used by the ProcessManagerStateMachine 12 | /// 13 | /// 14 | public interface ICustomMessageHandler 15 | { 16 | Task Handle(IIntegrationMessage message, MessageContext context); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Handling/IMessageDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully.Handlers 7 | { 8 | public interface IMessageDispatcher 9 | { 10 | Task Dispatch(IIntegrationMessage message, MessageContext context); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Handling/IMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Eventfully 6 | { 7 | public interface IMessageHandler 8 | { 9 | Task Handle(T message, MessageContext context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Handling/MessageContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Eventfully.Outboxing; 6 | 7 | namespace Eventfully 8 | { 9 | public class MessageContext 10 | { 11 | public MessageMetaData MetaData { get; protected set; } 12 | 13 | internal IEndpoint Endpoint { get; set; } 14 | 15 | internal MessagingService MessagingService { get; set; } 16 | 17 | internal MessageTypeProperties Props { get; set; } 18 | 19 | internal IOutboxSession OutboxSession { get; set; } 20 | 21 | public MessageContext(MessageMetaData meta, IEndpoint endpoint, MessagingService messagingService, MessageTypeProperties props = null) 22 | { 23 | MetaData = meta; 24 | Endpoint = endpoint; 25 | MessagingService = messagingService; 26 | Props = props; 27 | } 28 | 29 | public Task Reply(IIntegrationReply reply, MessageMetaData replyMetaData = null) 30 | { 31 | var replyToEndpoint = this.GetEndpointForReply(); 32 | return MessagingService.Reply(reply, replyToEndpoint, this.OutboxSession, replyMetaData, this.MetaData); 33 | } 34 | 35 | internal IEndpoint GetEndpointForReply() 36 | { 37 | return Endpoint.Transport.FindEndpointForReply(this); 38 | } 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Handling/MessageDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Eventfully.Outboxing; 6 | 7 | namespace Eventfully.Handlers 8 | { 9 | /// 10 | /// Inspired by Jimmy Bogard's blog and MediatR project 11 | /// https://github.com/jbogard/MediatR/blob/master/LICENSE 12 | /// 13 | public class MessageDispatcher : IMessageDispatcher 14 | { 15 | private readonly IServiceFactory _handlerFactory; 16 | 17 | public MessageDispatcher(IServiceFactory handlerFactory) 18 | { 19 | if (handlerFactory == null) throw new ArgumentNullException(nameof(handlerFactory)); 20 | _handlerFactory = handlerFactory; 21 | } 22 | 23 | public async Task Dispatch(IIntegrationMessage message, MessageContext context) 24 | { 25 | var handler = GetHandler(message); 26 | await handler.Handle(message, context, _handlerFactory); 27 | } 28 | 29 | private static IntegrationMessageDispatcherHandler GetHandler(IIntegrationMessage message) 30 | { 31 | var genericDispatcherType = typeof(IntegrationMessageDispatcherHandler<>) 32 | .MakeGenericType(message.GetType()); 33 | 34 | return (IntegrationMessageDispatcherHandler) 35 | Activator.CreateInstance(genericDispatcherType); 36 | } 37 | 38 | private abstract class IntegrationMessageDispatcherHandler 39 | { 40 | public abstract Task Handle(IIntegrationMessage message, MessageContext context, IServiceFactory handlerFactory); 41 | } 42 | 43 | 44 | private class IntegrationMessageDispatcherHandler : IntegrationMessageDispatcherHandler 45 | where T : IIntegrationMessage 46 | { 47 | public override Task Handle(IIntegrationMessage message, MessageContext context, IServiceFactory handlerFactory) 48 | { 49 | return HandleCore((T)message, context, handlerFactory); 50 | } 51 | 52 | private static async Task HandleCore(T message, MessageContext context, IServiceFactory handlerFactory) 53 | { 54 | using (var scope = handlerFactory.CreateScope())//for dependency injection 55 | { 56 | context.OutboxSession = scope.GetInstance(); 57 | 58 | //is there a processManager/Saga for this message type 59 | if (context.Props != null && context.Props.HasSagaHandler) 60 | { 61 | var sagaProps = MessagingMap.GetSagaProps(context.Props.SagaType) ?? 62 | throw new ApplicationException($"Couldn't find saga for Handler: {context.Props.Type}, SagaType: {context.Props.SagaType}"); 63 | 64 | var saga = (ISaga)scope.GetInstance(sagaProps.SagaType); 65 | 66 | var persistence = scope.GetInstance(sagaProps.SagaPersistenceType) as ISagaPersistence; 67 | await persistence.LoadOrCreateState(saga, saga.FindKey(message, context.MetaData)); 68 | 69 | if (saga is IMessageHandler)//includes ITriggeredBy 70 | await ((IMessageHandler)saga).Handle(message, context); 71 | else if(saga is ICustomMessageHandler) 72 | await ((ICustomMessageHandler)saga).Handle(message, context); 73 | 74 | await persistence.AddOrUpdateState(saga); 75 | } 76 | else // basic handler class 77 | { 78 | var handler = scope.GetInstance>(); 79 | await handler.Handle(message, context); 80 | } 81 | } 82 | } 83 | 84 | 85 | } 86 | 87 | 88 | 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IIntegrationMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | 6 | namespace Eventfully 7 | { 8 | public interface IIntegrationMessage 9 | { 10 | string MessageType { get; } 11 | 12 | } 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IMessageExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public interface IMessageExtractor 8 | { 9 | IIntegrationMessage Extract(byte[] data); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IntegrationCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public interface IIntegrationCommand : IIntegrationMessage 9 | { 10 | } 11 | 12 | public abstract class IntegrationCommand : IntegrationMessage, IIntegrationCommand 13 | { 14 | public IntegrationCommand() 15 | { 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public interface IIntegrationEvent : IIntegrationMessage 8 | { 9 | } 10 | 11 | public abstract class IntegrationEvent : IntegrationMessage, IIntegrationEvent 12 | { 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IntegrationMessage.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace Eventfully 8 | { 9 | public abstract class IntegrationMessage : IIntegrationMessage 10 | { 11 | public abstract string MessageType { get; } 12 | 13 | public DateTime? CreatedAtUtc { get; set; } = DateTime.UtcNow; 14 | 15 | public IntegrationMessage() 16 | { 17 | } 18 | 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/IntegrationReply.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public interface IIntegrationReply : IIntegrationMessage 9 | { 10 | } 11 | 12 | public abstract class IntegrationReply : IntegrationMessage, IIntegrationReply 13 | { 14 | public IntegrationReply() {} 15 | 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Messages/WrappedMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public interface IsSerialized 9 | { 10 | byte[] Message { get; set; } 11 | } 12 | 13 | /// 14 | /// A pre-serialized message whose MessageTypeIdentifier is specified explicitly 15 | /// This is useful for sending pre-serialized messages through the Outbound message 16 | /// pipeline 17 | /// 18 | public class WrappedMessage : IIntegrationMessage, IsSerialized 19 | { 20 | public string MessageType { get; set; } 21 | public byte[] Message { get; set; } 22 | 23 | public WrappedMessage() { } 24 | public WrappedMessage(string messageTypeIdenfifier, byte[] message) 25 | { 26 | this.MessageType = messageTypeIdenfifier; 27 | this.Message = message; 28 | } 29 | } 30 | 31 | public class WrappedCommand : WrappedMessage, IIntegrationCommand { 32 | public WrappedCommand(string messageTypeIdenfifier, byte[] message) : base(messageTypeIdenfifier, message) { } 33 | } 34 | public class WrappedEvent : WrappedMessage, IIntegrationEvent { 35 | public WrappedEvent(string messageTypeIdenfifier, byte[] message) : base(messageTypeIdenfifier, message) { } 36 | } 37 | public class WrappedReply : WrappedMessage, IIntegrationReply { 38 | public WrappedReply(string messageTypeIdenfifier, byte[] message) : base(messageTypeIdenfifier, message) { } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Outboxing/IOutbox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | 7 | namespace Eventfully.Outboxing 8 | { 9 | 10 | public interface IOutbox 11 | { 12 | /// 13 | /// Find messages from the outbox that are ready and relay the to their true endpoint 14 | /// 15 | /// method provided to relay the messages 16 | /// 17 | Task Relay(Dispatcher dispatcher);// Func relayCallback); 18 | 19 | /// 20 | /// Do any periodic cleanup necessary on the inbox 21 | /// ex. deleting old processed messages 22 | /// 23 | /// the age of old processed messages that can be deleted 24 | /// 25 | Task CleanUp(TimeSpan cleanupAge); 26 | 27 | /// 28 | /// Do any periodic resets necessary on the inbox 29 | /// ex. resetting messages that began processing but never finished 30 | /// 31 | /// the age of old pending messages that can be reset 32 | /// 33 | Task Reset(TimeSpan resetAge); 34 | 35 | Task StartAsync(Dispatcher dispatcher); 36 | 37 | Task StopAsync(); 38 | } 39 | 40 | //public interface IOutboxWithTransient : IOutbox 41 | //{ 42 | // Task Relay(Func relayCallback); 43 | 44 | //} 45 | 46 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Outboxing/IOutboxManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Eventfully.Outboxing 6 | { 7 | public interface IOutboxManager : IDisposable 8 | { 9 | Task StartAsync(CancellationToken stoppingToken); 10 | 11 | Task StopAsync(); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Outboxing/IOutboxSession.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | 7 | namespace Eventfully.Outboxing 8 | { 9 | 10 | public interface IOutboxSession 11 | { 12 | /// 13 | /// Send a message to the outbox 14 | /// 15 | Task Dispatch(string messageTypeIdentifier, byte[] message, MessageMetaData meta, IEndpoint endpoint = null, OutboxDispatchOptions options = null); 16 | 17 | ///// 18 | ///// Find messages from the outbox that are ready and relay the to their true endpoint 19 | ///// 20 | ///// method provided to relay the messages 21 | ///// 22 | //Task Relay(Func relayMessage); 23 | 24 | ///// 25 | ///// Do any periodic cleanup necessary on the inbox 26 | ///// ex. deleting old processed messages 27 | ///// 28 | ///// the age of old processed messages that can be deleted 29 | ///// 30 | //Task CleanUp(TimeSpan cleanupAge); 31 | 32 | ///// 33 | ///// Do any periodic resets necessary on the inbox 34 | ///// ex. resetting messages that began processing but never finished 35 | ///// 36 | ///// the age of old pending messages that can be reset 37 | ///// 38 | //Task Reset(TimeSpan resetAge); 39 | 40 | //Task DispatchTransient(Func dispatchMessage); 41 | 42 | 43 | } 44 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Outboxing/OutboxDispatchOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Outboxing 6 | { 7 | public class OutboxDispatchOptions 8 | { 9 | public bool SkipTransientDispatch { get; set; } = false; 10 | 11 | public TimeSpan? Delay { get; set; } 12 | 13 | public DateTime? ExpiresAtUtc { get; set; } 14 | 15 | public OutboxDispatchOptions() 16 | { 17 | } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Outboxing/OutboxDispatchResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Outboxing 6 | { 7 | public class OutboxRelayResult 8 | { 9 | public int MessageCount { get; set; } 10 | public int MaxMessageCount { get; set; } 11 | 12 | public OutboxRelayResult(int messageCount, int maxMessageCount) 13 | { 14 | this.MessageCount = messageCount; 15 | this.MaxMessageCount = maxMessageCount; 16 | } 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Eventfully.Core/README.md: -------------------------------------------------------------------------------- 1 |  2 | - reliable messaging framework 3 | - light weight 4 | - no shared message types between apps/services 5 | - easy customization of message deserialization 6 | - message outbox support 7 | - simple sagas 8 | - encryption support (AES) 9 | - azure key vault support 10 | - EFCore Support 11 | - Microsoft Dependency Injection Support 12 | - Azure Service Bus Support 13 | - fluent configurtion 14 | 15 | Extensible 16 | - message pipeline with with easy filters on raw data and messages 17 | - extensible transports, outbox, encryption, dependency injection 18 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Saga/ISaga.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully 7 | { 8 | public interface ISaga 9 | { 10 | object State { get;} 11 | void SetState(object state); 12 | object FindKey(IIntegrationMessage message, MessageMetaData meta); 13 | } 14 | 15 | public interface ISaga : ISaga 16 | { 17 | new S State { get; } 18 | new K FindKey(IIntegrationMessage message, MessageMetaData meta); 19 | void SetState(S state); 20 | } 21 | 22 | public class Saga : ISaga 23 | { 24 | protected Dictionary> _keyMappers = new Dictionary>(); 25 | 26 | 27 | public Saga() 28 | { 29 | } 30 | 31 | protected void MapIdFor(Func mapper) where M : IIntegrationMessage 32 | { 33 | Func untyped = (m,md) => mapper((M)m, md); 34 | _keyMappers.Add(typeof(M), untyped); 35 | } 36 | 37 | public K FindKey(IIntegrationMessage message, MessageMetaData meta) 38 | { 39 | return _keyMappers[message.GetType()].Invoke(message, meta); 40 | } 41 | 42 | object ISaga.FindKey(IIntegrationMessage message, MessageMetaData meta) => FindKey(message, meta); 43 | 44 | public S State { get; protected set; } 45 | object ISaga.State { get => State; } 46 | void ISaga.SetState(object state) { 47 | this.SetState((S)state); 48 | } 49 | 50 | public virtual void SetState(S state) 51 | { 52 | if (state == null) 53 | throw new InvalidOperationException("State can not be null"); 54 | this.State = state; 55 | } 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Saga/ISagaPersistence.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | 6 | namespace Eventfully 7 | { 8 | 9 | public interface ISagaPersistence 10 | { 11 | Task LoadOrCreateState(ISaga saga, object sagaId); 12 | Task AddOrUpdateState(ISaga saga); 13 | } 14 | 15 | public interface ISagaPersistence : ISagaPersistence 16 | { 17 | Task LoadOrCreateState(ISaga saga, K sagaId); 18 | Task AddOrUpdateState(ISaga saga); 19 | } 20 | 21 | public abstract class SagaPersistence : ISagaPersistence 22 | { 23 | public abstract Task AddOrUpdateState(ISaga saga); 24 | 25 | public abstract Task LoadOrCreateState(ISaga saga, K sagaId); 26 | 27 | Task ISagaPersistence.LoadOrCreateState(ISaga saga, object sagaId) => LoadOrCreateState((ISaga) saga, (K) sagaId); 28 | 29 | Task ISagaPersistence.AddOrUpdateState(ISaga saga) => AddOrUpdateState((ISaga)saga); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Saga/ITriggeredBy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace Eventfully 6 | { 7 | /// 8 | /// Alias for IMessageHandler used to set a message that starts a saga/process manager 9 | /// 10 | /// 11 | public interface ITriggeredBy : IMessageHandler { } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/ITransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Eventfully.Transports 8 | { 9 | public interface ITransport 10 | { 11 | bool SupportsDelayedDispatch { get; } 12 | 13 | //Task StartAsync(IEndpoint endpoint, CancellationToken cancellationToken); 14 | Task StartAsync(IEndpoint endpoint, Handler handler, CancellationToken cancellationToken); 15 | 16 | Task StopAsync(); 17 | 18 | Task Dispatch(string messageTypeIdenfifier, byte[] message, IEndpoint endpoint, MessageMetaData metaData = null); 19 | 20 | void SetReplyToForCommand(IEndpoint endpoint, IIntegrationCommand command, MessageMetaData meta); 21 | 22 | IEndpoint FindEndpointForReply(MessageContext commandContext); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/ITransportFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using Eventfully; 5 | 6 | namespace Eventfully.Transports 7 | { 8 | public interface ITransportFactory 9 | { 10 | ITransport Create(TransportSettings settings); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/Transport.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using System.Text; 4 | //using System.Threading; 5 | //using System.Threading.Tasks; 6 | 7 | //namespace Eventfully.Transports 8 | //{ 9 | // public abstract class Transport : ITransport 10 | // { 11 | 12 | // public abstract bool SupportsDelayedDispatch { get; } 13 | 14 | // /// 15 | // /// Start sending/receiving for the endpoint 16 | // /// 17 | // /// 18 | // /// 19 | // /// 20 | // public abstract Task StartAsync(IEndpoint endpoint, CancellationToken cancellationToken); 21 | 22 | // /// 23 | // /// Dispatch a pre-serialized message to the supplied endpoint 24 | // /// 25 | // /// 26 | // /// 27 | // /// 28 | // /// 29 | // /// 30 | // public abstract Task Dispatch(string messageTypeIdentifier, byte[] message, IEndpoint endpoint, MessageMetaData metaData = null); 31 | 32 | // /// 33 | // /// Use the ReplyTo information in the meata data 34 | // /// to provide an endpoint 35 | // /// 36 | // /// 37 | // /// 38 | // public abstract IEndpoint FindEndpointForReply(MessageContext commandContext); 39 | 40 | // /// 41 | // /// Populate the ReplyTo meta data for a command using 42 | // /// the transports addressing/routing scheme 43 | // /// 44 | // /// 45 | // /// 46 | // /// 47 | // public abstract void SetReplyToForCommand(IEndpoint endpoint, IIntegrationCommand command, MessageMetaData meta); 48 | 49 | // public abstract Task StopAsync() 50 | 51 | // } 52 | //} 53 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/TransportConfigurationExtenstions.cs: -------------------------------------------------------------------------------- 1 | using Eventfully.Transports; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Eventfully 7 | { 8 | public static class TransportConfigurationExtensions 9 | { 10 | public static LocalTransportSettings UseLocalTransport(this IEndpointFluent endpointSettings) 11 | { 12 | var settings = new LocalTransportSettings(); 13 | endpointSettings.TransportSettings = settings; 14 | return settings; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/TransportMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | /// 9 | /// The Message meta data and message content byte[] 10 | /// standardized to send/receive from a transport 11 | /// 12 | public class TransportMessage 13 | { 14 | public string MessageTypeIdentifier { get; set; } 15 | public byte[] Data { get; set; } 16 | public MessageMetaData MetaData { get; set; } 17 | 18 | public TransportMessage(string messageTypeIdentifier, byte[] data, MessageMetaData meta) 19 | { 20 | this.MessageTypeIdentifier = messageTypeIdentifier; 21 | this.Data = data; 22 | this.MetaData = meta; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Transports/TransportSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Transports 6 | { 7 | public interface ITransportSettings 8 | { 9 | ITransportFactory Factory { get; } 10 | 11 | ITransport Create(); 12 | } 13 | 14 | 15 | public abstract class TransportSettings : ITransportSettings 16 | { 17 | public abstract ITransportFactory Factory { get; } 18 | 19 | public ITransport Create() 20 | { 21 | return Factory.Create(this); 22 | } 23 | 24 | 25 | } 26 | 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/ConstantRetryStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public class ConstantRetryStrategy : IRetryIntervalStrategy 8 | { 9 | private readonly double _intervalInSeconds; 10 | public ConstantRetryStrategy(double intervalInSeconds) 11 | { 12 | if (intervalInSeconds <= 0) 13 | throw new InvalidOperationException("interval must be greater than 0"); 14 | _intervalInSeconds = intervalInSeconds; 15 | } 16 | public DateTime GetNextDateUtc(int tryCount, DateTime? nowUtc = null) 17 | { 18 | var counter = tryCount > 0 ? tryCount : 1; 19 | nowUtc = nowUtc.HasValue ? nowUtc : DateTime.UtcNow; 20 | 21 | return nowUtc.Value.AddSeconds(_intervalInSeconds); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/DefaultExponentialRetryStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | public class DefaultExponentialRetryStrategy : IRetryIntervalStrategy 8 | { 9 | public DateTime GetNextDateUtc(int tryCount, DateTime? nowUtc = null) 10 | { 11 | var counter = tryCount > 0 ? tryCount : 1; 12 | nowUtc = nowUtc.HasValue ? nowUtc : DateTime.UtcNow; 13 | 14 | //2^3,2^4, 2^5... (8,16,32,64,128 (2min), 256 (4min), 512 (8min), 1024 (17min), 2048 (34min), 4096 (1hr) , 8192 ( 2.2hr) ....) 15 | return nowUtc.Value.AddSeconds(Math.Pow(2, counter + 2)); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/Exceptions/EndpointNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public class EndpointNotFoundException : NonTransientException 9 | { 10 | public EndpointNotFoundException(string messageType) : base($"Endpoint not found for Message Type. MessageType: {messageType}") 11 | { 12 | 13 | } 14 | public EndpointNotFoundException(string endpointName, string additionalMessage) : base($"Endpoint not found for Label :{endpointName}. {additionalMessage}") 15 | { 16 | 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/Exceptions/InvalidMessageTypeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public class InvalidMessageTypeException : NonTransientException 9 | { 10 | public InvalidMessageTypeException(string messageType):base($"Invalid Message Type. MessageType: {messageType}") 11 | { 12 | 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/Exceptions/MessageExpiredException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public class MessageExpiredException : NonTransientException 9 | { 10 | public MessageExpiredException(string messageType):base($"Message Expired. MessageType: {messageType}") 11 | { 12 | 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/Exceptions/NonTransientException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | [Serializable] 9 | public class NonTransientException : ApplicationException 10 | { 11 | public NonTransientException() 12 | { 13 | 14 | } 15 | 16 | public NonTransientException(string message, Exception exc = null) 17 | : base(message, exc) 18 | { 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/Exceptions/UnknownMessageTypeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully 6 | { 7 | 8 | public class UnknownMessageTypeException : NonTransientException 9 | { 10 | public UnknownMessageTypeException(string messageType):base($"Unknown Message Type. MessageType: {messageType}") 11 | { 12 | 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/ICountingSemaphore.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Eventfully 4 | { 5 | public interface ICountingSemaphore 6 | { 7 | int MaxConcurrentOwners { get; } 8 | string Name { get; } 9 | int TimeoutInSeconds { get; } 10 | 11 | Task TryAcquire(string ownerId); 12 | Task TryRelease(string ownerId); 13 | Task TryRenew(string ownerId); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/IRetryIntervalStrategy.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Eventfully 4 | { 5 | public interface IRetryIntervalStrategy 6 | { 7 | DateTime GetNextDateUtc(int tryCount, DateTime? nowUtc = null); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Eventfully.Core/Util/MessagingLogging.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Eventfully 7 | { 8 | public class Logging 9 | { 10 | public static ILoggerFactory LoggerFactory = null; 11 | 12 | public static ILogger CreateLogger() => LoggerFactory.CreateLogger(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Eventfully.EFCoreOutbox/Eventfully.EFCoreOutbox.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Eventfully.EFCoreOutbox/README.txt: -------------------------------------------------------------------------------- 1 |  - Add property to your DbContext 2 | 3 | public DbSet OutboxEvents { get; set; } 4 | 5 | 6 | - Add to your DbContext OnModelCreating 7 | 8 | builder.ApplyConfiguration(new Common.Infrastructure.Messaging.EFCore.OutboxEventEntityConfiguration()); 9 | builder.ApplyConfiguration(new Common.Infrastructure.Messaging.EFCore.OutboxEventDataEntityConfiguration()); 10 | 11 | 12 | - If using migrations you can 13 | 14 | Add-Migration Outbox 15 | 16 | 17 | - Configure Depentcy Injection 18 | 19 | 20 | //Needed for reading/updating/deleting the outbox 21 | services.AddTransient>(x => 22 | new Common.Infrastructure.SqlHelper.SqlConnectionFactory(_config.GetConnectionString("ApplicationConnection")) 23 | ); 24 | 25 | //needed for publishing the events from the outbox 26 | services.AddScoped(); 27 | 28 | //needed for injecting the outbox into the DbContext and the OutboxEventPumpService 29 | services.AddScoped(); 30 | 31 | //needed for injecting into application layer handlers 32 | services.AddScoped(x => 33 | x.GetRequiredService() 34 | ); 35 | 36 | //needed to start the event pump (can be done anywhere, doesn't have to be in a webapp) 37 | services.AddHostedService(); 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Eventfully.EFCoreOutbox/RegistrationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Eventfully.Outboxing; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Text; 8 | using Eventfully.EFCoreOutbox; 9 | 10 | namespace Eventfully 11 | { 12 | public static class RegistrationExtensions 13 | { 14 | public static void WithEFCoreOutbox(this IServiceRegistrar services, 15 | Action config 16 | ) where T : DbContext 17 | { 18 | OutboxSettings settings = new OutboxSettings(); 19 | if (config != null) 20 | config.Invoke(settings); 21 | services.AddSingleton(settings); 22 | services.AddSingleton>(true); 23 | services.AddTransient>(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Eventfully.EFCoreOutbox/SqlConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using System.Data.SqlClient; 4 | 5 | namespace Eventfully.EFCoreOutbox 6 | { 7 | public class SqlConnectionFactory 8 | { 9 | private readonly string _connectionString; 10 | public SqlConnectionFactory() 11 | { 12 | } 13 | 14 | public SqlConnectionFactory(string connectionString) 15 | { 16 | _connectionString = connectionString; 17 | } 18 | 19 | public SqlConnection Get() 20 | { 21 | return new SqlConnection(_connectionString); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Eventfully.MicrosoftDependencyInjection/Eventfully.Extensions.Microsoft.DependencyInjection.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Eventfully.MicrosoftDependencyInjection/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using Eventfully.Handlers; 9 | using Eventfully.Outboxing; 10 | 11 | namespace Eventfully 12 | { 13 | 14 | 15 | public static class DependencyInjectionExtensions 16 | { 17 | public static IServiceRegistrar AddMessaging(this IServiceCollection services, 18 | Profile profile, 19 | params Assembly[] messageAndHandlerAssemblies) 20 | { 21 | return AddMessaging(services, profile, null, null, messageAndHandlerAssemblies); 22 | } 23 | 24 | 25 | public static IServiceRegistrar AddMessaging(this IServiceCollection services, 26 | Profile profile, 27 | EndpointBindings bindings, 28 | params Assembly[] messageAndHandlerAssemblies) 29 | { 30 | return AddMessaging(services, profile, bindings, null, messageAndHandlerAssemblies); 31 | } 32 | 33 | public static IServiceRegistrar AddMessaging(this IServiceCollection services, 34 | Profile profile, 35 | EndpointBindings bindings, 36 | Action config, 37 | params Assembly[] messageAndHandlerAssemblies) 38 | { 39 | MessagingService.InitializeTypes(messageAndHandlerAssemblies); 40 | if(bindings != null) 41 | profile.AddBindings(bindings); 42 | var messagingConfig = new MessagingConfiguration(); 43 | if (config != null) 44 | config.Invoke(messagingConfig); 45 | 46 | services.AddSingleton(messagingConfig); 47 | services.AddTransient(x=> new MicrosoftDependencyInjectionServiceFactory(x)); 48 | services.AddSingleton(profile); 49 | services.AddSingleton(); 50 | services.AddTransient(); 51 | 52 | //setup user message handlers 53 | services.Scan(c => 54 | { 55 | c.FromAssemblies(messageAndHandlerAssemblies) 56 | .AddClasses(t => t.AssignableTo(typeof(IMessageHandler<>))) 57 | .AsImplementedInterfaces() 58 | .WithTransientLifetime() 59 | .AddClasses(t => t.AssignableTo(typeof(ICustomMessageHandler<>))) 60 | .AsImplementedInterfaces() 61 | .WithTransientLifetime() 62 | .AddClasses(t => t.AssignableTo(typeof(ISaga<,>))) 63 | .AsImplementedInterfaces() 64 | .AsSelf() 65 | .WithTransientLifetime() 66 | .AddClasses(t => t.AssignableTo(typeof(ISagaPersistence<,>))) 67 | .AsImplementedInterfaces() 68 | .WithTransientLifetime() 69 | ; 70 | }); 71 | 72 | return new ServiceRegistrar(services); 73 | } 74 | 75 | //public static void UseMessagingHost(this IApplicationBuilder builder) 76 | //{ 77 | // UseMessagingHost(builder.ApplicationServices); 78 | //} 79 | 80 | /// 81 | /// Helper for applications other than asp.net core where you might not have access to an IApplicationBuilder 82 | /// 83 | /// 84 | public static void UseMessagingHost(this IServiceProvider provider) 85 | { 86 | var messagingService = provider.GetRequiredService(); 87 | Logging.LoggerFactory = provider.GetRequiredService(); 88 | messagingService.StartAsync().ConfigureAwait(false).GetAwaiter(); 89 | } 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /src/Eventfully.MicrosoftDependencyInjection/ServiceFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Eventfully 8 | { 9 | public class MicrosoftDependencyInjectionServiceFactory : IServiceFactory 10 | { 11 | private readonly IServiceProvider _provider; 12 | private readonly IServiceScope _scope; 13 | public MicrosoftDependencyInjectionServiceFactory(IServiceProvider provider) => _provider = provider; 14 | 15 | private MicrosoftDependencyInjectionServiceFactory(IServiceScope scope) { 16 | _scope = scope; 17 | _provider = _scope.ServiceProvider; 18 | } 19 | public object GetInstance(Type type) => _provider.GetRequiredService(type); 20 | 21 | public IServiceFactory CreateScope() => new MicrosoftDependencyInjectionServiceFactory(_provider.CreateScope()); 22 | 23 | public void Dispose() 24 | { 25 | if (_scope != null) 26 | _scope.Dispose(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Eventfully.MicrosoftDependencyInjection/ServiceRegistrar.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Eventfully 7 | { 8 | public class ServiceRegistrar : IServiceRegistrar 9 | { 10 | private IServiceCollection _services; 11 | public ServiceRegistrar(IServiceCollection services) => _services = services; 12 | 13 | public void AddSingleton(object o) => _services.AddSingleton(o); 14 | public void AddSingleton(object o) => _services.AddSingleton(typeof(T), o); 15 | public void AddSingleton() => _services.AddSingleton(typeof(T)); 16 | public void AddSingleton(bool asBoth = false) where T : I 17 | { 18 | if(asBoth) 19 | { 20 | _services.AddSingleton(typeof(T)); 21 | _services.AddSingleton(typeof(I), x => 22 | x.GetRequiredService() 23 | ); 24 | } 25 | else 26 | _services.AddSingleton(typeof(I), typeof(T)); 27 | } 28 | public void AddTransient() => _services.AddTransient(typeof(T)); 29 | public void AddTransient() where T: I => _services.AddTransient(typeof(I), typeof(T)); 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Eventfully.Semaphore.SqlServer/CreateSqlSemaphoreTable.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [dbo].[__Semaphores]( 2 | [Name] nvarchar(200) NOT NULL, 3 | [Owners] [nvarchar](max) NULL, 4 | [RowVersion] [rowversion] NOT NULL, 5 | CONSTRAINT [PK___Semaphores] PRIMARY KEY CLUSTERED([Name] ASC) 6 | ) -------------------------------------------------------------------------------- /src/Eventfully.Semaphore.SqlServer/Eventfully.Semaphore.SqlServer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | true 24 | \ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Eventfully.Semaphore.SqlServer/README.txt: -------------------------------------------------------------------------------- 1 |  2 | In order to use the SqlServerSemaphore you must have the __Sempahores table in your database 3 | 4 | CREATE TABLE [dbo].[__Semaphores]( 5 | [Name] nvarchar(200) NOT NULL, 6 | [Owners] [nvarchar](max) NULL, 7 | [RowVersion] [rowversion] NOT NULL, 8 | CONSTRAINT [PK___Semaphores] PRIMARY KEY CLUSTERED([Name] ASC) 9 | ) -------------------------------------------------------------------------------- /src/Eventfully.Semaphore.SqlServer/SqlConnectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Microsoft.Data.SqlClient; 4 | 5 | namespace Eventfully.Semaphore.SqlServer 6 | { 7 | public class SqlConnectionFactory 8 | { 9 | private readonly string _connectionString; 10 | public SqlConnectionFactory() 11 | { 12 | } 13 | 14 | public SqlConnectionFactory(string connectionString) 15 | { 16 | _connectionString = connectionString; 17 | } 18 | 19 | public SqlConnection Get() 20 | { 21 | return new SqlConnection(_connectionString); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/AzureServiceBusConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Transports.AzureServiceBus 6 | { 7 | 8 | 9 | public class WrappedTransportSettings : EndpointSubSettings 10 | { 11 | private AzureServiceBusTransportSettings _transportSettings; 12 | public WrappedTransportSettings(IEndpointFluent endpointSettings, AzureServiceBusTransportSettings transportSettings) : base(endpointSettings) 13 | { 14 | _transportSettings = transportSettings; 15 | } 16 | 17 | public bool SpecialFeature { 18 | get { return _transportSettings.SpecialFeature; } 19 | set { _transportSettings.SpecialFeature = value; } 20 | } 21 | 22 | } 23 | 24 | public static class AzureServiceBusConfigurationExtensions 25 | { 26 | public static WrappedTransportSettings UseAzureServiceBusTransport(this IEndpointFluent endpointSettings) 27 | { 28 | var settings = new AzureServiceBusTransportSettings(); 29 | endpointSettings.TransportSettings = settings; 30 | return new WrappedTransportSettings(endpointSettings, settings); 31 | } 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/AzureServiceBusMetaDataMapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.ServiceBus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | 6 | namespace Eventfully.Transports.AzureServiceBus 7 | { 8 | public class AzureServiceBusMetaDataMapper 9 | { 10 | public void ApplyMetaData(Message message, MessageMetaData meta, string messageTypeIdentifier) 11 | { 12 | if (meta != null) 13 | { 14 | foreach (var item in meta) 15 | { 16 | if (HeaderType.MessageId.Equals(item.Key) && !String.IsNullOrEmpty(item.Value)) 17 | message.MessageId = item.Value; 18 | else if (HeaderType.ContentType.Equals(item.Key)) 19 | message.ContentType = item.Value; 20 | else if (HeaderType.CorrelationId.Equals(item.Key) && !String.IsNullOrEmpty(item.Value)) 21 | message.CorrelationId = item.Value; 22 | else if (HeaderType.ReplyTo.Equals(item.Key)) 23 | message.ReplyTo = item.Value; 24 | else if (HeaderType.SessionId.Equals(item.Key) && !String.IsNullOrEmpty(item.Value)) 25 | message.SessionId = item.Value; 26 | else if (HeaderType.TimeToLive.Equals(item.Key) && meta.TimeToLive.HasValue) 27 | message.TimeToLive = meta.TimeToLive.Value; 28 | else 29 | { 30 | message.UserProperties.Add(item.Key, item.Value); 31 | } 32 | } 33 | } 34 | message.Label = messageTypeIdentifier; 35 | } 36 | 37 | public MessageMetaData ExtractMetaData(Message message) 38 | { 39 | MessageMetaData meta = new MessageMetaData(); 40 | 41 | if (!String.IsNullOrEmpty(message.Label)) 42 | meta.MessageType = message.Label; 43 | 44 | if (!String.IsNullOrEmpty(message.MessageId)) 45 | meta.MessageId = message.MessageId; 46 | 47 | try 48 | { 49 | //message throws exception if not in a state where expiration can be calculated 50 | if (message.ExpiresAtUtc != default(DateTime)) 51 | meta.ExpiresAtUtc = message.ExpiresAtUtc; 52 | } 53 | catch { } 54 | 55 | if (message.UserProperties.ContainsKey("OriginalMessageId")) 56 | { 57 | var messageId = (string)message.UserProperties["OriginalMessageId"]; 58 | meta.MessageId = messageId; 59 | meta.Add("TransientMessageId", message.MessageId); 60 | } 61 | 62 | if (!String.IsNullOrEmpty(message.ContentType)) 63 | meta.ContentType = message.ContentType; 64 | 65 | if (!String.IsNullOrEmpty(message.CorrelationId)) 66 | meta.CorrelationId = message.CorrelationId; 67 | 68 | if (!String.IsNullOrEmpty(message.ReplyTo)) 69 | meta.ReplyTo = message.ReplyTo; 70 | 71 | if (!String.IsNullOrEmpty(message.SessionId)) 72 | meta.SessionId = message.SessionId; 73 | 74 | foreach(var item in message.UserProperties) 75 | { 76 | meta[item.Key] = item.Value != null ? item.Value.ToString() : null; 77 | } 78 | 79 | return meta; 80 | } 81 | 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/AzureServiceBusTransportFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Transports.AzureServiceBus 6 | { 7 | public class AzureServiceBusTransportFactory : ITransportFactory 8 | { 9 | public ITransport Create(TransportSettings settings) 10 | { 11 | AzureServiceBusTransport transport = new AzureServiceBusTransport(settings); 12 | return transport; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/Eventfully.Transports.AzureServiceBus.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/Recoverability/AzureServiceBusCloneRecoverabilityProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.ServiceBus; 2 | using Microsoft.Azure.ServiceBus.Core; 3 | using Newtonsoft.Json; 4 | using Polly; 5 | using Polly.Retry; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace Eventfully.Transports.AzureServiceBus 12 | { 13 | 14 | public class AzureServiceBusCloneRecoverabilityProvider : IAzureServiceBusRecoverabilityProvider 15 | { 16 | private readonly AsyncRetryPolicy _completeImmediateRetryPolicy; 17 | private int _maxCompletionImmediateRetry = 1; 18 | private IRetryIntervalStrategy _retryStrategy; 19 | private int _maxDeliveryCount = 20; 20 | 21 | 22 | public AzureServiceBusCloneRecoverabilityProvider(IRetryIntervalStrategy retryStrategy = null, int maxDeliveryCount = 20) 23 | { 24 | _completeImmediateRetryPolicy = Policy 25 | .Handle() 26 | .RetryAsync(_maxCompletionImmediateRetry); 27 | 28 | _retryStrategy = retryStrategy != null ? retryStrategy : new DefaultExponentialRetryStrategy(); 29 | _maxDeliveryCount = maxDeliveryCount; 30 | 31 | } 32 | 33 | public async Task OnPreHandle(RecoverabilityContext context) 34 | { 35 | var count = _getRecoveryCount(context.Message, context); 36 | if (await _handleMaxRetry(count, context.Message, context)) 37 | context.SkipMessage = true; 38 | return context; 39 | } 40 | 41 | 42 | 43 | public async Task Recover(RecoverabilityContext context) 44 | { 45 | var sender = AzureServiceBusClientCache.GetSender(context.Endpoint.Settings.ConnectionString); 46 | var count = _getRecoveryCount(context.Message, context); 47 | 48 | var retryMessage = context.Message.Clone(); 49 | retryMessage.MessageId = Guid.NewGuid().ToString(); 50 | retryMessage.UserProperties.Add("OriginalMessageId", context.Message.MessageId); 51 | retryMessage.UserProperties.Add("RecoveryCount", ++count); 52 | 53 | await sender.ScheduleMessageAsync( 54 | retryMessage, 55 | this._retryStrategy.GetNextDateUtc(++count) 56 | ); 57 | 58 | //complete the original message - we've already scheduled a clone 59 | await _completeImmediateRetryPolicy.ExecuteAsync(() => 60 | context.Receiver.CompleteAsync(context.Message.SystemProperties.LockToken) 61 | ); 62 | } 63 | 64 | public Task OnPostHandle(RecoverabilityContext context)// int timesQueued, Endpoint endpoint, IMessageReceiver receiver, Message controlMessage = null, string description = null, Exception exc = null) 65 | { 66 | //no cleanup to do here 67 | return Task.CompletedTask; 68 | } 69 | 70 | private async Task _handleMaxRetry(int recoveryCount, Message message, RecoverabilityContext context) 71 | { 72 | if (message.SystemProperties.DeliveryCount > 1 || recoveryCount > _maxDeliveryCount) 73 | { 74 | await context.Receiver.DeadLetterAsync(message.SystemProperties.LockToken); 75 | return true; 76 | } 77 | return false; 78 | } 79 | 80 | private int _getRecoveryCount(Message message, RecoverabilityContext context) 81 | { 82 | var count = message.UserProperties.ContainsKey("RecoveryCount") ? 83 | (int)message.UserProperties["RecoveryCount"] 84 | : 0; 85 | return count; 86 | 87 | } 88 | 89 | 90 | 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/Recoverability/IAzureServiceBusRecoverabilityProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.Azure.ServiceBus; 4 | using Microsoft.Azure.ServiceBus.Core; 5 | 6 | namespace Eventfully.Transports.AzureServiceBus 7 | { 8 | public interface IAzureServiceBusRecoverabilityProvider 9 | { 10 | Task OnPreHandle(RecoverabilityContext context); 11 | Task Recover(RecoverabilityContext context); 12 | Task OnPostHandle(RecoverabilityContext context); 13 | 14 | } 15 | 16 | public class RecoverabilityContext 17 | { 18 | public bool SkipMessage { get; set; } = false; 19 | public Message Message { get; set; } 20 | public IMessageReceiver Receiver { get; set; } 21 | public IEndpoint Endpoint { get; set; } 22 | 23 | public Dictionary TempData = new Dictionary(); 24 | 25 | public Message TempMessage { get; set; } 26 | public RecoverabilityContext(IMessageReceiver receiver, IEndpoint endpoint, Message message) 27 | { 28 | Receiver = receiver; 29 | Endpoint = endpoint; 30 | Message = message; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Eventfully.Transports.AzureServiceBus/Recoverability/RecoverabilityControlMessage.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.ServiceBus; 2 | using Newtonsoft.Json; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace Eventfully.Transports.AzureServiceBus 8 | { 9 | public class RecoverabilityControlMessage 10 | { 11 | public int RecoveryCount { get; set; } = 1; 12 | public Int64 SequenceNumber { get; set; } 13 | 14 | public RecoverabilityControlMessage() 15 | { 16 | 17 | } 18 | 19 | public RecoverabilityControlMessage(Int64 sequenceNumber, int recoveryScheduledCount = 1) 20 | { 21 | recoveryScheduledCount = recoveryScheduledCount < 1 ? 1 : recoveryScheduledCount; 22 | this.SequenceNumber = sequenceNumber; 23 | this.RecoveryCount = recoveryScheduledCount; 24 | } 25 | 26 | 27 | //public static RecoverabilityControlMessage FromServiceBusMessage(Message) 28 | //public static Message Create(Int64 sequenceNumber, int lastRecoveryCount = 0) 29 | //{ 30 | // lastRecoveryCount = lastRecoveryCount < 0 ? 0 : lastRecoveryCount; 31 | // var recoverMessage = new RecoverabilityControlMessage(sequenceNumber, lastRecoveryCount++); 32 | // return new Message( 33 | // Encoding.UTF8.GetBytes( 34 | // JsonConvert.SerializeObject(recoverMessage) 35 | // ) 36 | // ) 37 | //} 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Eventfully.Core.Analyzers.Test/Eventfully.Core.Analyzers.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/Eventfully.Core.Analyzers.Test/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | 4 | namespace TestHelper 5 | { 6 | /// 7 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 8 | /// 9 | public struct DiagnosticResultLocation 10 | { 11 | public DiagnosticResultLocation(string path, int line, int column) 12 | { 13 | if (line < -1) 14 | { 15 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 16 | } 17 | 18 | if (column < -1) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 21 | } 22 | 23 | this.Path = path; 24 | this.Line = line; 25 | this.Column = column; 26 | } 27 | 28 | public string Path { get; } 29 | public int Line { get; } 30 | public int Column { get; } 31 | } 32 | 33 | /// 34 | /// Struct that stores information about a Diagnostic appearing in a source 35 | /// 36 | public struct DiagnosticResult 37 | { 38 | private DiagnosticResultLocation[] locations; 39 | 40 | public DiagnosticResultLocation[] Locations 41 | { 42 | get 43 | { 44 | if (this.locations == null) 45 | { 46 | this.locations = new DiagnosticResultLocation[] { }; 47 | } 48 | return this.locations; 49 | } 50 | 51 | set 52 | { 53 | this.locations = value; 54 | } 55 | } 56 | 57 | public DiagnosticSeverity Severity { get; set; } 58 | 59 | public string Id { get; set; } 60 | 61 | public string Message { get; set; } 62 | 63 | public string Path 64 | { 65 | get 66 | { 67 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 68 | } 69 | } 70 | 71 | public int Line 72 | { 73 | get 74 | { 75 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 76 | } 77 | } 78 | 79 | public int Column 80 | { 81 | get 82 | { 83 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Eventfully.Core.UnitTests/Eventfully.Core.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | 9D2035C3-769F-4026-B9C1-199A05CF8EFA 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/Eventfully.Core.UnitTests/MetaDataTests.cs: -------------------------------------------------------------------------------- 1 | using Shouldly; 2 | using System; 3 | using Xunit; 4 | 5 | namespace Eventfully.Core.UnitTests 6 | { 7 | public class MetaDataTests 8 | { 9 | 10 | public MetaDataTests() 11 | { 12 | 13 | } 14 | 15 | 16 | [Fact] 17 | public void Should_calculate_expiration() 18 | { 19 | var now = DateTime.UtcNow; 20 | var future = now.AddSeconds(1); 21 | var past = now.AddSeconds(-1); 22 | 23 | MessageMetaData md = new MessageMetaData() { ExpiresAtUtc = now}; 24 | md.IsExpired(future).ShouldBeTrue(); 25 | md.IsExpired(past).ShouldBeFalse(); 26 | md.IsExpired(now).ShouldBeTrue(); //expirate <= 27 | } 28 | 29 | [Fact] 30 | public void Should_default_skip_transient_dispatch_true() 31 | { 32 | MessageMetaData md = new MessageMetaData(); 33 | md.SkipTransientDispatch.ShouldBeFalse(); 34 | 35 | } 36 | 37 | [Fact] 38 | public void Should_ignore_null_values_in_constructor() 39 | { 40 | MessageMetaData md = new MessageMetaData(); 41 | md.ContainsKey(HeaderType.MessageId.Value).ShouldBeFalse(); 42 | md.ContainsKey(HeaderType.DispatchDelay.Value).ShouldBeFalse(); 43 | md.ContainsKey(HeaderType.CorrelationId.Value).ShouldBeFalse(); 44 | md.ContainsKey(HeaderType.ExpiresAtUtc.Value).ShouldBeFalse(); 45 | 46 | } 47 | 48 | } 49 | 50 | 51 | 52 | 53 | 54 | } -------------------------------------------------------------------------------- /tests/Eventfully.Core.UnitTests/Util/UnitTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using FakeItEasy; 10 | using System.Reflection; 11 | using Eventfully.Handlers; 12 | using Newtonsoft.Json; 13 | using System.Text; 14 | using Eventfully.Outboxing; 15 | 16 | namespace Eventfully.Core.UnitTests 17 | { 18 | 19 | public class UnitTestFixture 20 | { 21 | 22 | protected static IConfigurationRoot _config; 23 | protected static IServiceProvider _serviceProvider; 24 | //public static MessagingService MessagingService; 25 | public static IOutbox Outbox { get; set; } 26 | 27 | static UnitTestFixture() 28 | { 29 | //Outbox = A.Fake(); 30 | //var handlerFactory = A.Fake(); 31 | //MessagingService = new MessagingService(Outbox, handlerFactory); 32 | } 33 | 34 | //public static IServiceScope NewScope() => _serviceProvider.CreateScope(); 35 | 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Eventfully.EFCoreOutbox.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | d57cc2de-a5e2-49b8-99d2-d9ede30b10ca 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | PreserveNewest 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Migrations/20200106191125_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.EFCoreOutbox.IntegrationTests; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Eventfully.EFCoreOutbox.IntegrationTests.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20200106191125_Initial")] 14 | partial class Initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 25 | { 26 | b.Property("Id"); 27 | 28 | b.Property("CreatedAtUtc"); 29 | 30 | b.Property("Endpoint") 31 | .HasMaxLength(500); 32 | 33 | b.Property("ExpiresAtUtc"); 34 | 35 | b.Property("PriorityDateUtc"); 36 | 37 | b.Property("Status"); 38 | 39 | b.Property("TryCount"); 40 | 41 | b.Property("Type") 42 | .IsRequired() 43 | .HasMaxLength(500); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.HasIndex("PriorityDateUtc", "Status") 48 | .HasName("IX_PriorityDateUtc") 49 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 50 | 51 | b.ToTable("OutboxMessages"); 52 | }); 53 | 54 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 55 | { 56 | b.Property("Id"); 57 | 58 | b.Property("Data") 59 | .IsRequired(); 60 | 61 | b.Property("MetaData"); 62 | 63 | b.HasKey("Id"); 64 | 65 | b.ToTable("OutboxMessageData"); 66 | }); 67 | 68 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 69 | { 70 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 71 | .WithOne("MessageData") 72 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 73 | .OnDelete(DeleteBehavior.Cascade); 74 | }); 75 | #pragma warning restore 612, 618 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Migrations/20200106191125_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Eventfully.EFCoreOutbox.IntegrationTests.Migrations 5 | { 6 | public partial class Initial : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "OutboxMessages", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false), 15 | PriorityDateUtc = table.Column(nullable: false), 16 | TryCount = table.Column(nullable: false), 17 | Type = table.Column(maxLength: 500, nullable: false), 18 | Endpoint = table.Column(maxLength: 500, nullable: true), 19 | Status = table.Column(nullable: false), 20 | CreatedAtUtc = table.Column(nullable: false), 21 | ExpiresAtUtc = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_OutboxMessages", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "OutboxMessageData", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false), 33 | Data = table.Column(nullable: false), 34 | MetaData = table.Column(nullable: true) 35 | }, 36 | constraints: table => 37 | { 38 | table.PrimaryKey("PK_OutboxMessageData", x => x.Id); 39 | table.ForeignKey( 40 | name: "FK_OutboxMessageData_OutboxMessages_Id", 41 | column: x => x.Id, 42 | principalTable: "OutboxMessages", 43 | principalColumn: "Id", 44 | onDelete: ReferentialAction.Cascade); 45 | }); 46 | 47 | migrationBuilder.CreateIndex( 48 | name: "IX_PriorityDateUtc", 49 | table: "OutboxMessages", 50 | columns: new[] { "PriorityDateUtc", "Status" }) 51 | .Annotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 52 | } 53 | 54 | protected override void Down(MigrationBuilder migrationBuilder) 55 | { 56 | migrationBuilder.DropTable( 57 | name: "OutboxMessageData"); 58 | 59 | migrationBuilder.DropTable( 60 | name: "OutboxMessages"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Eventfully.EFCoreOutbox.IntegrationTests; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Eventfully.EFCoreOutbox.IntegrationTests.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage", b => 23 | { 24 | b.Property("Id"); 25 | 26 | b.Property("CreatedAtUtc"); 27 | 28 | b.Property("Endpoint") 29 | .HasMaxLength(500); 30 | 31 | b.Property("ExpiresAtUtc"); 32 | 33 | b.Property("PriorityDateUtc"); 34 | 35 | b.Property("Status"); 36 | 37 | b.Property("TryCount"); 38 | 39 | b.Property("Type") 40 | .IsRequired() 41 | .HasMaxLength(500); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.HasIndex("PriorityDateUtc", "Status") 46 | .HasName("IX_PriorityDateUtc") 47 | .HasAnnotation("SqlServer:Include", new[] { "TryCount", "Type", "ExpiresAtUtc", "CreatedAtUtc", "Endpoint" }); 48 | 49 | b.ToTable("OutboxMessages"); 50 | }); 51 | 52 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 53 | { 54 | b.Property("Id"); 55 | 56 | b.Property("Data") 57 | .IsRequired(); 58 | 59 | b.Property("MetaData"); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.ToTable("OutboxMessageData"); 64 | }); 65 | 66 | modelBuilder.Entity("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", b => 67 | { 68 | b.HasOne("Eventfully.EFCoreOutbox.OutboxMessage") 69 | .WithOne("MessageData") 70 | .HasForeignKey("Eventfully.EFCoreOutbox.OutboxMessage+OutboxMessageData", "Id") 71 | .OnDelete(DeleteBehavior.Cascade); 72 | }); 73 | #pragma warning restore 612, 618 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Util/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Migrations; 9 | using Eventfully; 10 | using Eventfully.EFCoreOutbox; 11 | using Microsoft.EntityFrameworkCore.Design; 12 | using Microsoft.Extensions.Configuration; 13 | using System.IO; 14 | using Microsoft.Extensions.DependencyInjection; 15 | using System.Reflection; 16 | 17 | namespace Eventfully.EFCoreOutbox.IntegrationTests 18 | { 19 | public class ApplicationDbContext : DbContext, ISupportTransientDispatch 20 | { 21 | public event EventHandler ChangesPersisted; 22 | 23 | public ApplicationDbContext(DbContextOptions options) 24 | : base(options) 25 | {} 26 | 27 | 28 | protected override void OnModelCreating(ModelBuilder builder) 29 | { 30 | base.OnModelCreating(builder); 31 | 32 | /*** OUTBOX Message Entities (see Messaging.EFCoreOutbox) ***/ 33 | builder.AddEFCoreOutbox(); 34 | } 35 | 36 | public override int SaveChanges() 37 | { 38 | _preSaveChanges(); 39 | var res = base.SaveChanges(); 40 | _postSaveChanges(); 41 | return res; 42 | } 43 | 44 | public override async Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)) 45 | { 46 | _preSaveChanges(); 47 | var res = await base.SaveChangesAsync(cancellationToken); 48 | _postSaveChanges(); 49 | return res; 50 | } 51 | 52 | private void _preSaveChanges() 53 | { 54 | _addDateTimeStamps(); 55 | } 56 | private void _postSaveChanges() 57 | { 58 | this.ChangesPersisted?.Invoke(this, null); 59 | } 60 | 61 | 62 | private void _addDateTimeStamps() 63 | { 64 | foreach (var item in ChangeTracker.Entries().Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)) 65 | { 66 | var now = DateTime.UtcNow; 67 | 68 | if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAt") != null) 69 | item.Property("CreatedAt").CurrentValue = now; 70 | else if (item.State == EntityState.Added && item.Metadata.FindProperty("CreatedAtUtc") != null) 71 | item.Property("CreatedAtUtc").CurrentValue = now; 72 | 73 | if (item.Metadata.FindProperty("UpdatedAt") != null) 74 | item.Property("UpdatedAt").CurrentValue = now; 75 | else if (item.Metadata.FindProperty("UpdatedAtUtc") != null) 76 | item.Property("UpdatedAtUtc").CurrentValue = now; 77 | } 78 | } 79 | 80 | } 81 | 82 | 83 | 84 | 85 | } 86 | 87 | 88 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Util/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Nito.AsyncEx; 7 | using Microsoft.Extensions.Logging; 8 | using Xunit.Sdk; 9 | using Xunit.Abstractions; 10 | 11 | namespace Eventfully.EFCoreOutbox.IntegrationTests 12 | { 13 | 14 | public abstract class IntegrationTestBase : IAsyncLifetime 15 | { 16 | private static readonly AsyncLock Mutex = new AsyncLock(); 17 | 18 | private static bool _initialized; 19 | 20 | /// 21 | /// Clear database once per session 22 | /// 23 | /// 24 | public virtual async Task InitializeAsync() 25 | { 26 | if (_initialized) return; 27 | 28 | using (await Mutex.LockAsync()) 29 | { 30 | if (_initialized) return; 31 | await IntegrationTestFixture.ResetCheckpoint(); 32 | _initialized = true; 33 | } 34 | } 35 | 36 | public virtual Task DisposeAsync() => Task.CompletedTask; 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/Util/TestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.EFCoreOutbox.IntegrationTests 6 | { 7 | public class TestMessage : IntegrationCommand 8 | { 9 | public override string MessageType => "Test.MessageType"; 10 | 11 | public Guid Id { get; set; } 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public DateTime MessageDate { get; set; } = DateTime.UtcNow; 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.IntegrationTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationConnection": "Server=(localdb)\\mssqllocaldb;Database=eventfully-test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | 6 | 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Warning", 10 | "System": "Warning", 11 | "Microsoft": "Warning", 12 | "Eventfully": "Debug" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.UnitTests/Eventfully.EFCoreOutbox.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.0 5 | d57cc2de-a5e2-49b8-99d2-d9ede30b10ca 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/Eventfully.EFCoreOutbox.UnitTests/TestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.EFCoreOutbox.UnitTests 6 | { 7 | public class TestMessage : IntegrationCommand 8 | { 9 | public override string MessageType => "Test.MesageType"; 10 | 11 | public Guid Id { get; set; } 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public DateTime MessageDate { get; set; } = DateTime.UtcNow; 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Eventfully.Semaphore.SqlServer.IntegrationTests/Eventfully.Semaphore.SqlServer.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | 4F95FFB6-A222-4330-8261-DCA7F4C8CCB6 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | true 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers; buildtransitive 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/Eventfully.Semaphore.SqlServer.IntegrationTests/Util/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Nito.AsyncEx; 7 | using Microsoft.Extensions.Logging; 8 | using Xunit.Sdk; 9 | using Xunit.Abstractions; 10 | 11 | namespace Eventfully.Semaphore.SqlServer.IntegrationTests 12 | { 13 | 14 | public abstract class IntegrationTestBase : IAsyncLifetime 15 | { 16 | private static readonly AsyncLock Mutex = new AsyncLock(); 17 | 18 | private static bool _initialized; 19 | 20 | /// 21 | /// Clear database once per session 22 | /// 23 | /// 24 | public virtual async Task InitializeAsync() 25 | { 26 | if (_initialized) return; 27 | 28 | using (await Mutex.LockAsync()) 29 | { 30 | if (_initialized) return; 31 | //Uncomment to clear database at beginning of session 32 | //await IntegrationTestFixture.ResetCheckpoint(); 33 | _initialized = true; 34 | } 35 | } 36 | 37 | public virtual Task DisposeAsync() => Task.CompletedTask; 38 | } 39 | 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /tests/Eventfully.Semaphore.SqlServer.IntegrationTests/Util/IntegrationTestFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Respawn; 10 | using FakeItEasy; 11 | using System.Reflection; 12 | using Newtonsoft.Json; 13 | using System.Text; 14 | using Xunit; 15 | 16 | [assembly: CollectionBehavior(DisableTestParallelization = true)] 17 | 18 | namespace Eventfully.Semaphore.SqlServer.IntegrationTests 19 | { 20 | /// 21 | ///A class with no code, only used to define the collection 22 | /// 23 | //[CollectionDefinition("Sequential", DisableParallelization = true)] 24 | //public class SequentialCollectionDefinition {} 25 | 26 | public class IntegrationTestFixture //: IDesignTimeDbContextFactory 27 | { 28 | 29 | protected static IConfigurationRoot _config; 30 | protected static IServiceProvider _serviceProvider; 31 | protected static readonly Checkpoint _checkpoint; 32 | 33 | static IntegrationTestFixture() 34 | { 35 | _config = new ConfigurationBuilder() 36 | .SetBasePath(Directory.GetCurrentDirectory()) 37 | .AddJsonFile("appsettings.json", optional: true) 38 | .AddUserSecrets() 39 | .AddEnvironmentVariables() 40 | .Build(); 41 | 42 | var services = new ServiceCollection(); 43 | 44 | services.AddSingleton(_config); 45 | 46 | services.AddLogging(builder => { 47 | builder.AddConfiguration(_config.GetSection("Logging")); 48 | //builder.AddConsole(); 49 | builder.AddDebug(); 50 | }); 51 | 52 | _serviceProvider = services.BuildServiceProvider(); 53 | 54 | _checkpoint = new Checkpoint() 55 | { 56 | TablesToIgnore = new[] 57 | { 58 | "__EFMigrationsHistory", 59 | }, 60 | //SchemasToExclude = new[]{} 61 | }; 62 | 63 | ///Setup internal logging for eventfully 64 | Logging.LoggerFactory = _serviceProvider.GetRequiredService(); 65 | 66 | 67 | } 68 | 69 | public static string ConnectionString => _config.GetConnectionString("ApplicationConnection"); 70 | 71 | public static Task ResetCheckpoint() => _checkpoint.Reset(ConnectionString); 72 | 73 | public static IServiceScope NewScope() => _serviceProvider.CreateScope(); 74 | 75 | 76 | 77 | 78 | //public ApplicationDbContext CreateDbContext(string[] args) 79 | // { 80 | // var builder = new ConfigurationBuilder() 81 | // .SetBasePath(Directory.GetCurrentDirectory()) 82 | // .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 83 | // .AddUserSecrets(); 84 | // var config = builder.Build(); 85 | // var services = new ServiceCollection(); 86 | 87 | // var migrationsAssembly = typeof(IntegrationTestFixture).GetTypeInfo().Assembly.GetName().Name; 88 | 89 | // services.AddDbContext( 90 | // x => x.UseSqlServer(config.GetConnectionString("ApplicationConnection"), 91 | // b => b.MigrationsAssembly(migrationsAssembly) 92 | // )); 93 | 94 | // var _serviceProvider = services.BuildServiceProvider(); 95 | // var db = _serviceProvider.GetService(); 96 | // return db; 97 | // } 98 | 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/Eventfully.Semaphore.SqlServer.IntegrationTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationConnection": "Server=(localdb)\\mssqllocaldb;Database=eventfully-test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | 6 | 7 | "Logging": { 8 | "LogLevel": { 9 | "Default": "Warning", 10 | "System": "Warning", 11 | "Microsoft": "Warning", 12 | "Eventfully": "Debug" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.IntegrationTests/Eventfully.Transports.AzureServiceBus.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | C9ED1696-A9AB-4A97-911B-C560D02D8762 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | true 17 | PreserveNewest 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers; buildtransitive 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.IntegrationTests/Util/FakeInterfaces.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.ServiceBus; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Eventfully.Transports.AzureServieBus.IntegrationTests 9 | { 10 | public interface ITestMessageHandler 11 | { 12 | Task HandleException(ExceptionReceivedEventArgs args); 13 | Task HandleMessage(Message m, CancellationToken token); 14 | 15 | } 16 | 17 | public interface ITestPumpCallback 18 | { 19 | Task Handle(TransportMessage message, IEndpoint endpoint); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.IntegrationTests/Util/IntegrationTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | using Nito.AsyncEx; 7 | using Microsoft.Extensions.Logging; 8 | using Xunit.Sdk; 9 | using Xunit.Abstractions; 10 | 11 | namespace Eventfully.Transports.AzureServieBus.IntegrationTests 12 | { 13 | 14 | public abstract class IntegrationTestBase : IAsyncLifetime 15 | { 16 | private static readonly AsyncLock Mutex = new AsyncLock(); 17 | 18 | private static bool _initialized; 19 | 20 | /// 21 | /// Clear database once per session 22 | /// 23 | /// 24 | public virtual async Task InitializeAsync() 25 | { 26 | if (_initialized) return; 27 | 28 | using (await Mutex.LockAsync()) 29 | { 30 | if (_initialized) return; 31 | //await IntegrationTestFixture.ResetCheckpoint(); 32 | _initialized = true; 33 | } 34 | } 35 | 36 | public virtual Task DisposeAsync() => Task.CompletedTask; 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.IntegrationTests/Util/TestMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Eventfully.Transports.AzureServieBus.IntegrationTests 6 | { 7 | public class TestMessage : IntegrationEvent 8 | { 9 | public override string MessageType => "Test.AsbMessageType"; 10 | 11 | public Guid Id { get; set; } 12 | public string Name { get; set; } 13 | public string Description { get; set; } 14 | public DateTime MessageDate { get; set; } = DateTime.UtcNow; 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.IntegrationTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "ApplicationConnection": "Server=(localdb)\\mssqllocaldb;Database=eventfully-test;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | 6 | "QueueConnectionString": "Endpoint=sb://xxx.servicebus.windows.net/;SharedAccessKeyName=integration-test;SharedAccessKey=;EntityPath=eventfully-test-queue", 7 | "TopicConnectionString": "Endpoint=sb://xxx.servicebus.windows.net/;SharedAccessKeyName=integration-test;SharedAccessKey=;EntityPath=eventfully-test-topic", 8 | "SubscriptionConnectionString": "Endpoint=sb://xxx.servicebus.windows.net/;SharedAccessKeyName=integration-test-read;SharedAccessKey=;EntityPath=eventfully-test-topic/subscriptions/eventfully-test-subscription", 9 | 10 | 11 | "Logging": { 12 | "LogLevel": { 13 | "Default": "Warning", 14 | "System": "Warning", 15 | "Microsoft": "Warning", 16 | "Eventfully": "Debug" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Eventfully.Transports.AzureServiceBus.UnitTests.UnitTests/Eventfully.Transports.AzureServiceBus.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | netcoreapp2.2 6 | 0E43ABC9-07D8-4893-91B3-32AD711DB8F3 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | --------------------------------------------------------------------------------