diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5468acdd..9ea9a83a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,31 +9,29 @@ on: - master jobs: build: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup dotnet 6.0 - uses: actions/setup-dotnet@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' - - name: Setup dotnet 8.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '8.0.100' + dotnet-version: | + 8.0.x + 9.0.x - name: Build and Test run: ./Build.ps1 shell: pwsh - name: Push to MyGet env: - NUGET_URL: https://www.myget.org/F/mediatr-ci/api/v3/index.json - NUGET_API_KEY: ${{ secrets.MYGET_MEDIATR_CI_API_KEY }} + NUGET_URL: https://www.myget.org/F/luckypennysoftware/api/v3/index.json + NUGET_API_KEY: ${{ secrets.MYGET_API_KEY }} run: ./Push.ps1 shell: pwsh - name: Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: artifacts path: artifacts/**/* \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80197b5b..a5f59fd5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,39 +8,37 @@ jobs: build: strategy: matrix: - os: [ubuntu-latest] + os: [windows-latest] fail-fast: false - runs-on: ubuntu-latest + runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup dotnet 6.0 - uses: actions/setup-dotnet@v1 + - name: Setup dotnet + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' - - name: Setup dotnet 8.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '8.0.100' + dotnet-version: | + 8.0.x + 9.0.x - name: Build and Test run: ./Build.ps1 shell: pwsh - name: Push to MyGet env: - NUGET_URL: https://www.myget.org/F/mediatr-ci/api/v3/index.json - NUGET_API_KEY: ${{ secrets.MYGET_MEDIATR_CI_API_KEY }} + NUGET_URL: https://www.myget.org/F/luckypennysoftware/api/v3/index.json + NUGET_API_KEY: ${{ secrets.MYGET_API_KEY }} run: ./Push.ps1 shell: pwsh - name: Push to NuGet env: NUGET_URL: https://api.nuget.org/v3/index.json - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + NUGET_API_KEY: ${{ secrets.MEDIATR_NUGET_API_KEY }} run: ./Push.ps1 shell: pwsh - name: Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: artifacts path: artifacts/**/* \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e06d2081..00000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..8d5c9bce --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,10 @@ +By accessing code under the [Lucky Penny Software GitHub Organization](https://github.com/LuckyPennySoftware) (Lucky Penny Software) here, you are agreeing to the following licensing terms. +If you do not agree to these terms, do not access Lucky Penny Software code. + +Your license to Lucky Penny Software source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here: + +https://opensource.org/license/rpl-1-5/ + +If you do not wish to release the source of software you build using Lucky Penny Software source code and/or binaries under the terms above, you may use Lucky Penny Software source code and/or binaries under the License Agreement described here: + +https://luckypennysoftware.com/license \ No newline at end of file diff --git a/README.md b/README.md index 339e29c5..32399f89 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ MediatR ======= -![CI](https://github.com/jbogard/MediatR/workflows/CI/badge.svg) +![CI](https://github.com/LuckyPennySoftware/MediatR/workflows/CI/badge.svg) [![NuGet](https://img.shields.io/nuget/dt/mediatr.svg)](https://www.nuget.org/packages/mediatr) [![NuGet](https://img.shields.io/nuget/vpre/mediatr.svg)](https://www.nuget.org/packages/mediatr) [![MyGet (dev)](https://img.shields.io/myget/mediatr-ci/v/MediatR.svg)](https://myget.org/gallery/mediatr-ci) @@ -12,7 +12,7 @@ In-process messaging with no dependencies. Supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatching via C# generic variance. -Examples in the [wiki](https://github.com/jbogard/MediatR/wiki). +Examples in the [wiki](https://github.com/LuckyPennySoftware/MediatR/wiki). ### Installing MediatR @@ -87,3 +87,16 @@ services.AddMediatR(cfg => { ``` With additional methods for open generics and overloads for explicit service types. + +### Setting the license key + +You can set the license key when registering MediatR: + +```csharp +services.AddMediatR(cfg => +{ + cfg.LicenseKey = ""; +}) +``` + +You can register for your license key at [MediatR.io](https://mediatr.io) \ No newline at end of file diff --git a/samples/MediatR.Examples.Autofac/Program.cs b/samples/MediatR.Examples.Autofac/Program.cs index 572b30fe..e258ec25 100644 --- a/samples/MediatR.Examples.Autofac/Program.cs +++ b/samples/MediatR.Examples.Autofac/Program.cs @@ -45,7 +45,7 @@ private static IMediator BuildMediator(WrappingWriter writer) // this call will cause a handler to be called twice // in general you should try to avoid having a class implementing for instance `IRequestHandler<,>` and `INotificationHandler<>` // the other option would be to remove this call - // see also https://github.com/jbogard/MediatR/issues/462 + // see also https://github.com/LuckyPennySoftware/MediatR/issues/462 .AsImplementedInterfaces(); } diff --git a/src/MediatR.Contracts/MediatR.Contracts.csproj b/src/MediatR.Contracts/MediatR.Contracts.csproj index a311f618..38183c67 100644 --- a/src/MediatR.Contracts/MediatR.Contracts.csproj +++ b/src/MediatR.Contracts/MediatR.Contracts.csproj @@ -6,7 +6,6 @@ Contracts package for requests, responses, and notifications Copyright Jimmy Bogard netstandard2.0 - enable strict mediator;request;response;queries;commands;notifications true diff --git a/src/MediatR/Entities/OpenBehavior.cs b/src/MediatR/Entities/OpenBehavior.cs new file mode 100644 index 00000000..0cb97c34 --- /dev/null +++ b/src/MediatR/Entities/OpenBehavior.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; + +namespace MediatR.Entities; +/// +/// Represents a registration entity for pipeline behaviors with a specified service lifetime. +/// +public class OpenBehavior +{ + /// + /// Initializes a new instance of the class. + /// + /// The type of the pipeline behavior to register. + /// The lifetime of the registered service. Defaults to Transient. + /// Thrown if the specified type does not implement IPipelineBehavior. + /// Thrown if is null. + public OpenBehavior(Type openBehaviorType, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + { + ValidatePipelineBehaviorType(openBehaviorType); + OpenBehaviorType = openBehaviorType; + ServiceLifetime = serviceLifetime; + } + + /// + /// The type of the open behavior. + /// + public Type OpenBehaviorType { get; } + + /// + /// The service lifetime of the open behavior. + /// + public ServiceLifetime ServiceLifetime { get; } + + /// + /// Validates whether the specified type implements the interface. + /// + /// The type to validate. + /// Thrown if the type does not implement . + /// Thrown if is null. + private static void ValidatePipelineBehaviorType(Type openBehaviorType) + { + if (openBehaviorType == null) throw new ArgumentNullException($"Open behavior type can not be null."); + + bool isPipelineBehavior = openBehaviorType.GetInterfaces() + .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPipelineBehavior<,>)); + + if (!isPipelineBehavior) + { + throw new InvalidOperationException($"The type \"{openBehaviorType.Name}\" must implement IPipelineBehavior<,> interface."); + } + } +} \ No newline at end of file diff --git a/src/MediatR/IPipelineBehavior.cs b/src/MediatR/IPipelineBehavior.cs index be6b1cb3..142ec707 100644 --- a/src/MediatR/IPipelineBehavior.cs +++ b/src/MediatR/IPipelineBehavior.cs @@ -9,7 +9,7 @@ namespace MediatR; /// /// Response type /// Awaitable task returning a -public delegate Task RequestHandlerDelegate(); +public delegate Task RequestHandlerDelegate(CancellationToken t = default); /// /// Pipeline behavior to surround the inner handler. diff --git a/src/MediatR/Licensing/Edition.cs b/src/MediatR/Licensing/Edition.cs new file mode 100644 index 00000000..6d97444b --- /dev/null +++ b/src/MediatR/Licensing/Edition.cs @@ -0,0 +1,9 @@ +namespace MediatR.Licensing; + +internal enum Edition +{ + Community = 0, + Standard = 1, + Professional = 2, + Enterprise = 3 +} \ No newline at end of file diff --git a/src/MediatR/Licensing/License.cs b/src/MediatR/Licensing/License.cs new file mode 100644 index 00000000..8848be66 --- /dev/null +++ b/src/MediatR/Licensing/License.cs @@ -0,0 +1,64 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; + +namespace MediatR.Licensing; + +internal class License +{ + internal License(params Claim[] claims) : this(new ClaimsPrincipal(new ClaimsIdentity(claims))) + { + + } + + public License(ClaimsPrincipal claims) + { + if (Guid.TryParse(claims.FindFirst("account_id")?.Value, out var accountId)) + { + AccountId = accountId; + } + + CustomerId = claims.FindFirst("customer_id")?.Value; + SubscriptionId = claims.FindFirst("sub_id")?.Value; + + if (long.TryParse(claims.FindFirst("iat")?.Value, out var iat)) + { + var startedAt = DateTimeOffset.FromUnixTimeSeconds(iat); + StartDate = startedAt; + } + + if (long.TryParse(claims.FindFirst("exp")?.Value, out var exp)) + { + var expiredAt = DateTimeOffset.FromUnixTimeSeconds(exp); + ExpirationDate = expiredAt; + } + + if (Enum.TryParse(claims.FindFirst("edition")?.Value, out var edition)) + { + Edition = edition; + } + + if (Enum.TryParse(claims.FindFirst("type")?.Value, out var productType)) + { + ProductType = productType; + } + + IsConfigured = AccountId != null + && CustomerId != null + && SubscriptionId != null + && StartDate != null + && ExpirationDate != null + && Edition != null + && ProductType != null; + } + + public Guid? AccountId { get; } + public string? CustomerId { get; } + public string? SubscriptionId { get; } + public DateTimeOffset? StartDate { get; } + public DateTimeOffset? ExpirationDate { get; } + public Edition? Edition { get; } + public ProductType? ProductType { get; } + + public bool IsConfigured { get; } +} \ No newline at end of file diff --git a/src/MediatR/Licensing/LicenseAccessor.cs b/src/MediatR/Licensing/LicenseAccessor.cs new file mode 100644 index 00000000..4df15258 --- /dev/null +++ b/src/MediatR/Licensing/LicenseAccessor.cs @@ -0,0 +1,84 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Tokens; +using Convert = System.Convert; + +namespace MediatR.Licensing; + +internal class LicenseAccessor +{ + private readonly MediatRServiceConfiguration _configuration; + private readonly ILogger _logger; + + public LicenseAccessor(MediatRServiceConfiguration configuration, ILoggerFactory loggerFactory) + { + _configuration = configuration; + _logger = loggerFactory.CreateLogger("LuckyPennySoftware.MediatR.License"); + } + + private License? _license; + private readonly object _lock = new(); + + public License Current => _license ??= Initialize(); + + private License Initialize() + { + lock (_lock) + { + if (_license != null) + { + return _license; + } + + var key = _configuration.LicenseKey; + if (key == null) + { + return new License(); + } + + var licenseClaims = ValidateKey(key); + return licenseClaims.Any() + ? new License(new ClaimsPrincipal(new ClaimsIdentity(licenseClaims))) + : new License(); + } + } + + private Claim[] ValidateKey(string licenseKey) + { + var handler = new JsonWebTokenHandler(); + + var rsa = new RSAParameters + { + Exponent = Convert.FromBase64String("AQAB"), + Modulus = Convert.FromBase64String( + "2LTtdJV2b0mYoRqChRCfcqnbpKvsiCcDYwJ+qPtvQXWXozOhGo02/V0SWMFBdbZHUzpEytIiEcojo7Vbq5mQmt4lg92auyPKsWq6qSmCVZCUuL/kpYqLCit4yUC0YqZfw4H9zLf1yAIOgyXQf1x6g+kscDo1pWAniSl9a9l/LXRVEnGz+OfeUrN/5gzpracGUY6phx6T09UCRuzi4YqqO4VJzL877W0jCW2Q7jMzHxOK04VSjNc22CADuCd34mrFs23R0vVm1DVLYtPGD76/rGOcxO6vmRc7ydBAvt1IoUsrY0vQ2rahp51YPxqqhKPd8nNOomHWblCCA7YUeV3C1Q==") + };; + + var key = new RsaSecurityKey(rsa) + { + KeyId = "LuckyPennySoftwareLicenseKey/bbb13acb59904d89b4cb1c85f088ccf9" + }; + + var parms = new TokenValidationParameters + { + ValidIssuer = "https://luckypennysoftware.com", + ValidAudience = "LuckyPennySoftware", + IssuerSigningKey = key, + ValidateLifetime = false + }; + + var validateResult = handler.ValidateTokenAsync(licenseKey, parms).Result; + if (!validateResult.IsValid) + { + _logger.LogCritical(validateResult.Exception, "Error validating the Lucky Penny software license key"); + } + + return validateResult.ClaimsIdentity?.Claims.ToArray() ?? Array.Empty(); + } + +} \ No newline at end of file diff --git a/src/MediatR/Licensing/LicenseValidator.cs b/src/MediatR/Licensing/LicenseValidator.cs new file mode 100644 index 00000000..ae56499a --- /dev/null +++ b/src/MediatR/Licensing/LicenseValidator.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Logging; + +namespace MediatR.Licensing; + +internal class LicenseValidator +{ + private readonly ILogger _logger; + + public LicenseValidator(ILoggerFactory loggerFactory) + => _logger = loggerFactory.CreateLogger("LuckyPennySoftware.MediatR.License"); + + public void Validate(License license) + { + var errors = new List(); + + if (license is not { IsConfigured: true }) + { + var message = "You do not have a valid license key for the Lucky Penny software MediatR. " + + "This is allowed for development and testing scenarios. " + + "If you are running in production you are required to have a licensed version. " + + "Please visit https://luckypennysoftware.com to obtain a valid license."; + + _logger.LogWarning(message); + return; + } + + _logger.LogDebug("The Lucky Penny license key details: {license}", license); + + var diff = DateTime.UtcNow.Date.Subtract(license.ExpirationDate!.Value.Date).TotalDays; + if (diff > 0) + { + errors.Add($"Your license for the Lucky Penny software MediatR expired {diff} days ago."); + } + + if (license.ProductType!.Value != ProductType.MediatR + && license.ProductType.Value != ProductType.Bundle) + { + errors.Add("Your Lucky Penny software license does not include MediatR."); + } + + if (errors.Count > 0) + { + foreach (var err in errors) + { + _logger.LogError(err); + } + + _logger.LogError( + "Please visit https://luckypennysoftware.com to obtain a valid license for the Lucky Penny software MediatR."); + } + else + { + _logger.LogInformation("You have a valid license key for the Lucky Penny software {type} {edition} edition. The license expires on {licenseExpiration}.", + license.ProductType, + license.Edition, + license.ExpirationDate); + } + } +} \ No newline at end of file diff --git a/src/MediatR/Licensing/ProductType.cs b/src/MediatR/Licensing/ProductType.cs new file mode 100644 index 00000000..88ef88e0 --- /dev/null +++ b/src/MediatR/Licensing/ProductType.cs @@ -0,0 +1,8 @@ +namespace MediatR.Licensing; + +internal enum ProductType +{ + AutoMapper = 0, + MediatR = 1, + Bundle = 2 +} \ No newline at end of file diff --git a/src/MediatR/MediatR.csproj b/src/MediatR/MediatR.csproj index b39277b4..2f5a48a4 100644 --- a/src/MediatR/MediatR.csproj +++ b/src/MediatR/MediatR.csproj @@ -4,7 +4,7 @@ Jimmy Bogard Simple, unambitious mediator implementation in .NET Copyright Jimmy Bogard - netstandard2.0;net6.0 + netstandard2.0;net8.0;net9.0 enable strict mediator;request;response;queries;commands;notifications @@ -13,8 +13,10 @@ true gradient_128x128.png README.md + True + LICENSE.md + https://mediatr.io v - Apache-2.0 true true snupkg @@ -25,6 +27,8 @@ + + @@ -33,14 +37,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + + - - - - - + - + diff --git a/src/MediatR/Mediator.cs b/src/MediatR/Mediator.cs index 4d65de18..513fb75b 100644 --- a/src/MediatR/Mediator.cs +++ b/src/MediatR/Mediator.cs @@ -1,4 +1,5 @@ using MediatR.NotificationPublishers; +using Microsoft.Extensions.DependencyInjection; namespace MediatR; @@ -37,6 +38,8 @@ public Mediator(IServiceProvider serviceProvider, INotificationPublisher publish { _serviceProvider = serviceProvider; _publisher = publisher; + + _serviceProvider.CheckLicense(); } public Task Send(IRequest request, CancellationToken cancellationToken = default) diff --git a/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs b/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs index bc785f91..2fc7acf7 100644 --- a/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs +++ b/src/MediatR/MicrosoftExtensionsDI/MediatrServiceConfiguration.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using MediatR; +using MediatR.Entities; using MediatR.NotificationPublishers; using MediatR.Pipeline; using MediatR.Registration; @@ -94,6 +95,11 @@ public class MediatRServiceConfiguration /// public bool RegisterGenericHandlers { get; set; } = false; + /// + /// Gets or sets the license key. You can find your license key in your account. + /// + public string? LicenseKey { get; set; } + /// /// Register various handlers from assembly containing given type /// @@ -222,6 +228,37 @@ public MediatRServiceConfiguration AddOpenBehavior(Type openBehaviorType, Servic return this; } + /// + /// Registers multiple open behavior types against the open generic interface type + /// + /// An open generic behavior type list includes multiple open generic behavior types. + /// Optional service lifetime, defaults to . + /// This + public MediatRServiceConfiguration AddOpenBehaviors(IEnumerable openBehaviorTypes, ServiceLifetime serviceLifetime = ServiceLifetime.Transient) + { + foreach (var openBehaviorType in openBehaviorTypes) + { + AddOpenBehavior(openBehaviorType, serviceLifetime); + } + + return this; + } + + /// + /// Registers open behaviors against the open generic interface type + /// + /// An open generic behavior list includes multiple open generic behaviors. + /// This + public MediatRServiceConfiguration AddOpenBehaviors(IEnumerable openBehaviors) + { + foreach (var openBehavior in openBehaviors) + { + AddOpenBehavior(openBehavior.OpenBehaviorType!, openBehavior.ServiceLifetime); + } + + return this; + } + /// /// Register a closed stream behavior type /// diff --git a/src/MediatR/MicrosoftExtensionsDI/ServiceCollectionExtensions.cs b/src/MediatR/MicrosoftExtensionsDI/ServiceCollectionExtensions.cs index 6e211b27..3b7b31a1 100644 --- a/src/MediatR/MicrosoftExtensionsDI/ServiceCollectionExtensions.cs +++ b/src/MediatR/MicrosoftExtensionsDI/ServiceCollectionExtensions.cs @@ -1,8 +1,11 @@ using System; using System.Linq; using MediatR; +using MediatR.Licensing; using MediatR.Pipeline; using MediatR.Registration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -55,4 +58,19 @@ public static IServiceCollection AddMediatR(this IServiceCollection services, return services; } + + internal static void CheckLicense(this IServiceProvider serviceProvider) + { + if (LicenseChecked == false) + { + var licenseAccessor = serviceProvider.GetRequiredService(); + var licenseValidator = serviceProvider.GetRequiredService(); + var license = licenseAccessor.Current; + licenseValidator.Validate(license); + } + + LicenseChecked = true; + } + + internal static bool LicenseChecked { get; set; } } \ No newline at end of file diff --git a/src/MediatR/Pipeline/RequestExceptionActionProcessorBehavior.cs b/src/MediatR/Pipeline/RequestExceptionActionProcessorBehavior.cs index 9952b53c..2472e6b9 100644 --- a/src/MediatR/Pipeline/RequestExceptionActionProcessorBehavior.cs +++ b/src/MediatR/Pipeline/RequestExceptionActionProcessorBehavior.cs @@ -27,7 +27,7 @@ public async Task Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) { - var response = await next().ConfigureAwait(false); + var response = await next(cancellationToken).ConfigureAwait(false); foreach (var processor in _postProcessors) { diff --git a/src/MediatR/Pipeline/RequestPreProcessorBehavior.cs b/src/MediatR/Pipeline/RequestPreProcessorBehavior.cs index af09b048..1405c4ee 100644 --- a/src/MediatR/Pipeline/RequestPreProcessorBehavior.cs +++ b/src/MediatR/Pipeline/RequestPreProcessorBehavior.cs @@ -24,6 +24,6 @@ public async Task Handle(TRequest request, RequestHandlerDelegate(); + var concretions = new List(); var interfaces = new List(); var genericConcretions = new List(); var genericInterfaces = new List(); @@ -125,7 +126,7 @@ private static void ConnectImplementationsToTypesClosing(Type openRequestInterfa { genericConcretions.Add(type); foreach (var interfaceType in interfaceTypes) - { + { genericInterfaces.Fill(interfaceType); } } @@ -246,9 +247,9 @@ private static (Type Service, Type Implementation) GetConcreteRegistrationTypes( var combinations = GenerateCombinations(requestType, typesThatCanCloseForEachParameter, 0, cancellationToken); return combinations.Select(types => requestGenericTypeDefinition.MakeGenericType(types.ToArray())).ToList(); - } - - // Method to generate combinations recursively + } + + // Method to generate combinations recursively public static List> GenerateCombinations(Type requestType, List> lists, int depth = 0, CancellationToken cancellationToken = default) { if (depth == 0) @@ -308,8 +309,8 @@ private static void AddAllConcretionsThatClose(Type openRequestInterface, List GetConcreteRegistrationTypes(openRequestInterface, concreteRequest, concretion)); foreach (var (Service, Implementation) in registrationTypes) - { - cancellationToken.ThrowIfCancellationRequested(); + { + cancellationToken.ThrowIfCancellationRequested(); services.AddTransient(Service, Implementation); } } @@ -390,6 +391,10 @@ public static void AddRequiredServices(IServiceCollection services, MediatRServi services.TryAdd(new ServiceDescriptor(typeof(IMediator), serviceConfiguration.MediatorImplementationType, serviceConfiguration.Lifetime)); services.TryAdd(new ServiceDescriptor(typeof(ISender), sp => sp.GetRequiredService(), serviceConfiguration.Lifetime)); services.TryAdd(new ServiceDescriptor(typeof(IPublisher), sp => sp.GetRequiredService(), serviceConfiguration.Lifetime)); + + services.TryAddSingleton(serviceConfiguration); + services.TryAddSingleton(); + services.TryAddSingleton(); var notificationPublisherServiceDescriptor = serviceConfiguration.NotificationPublisherType != null ? new ServiceDescriptor(typeof(INotificationPublisher), serviceConfiguration.NotificationPublisherType, serviceConfiguration.Lifetime) diff --git a/src/MediatR/Wrappers/RequestHandlerWrapper.cs b/src/MediatR/Wrappers/RequestHandlerWrapper.cs index 1550ecf8..dc213560 100644 --- a/src/MediatR/Wrappers/RequestHandlerWrapper.cs +++ b/src/MediatR/Wrappers/RequestHandlerWrapper.cs @@ -34,14 +34,14 @@ public class RequestHandlerWrapperImpl : RequestHandlerWrap public override Task Handle(IRequest request, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - Task Handler() => serviceProvider.GetRequiredService>() - .Handle((TRequest) request, cancellationToken); + Task Handler(CancellationToken t = default) => serviceProvider.GetRequiredService>() + .Handle((TRequest) request, t == default ? cancellationToken : t); return serviceProvider .GetServices>() .Reverse() .Aggregate((RequestHandlerDelegate) Handler, - (next, pipeline) => () => pipeline.Handle((TRequest) request, next, cancellationToken))(); + (next, pipeline) => (t) => pipeline.Handle((TRequest) request, next, t == default ? cancellationToken : t))(); } } @@ -55,10 +55,10 @@ public class RequestHandlerWrapperImpl : RequestHandlerWrapper public override Task Handle(IRequest request, IServiceProvider serviceProvider, CancellationToken cancellationToken) { - async Task Handler() + async Task Handler(CancellationToken t = default) { await serviceProvider.GetRequiredService>() - .Handle((TRequest) request, cancellationToken); + .Handle((TRequest) request, t == default ? cancellationToken : t); return Unit.Value; } @@ -67,6 +67,6 @@ await serviceProvider.GetRequiredService>() .GetServices>() .Reverse() .Aggregate((RequestHandlerDelegate) Handler, - (next, pipeline) => () => pipeline.Handle((TRequest) request, next, cancellationToken))(); + (next, pipeline) => (t) => pipeline.Handle((TRequest) request, next, t == default ? cancellationToken : t))(); } } \ No newline at end of file diff --git a/src/MediatR/license.txt b/src/MediatR/license.txt new file mode 100644 index 00000000..8d5c9bce --- /dev/null +++ b/src/MediatR/license.txt @@ -0,0 +1,10 @@ +By accessing code under the [Lucky Penny Software GitHub Organization](https://github.com/LuckyPennySoftware) (Lucky Penny Software) here, you are agreeing to the following licensing terms. +If you do not agree to these terms, do not access Lucky Penny Software code. + +Your license to Lucky Penny Software source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here: + +https://opensource.org/license/rpl-1-5/ + +If you do not wish to release the source of software you build using Lucky Penny Software source code and/or binaries under the terms above, you may use Lucky Penny Software source code and/or binaries under the License Agreement described here: + +https://luckypennysoftware.com/license \ No newline at end of file diff --git a/test/MediatR.Tests/CreateStreamTests.cs b/test/MediatR.Tests/CreateStreamTests.cs index bbad3e3b..4139b75b 100644 --- a/test/MediatR.Tests/CreateStreamTests.cs +++ b/test/MediatR.Tests/CreateStreamTests.cs @@ -34,7 +34,7 @@ public async IAsyncEnumerable Handle(Ping request, [EnumeratorCancellation [Fact] public async Task Should_resolve_main_handler() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -66,7 +66,7 @@ public async Task Should_resolve_main_handler() [Fact] public async Task Should_resolve_main_handler_via_dynamic_dispatch() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -99,7 +99,7 @@ public async Task Should_resolve_main_handler_via_dynamic_dispatch() [Fact] public async Task Should_resolve_main_handler_by_specific_interface() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -130,7 +130,7 @@ public async Task Should_resolve_main_handler_by_specific_interface() [Fact] public void Should_raise_execption_on_null_request() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For().Use(); }); @@ -143,7 +143,7 @@ public void Should_raise_execption_on_null_request() [Fact] public void Should_raise_execption_on_null_request_via_dynamic_dispatch() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For().Use(); }); diff --git a/test/MediatR.Tests/ExceptionTests.cs b/test/MediatR.Tests/ExceptionTests.cs index 9b8e67a2..7bf81aff 100644 --- a/test/MediatR.Tests/ExceptionTests.cs +++ b/test/MediatR.Tests/ExceptionTests.cs @@ -71,7 +71,7 @@ public Task Handle(VoidNullPing request, CancellationToken cancellationToken) public ExceptionTests() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For().Use(); }); @@ -135,7 +135,7 @@ public async Task Should_not_throw_for_async_publish() [Fact] public async Task Should_throw_argument_exception_for_send_when_request_is_null() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -156,7 +156,7 @@ public async Task Should_throw_argument_exception_for_send_when_request_is_null( [Fact] public async Task Should_throw_argument_exception_for_void_send_when_request_is_null() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -177,7 +177,7 @@ public async Task Should_throw_argument_exception_for_void_send_when_request_is_ [Fact] public async Task Should_throw_argument_exception_for_publish_when_request_is_null() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -198,7 +198,7 @@ public async Task Should_throw_argument_exception_for_publish_when_request_is_nu [Fact] public async Task Should_throw_argument_exception_for_publish_when_request_is_null_object() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -219,7 +219,7 @@ public async Task Should_throw_argument_exception_for_publish_when_request_is_nu [Fact] public async Task Should_throw_argument_exception_for_publish_when_request_is_not_notification() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -253,7 +253,7 @@ public Task Handle(PingException request, CancellationToken cancellationToken) [Fact] public async Task Should_throw_exception_for_non_generic_send_when_exception_occurs() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -275,7 +275,7 @@ public async Task Should_throw_exception_for_non_generic_send_when_exception_occ [Fact] public async Task Should_throw_exception_for_non_request_send() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -302,7 +302,7 @@ public class NonRequest [Fact] public async Task Should_throw_exception_for_generic_send_when_exception_occurs() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/GenericRequestHandlerTests.cs b/test/MediatR.Tests/GenericRequestHandlerTests.cs index e6944e3e..18767f71 100644 --- a/test/MediatR.Tests/GenericRequestHandlerTests.cs +++ b/test/MediatR.Tests/GenericRequestHandlerTests.cs @@ -1,16 +1,4 @@ -using MediatR.Extensions.Microsoft.DependencyInjection.Tests; -using Microsoft.Extensions.DependencyInjection; -using Shouldly; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection.Emit; -using System.Reflection; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Xunit; -using System.Reflection.PortableExecutable; +using System.Linq; using MediatR.Tests.MicrosoftExtensionsDI; namespace MediatR.Tests @@ -98,7 +86,7 @@ public void ShouldNotRegisterDuplicateHandlers(int numberOfClasses, int numberOf public void ShouldThrowExceptionWhenTypesClosingExceedsMaximum() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); var assembly = GenerateTypesClosingExceedsMaximumAssembly(); @@ -117,7 +105,7 @@ public void ShouldThrowExceptionWhenTypesClosingExceedsMaximum() public void ShouldThrowExceptionWhenGenericHandlerRegistrationsExceedsMaximum() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); var assembly = GenerateHandlerRegistrationsExceedsMaximumAssembly(); @@ -136,7 +124,7 @@ public void ShouldThrowExceptionWhenGenericHandlerRegistrationsExceedsMaximum() public void ShouldThrowExceptionWhenGenericTypeParametersExceedsMaximum() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); var assembly = GenerateGenericTypeParametersExceedsMaximumAssembly(); @@ -155,7 +143,7 @@ public void ShouldThrowExceptionWhenGenericTypeParametersExceedsMaximum() public void ShouldThrowExceptionWhenTimeoutOccurs() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); var assembly = GenerateTimeoutOccursAssembly(); @@ -166,7 +154,7 @@ public void ShouldThrowExceptionWhenTimeoutOccurs() cfg.MaxGenericTypeParameters = 0; cfg.MaxGenericTypeRegistrations = 0; cfg.MaxTypesClosing = 0; - cfg.RegistrationTimeout = 1000; + cfg.RegistrationTimeout = 100; cfg.RegisterGenericHandlers = true; cfg.RegisterServicesFromAssembly(assembly); }); @@ -178,7 +166,7 @@ public void ShouldThrowExceptionWhenTimeoutOccurs() public void ShouldNotRegisterGenericHandlersWhenOptingOut() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); var assembly = GenerateOptOutAssembly(); services.AddMediatR(cfg => diff --git a/test/MediatR.Tests/GenericTypeConstraintsTests.cs b/test/MediatR.Tests/GenericTypeConstraintsTests.cs index 5a84f126..3d3da79b 100644 --- a/test/MediatR.Tests/GenericTypeConstraintsTests.cs +++ b/test/MediatR.Tests/GenericTypeConstraintsTests.cs @@ -89,7 +89,7 @@ public Task Handle(Ping request, CancellationToken cancellationToken) public GenericTypeConstraintsTests() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/GlobalUsings.cs b/test/MediatR.Tests/GlobalUsings.cs new file mode 100644 index 00000000..9ec9fd4a --- /dev/null +++ b/test/MediatR.Tests/GlobalUsings.cs @@ -0,0 +1,6 @@ +global using Microsoft.Extensions.DependencyInjection; +global using Shouldly; +global using System; +global using System.Threading; +global using System.Threading.Tasks; +global using Xunit; diff --git a/test/MediatR.Tests/Licensing/LicenseValidatorTests.cs b/test/MediatR.Tests/Licensing/LicenseValidatorTests.cs new file mode 100644 index 00000000..cd928be9 --- /dev/null +++ b/test/MediatR.Tests/Licensing/LicenseValidatorTests.cs @@ -0,0 +1,143 @@ +using System; +using System.Security.Claims; +using MediatR.Licensing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Shouldly; +using Xunit; +using License = MediatR.Licensing.License; + +namespace MediatR.Tests.Licensing; + +public class LicenseValidatorTests +{ + [Fact] + public void Should_return_invalid_when_no_claims() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License(); + + license.IsConfigured.ShouldBeFalse(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Warning); + } + + + [Fact] + public void Should_return_valid_when_community() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Community)), + new Claim("type", nameof(ProductType.Bundle))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages.ShouldNotContain(log => log.Level == LogLevel.Error + || log.Level == LogLevel.Warning + || log.Level == LogLevel.Critical); + } + + [Fact] + public void Should_return_invalid_when_not_correct_type() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddYears(1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(ProductType.AutoMapper))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Error); + } + + [Fact] + public void Should_return_invalid_when_expired() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var licenseValidator = new LicenseValidator(factory); + var license = new License( + new Claim("account_id", Guid.NewGuid().ToString()), + new Claim("customer_id", Guid.NewGuid().ToString()), + new Claim("sub_id", Guid.NewGuid().ToString()), + new Claim("iat", DateTimeOffset.UtcNow.AddYears(-1).ToUnixTimeSeconds().ToString()), + new Claim("exp", DateTimeOffset.UtcNow.AddDays(-1).ToUnixTimeSeconds().ToString()), + new Claim("edition", nameof(Edition.Professional)), + new Claim("type", nameof(ProductType.MediatR))); + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldContain(log => log.Level == LogLevel.Error); + } + + [Fact] + public void Should_return_valid_for_actual_valid_license() + { + var factory = new LoggerFactory(); + var provider = new FakeLoggerProvider(); + factory.AddProvider(provider); + + var config = new MediatRServiceConfiguration + { + LicenseKey = + "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxNzgxNTg2MDAwIiwiaWF0IjoiMTc1MDEwNDUyMiIsImFjY291bnRfaWQiOiJmMzQ4N2NhOWE5MDE0NWRlYmE4NGY4NDkwNDgxNWQ3NiIsImN1c3RvbWVyX2lkIjoiY3RtXzAxanhhcTVkcHNleHFmZmF0eDhkd3Ntd3Y2IiwiY29tcGFueSI6Ik15IFRlc3QgQ29tcGFueSIsInN1Yl9pZCI6InN1Yl8wMWp4eDVxZ3BnbTF0NDBhdDh2cGQzbm0zaCIsImVkaXRpb24iOiIyIiwidHlwZSI6IjEifQ.W-0ScVg5GxZ6R2ZcZfz8z5nnVAhEcMggnFLvyifm15ox9gei6xm6W4Wo1_RC75XqLzWyDqGp2lvgxucJqCDy3EpasDLADjyfRpqt14nZ81BnbjYgufERbfBRlX8i8O4ZfGg0BNb_nFNIP0XKuww4GGJ854HZOJds0CI31CH4JaghQkUSTaDaxGcrqb7K9RiWR90OhdkiUPBHk1p-EO2nogVFNothozEWKgCgVocvi9MguQBlJDC_e5Rg7c9XZdzTCkzwJAXAVdjoXaOvkPxTVSH09eOALuUXhi-FtKRzGVvVbqFVdEmiUDSPSs2ULeWz8GlfC1V33Wz2f3y69Lr9KA" + }; + var licenseAccessor = new LicenseAccessor(config, factory); + + var licenseValidator = new LicenseValidator(factory); + var license = licenseAccessor.Current; + + license.IsConfigured.ShouldBeTrue(); + + licenseValidator.Validate(license); + + var logMessages = provider.Collector.GetSnapshot(); + + logMessages + .ShouldNotContain(log => log.Level == LogLevel.Error); + } +} \ No newline at end of file diff --git a/test/MediatR.Tests/MediatR.Tests.csproj b/test/MediatR.Tests/MediatR.Tests.csproj index cff87cca..6f405ac3 100644 --- a/test/MediatR.Tests/MediatR.Tests.csproj +++ b/test/MediatR.Tests/MediatR.Tests.csproj @@ -1,22 +1,35 @@  - net8.0 + net9.0 + net481;net9.0 enable + $(NoWarn);CS8002; + false - + - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + all runtime; build; native; contentfiles; analyzers diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/AssemblyResolutionTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/AssemblyResolutionTests.cs index 6e27757e..b0574276 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/AssemblyResolutionTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/AssemblyResolutionTests.cs @@ -5,8 +5,8 @@ namespace MediatR.Extensions.Microsoft.DependencyInjection.Tests; using System; using System.Linq; using Shouldly; -using Xunit; - +using Xunit; + public class AssemblyResolutionTests { private readonly IServiceProvider _provider; @@ -14,11 +14,12 @@ public class AssemblyResolutionTests public AssemblyResolutionTests() { IServiceCollection services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); - services.AddMediatR(cfg => - { - cfg.RegisterServicesFromAssembly(typeof(Ping).Assembly); - cfg.RegisterGenericHandlers = true; + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssembly(typeof(Ping).Assembly); + cfg.RegisterGenericHandlers = true; }); _provider = services.BuildServiceProvider(); } @@ -64,12 +65,12 @@ public void ShouldRequireAtLeastOneAssembly() } [Fact] - public void ShouldResolveGenericVoidRequestHandler() - { + public void ShouldResolveGenericVoidRequestHandler() + { _provider.GetService>>().ShouldNotBeNull(); } - [Fact] + [Fact] public void ShouldResolveGenericReturnTypeRequestHandler() { _provider.GetService, string>>().ShouldNotBeNull(); diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/CustomMediatorTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/CustomMediatorTests.cs index 65eb3276..842bcd1c 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/CustomMediatorTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/CustomMediatorTests.cs @@ -14,6 +14,7 @@ public class CustomMediatorTests public CustomMediatorTests() { IServiceCollection services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); services.AddMediatR(cfg => { @@ -46,7 +47,7 @@ public void ShouldResolveNotificationHandlers() public void Can_Call_AddMediatr_multiple_times() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.MediatorImplementationType = typeof(MyCustomMediator); diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/DerivingRequestsTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/DerivingRequestsTests.cs index 4a8d57ec..eee16af0 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/DerivingRequestsTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/DerivingRequestsTests.cs @@ -14,6 +14,7 @@ public class DerivingRequestsTests public DerivingRequestsTests() { IServiceCollection services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining(typeof(Ping))); _provider = services.BuildServiceProvider(); diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/DuplicateAssemblyResolutionTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/DuplicateAssemblyResolutionTests.cs index 819b8f5d..7883aa96 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/DuplicateAssemblyResolutionTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/DuplicateAssemblyResolutionTests.cs @@ -14,7 +14,7 @@ public class DuplicateAssemblyResolutionTests public DuplicateAssemblyResolutionTests() { IServiceCollection services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(typeof(Ping).Assembly, typeof(Ping).Assembly)); _provider = services.BuildServiceProvider(); } diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/NotificationPublisherTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/NotificationPublisherTests.cs index 01fb3f3a..00482719 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/NotificationPublisherTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/NotificationPublisherTests.cs @@ -30,6 +30,7 @@ public async Task Publish(IEnumerable handlerExecut public void ShouldResolveDefaultPublisher() { var services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); services.AddMediatR(cfg => { @@ -51,7 +52,7 @@ public async Task ShouldSubstitutePublisherInstance() { var publisher = new MockPublisher(); var services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining(typeof(CustomMediatorTests)); @@ -72,7 +73,7 @@ public async Task ShouldSubstitutePublisherInstance() public async Task ShouldSubstitutePublisherServiceType() { var services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining(typeof(CustomMediatorTests)); @@ -98,7 +99,7 @@ public async Task ShouldSubstitutePublisherServiceType() public async Task ShouldSubstitutePublisherServiceTypeWithWhenAll() { var services = new ServiceCollection(); - services.AddSingleton(new Logger()); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining(typeof(CustomMediatorTests)); diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/PipeLineMultiCallToConstructorTest.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/PipeLineMultiCallToConstructorTest.cs index c46d14a5..6ef7b7f5 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/PipeLineMultiCallToConstructorTest.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/PipeLineMultiCallToConstructorTest.cs @@ -80,6 +80,7 @@ public async Task Should_not_call_constructor_multiple_times_when_using_a_pipeli IServiceCollection services = new ServiceCollection(); services.AddSingleton(output); + services.AddFakeLogging(); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ConstructorTestBehavior<,>)); services.AddMediatR(cfg => { diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/PipelineTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/PipelineTests.cs index c09b5ed9..f8c04223 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/PipelineTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/PipelineTests.cs @@ -1,14 +1,10 @@ using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; -using Xunit.Abstractions; namespace MediatR.Extensions.Microsoft.DependencyInjection.Tests; using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Pipeline; @@ -520,6 +516,7 @@ public void Should_pick_up_base_exception_behaviors() var output = new Logger(); IServiceCollection services = new ServiceCollection(); services.AddSingleton(output); + services.AddFakeLogging(); services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Ping).Assembly)); var provider = services.BuildServiceProvider(); @@ -946,6 +943,7 @@ public async Task Should_register_correctly() cfg.AddBehavior(); cfg.AddOpenBehavior(typeof(Open2Behavior<,>)); }); + services.AddFakeLogging(); var logger = new Logger(); services.AddSingleton(logger); services.AddSingleton(new MediatR.Tests.PipelineTests.Logger()); @@ -969,4 +967,97 @@ public async Task Should_register_correctly() "Invoked Handler", }); } + + + #region OpenBehaviorsForMultipleRegistration + sealed class OpenBehaviorMultipleRegistration0 : IPipelineBehavior + where TRequest : notnull + { + public OpenBehaviorMultipleRegistration0(IBlogger> logger) + { + this.logger = logger; + } + + readonly IBlogger> logger; + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + logger.Messages.Add("Invoked OpenBehaviorMultipleRegistration0"); + return next(); + } + } + sealed class OpenBehaviorMultipleRegistration1 : IPipelineBehavior + where TRequest : notnull + { + public OpenBehaviorMultipleRegistration1(IBlogger> logger) + { + this.logger = logger; + } + + readonly IBlogger> logger; + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + logger.Messages.Add("Invoked OpenBehaviorMultipleRegistration1"); + return next(); + } + } + sealed class OpenBehaviorMultipleRegistration2 : IPipelineBehavior + where TRequest : notnull + { + public OpenBehaviorMultipleRegistration2(IBlogger> logger) + { + this.logger = logger; + } + + readonly IBlogger> logger; + + public Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + logger.Messages.Add("Invoked OpenBehaviorMultipleRegistration2"); + return next(); + } + } + #endregion OpenBehaviorsForMultipleRegistration + + [Fact] + public async Task Should_register_open_behaviors_correctly() + { + var behaviorTypeList = new List + { + typeof(OpenBehaviorMultipleRegistration0<,>), + typeof(OpenBehaviorMultipleRegistration1<,>), + typeof(OpenBehaviorMultipleRegistration2<,>) + }; + var services = new ServiceCollection(); + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssemblyContaining(); + cfg.AddOpenBehaviors(behaviorTypeList); + }); + services.AddFakeLogging(); + var logger = new Logger(); + services.AddSingleton(logger); + services.AddSingleton(new MediatR.Tests.PipelineTests.Logger()); + services.AddSingleton(new MediatR.Tests.StreamPipelineTests.Logger()); + services.AddSingleton(new MediatR.Tests.SendTests.Dependency()); + services.AddSingleton(new System.IO.StringWriter()); + services.AddTransient(typeof(IBlogger<>), typeof(Blogger<>)); + var provider = services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateOnBuild = true + }); + + var mediator = provider.GetRequiredService(); + var request = new FooRequest(); + await mediator.Send(request); + + logger.Messages.ShouldBe(new[] + { + "Invoked OpenBehaviorMultipleRegistration0", + "Invoked OpenBehaviorMultipleRegistration1", + "Invoked OpenBehaviorMultipleRegistration2", + "Invoked Handler", + }); + } } \ No newline at end of file diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/StreamPipelineTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/StreamPipelineTests.cs index df801db7..9ca8fd1b 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/StreamPipelineTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/StreamPipelineTests.cs @@ -91,6 +91,7 @@ public async Task Should_register_and_wrap_with_behavior() var output = new Logger(); IServiceCollection services = new ServiceCollection(); services.AddSingleton(output); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssembly(typeof(Ping).Assembly); diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/TypeEvaluatorTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/TypeEvaluatorTests.cs index 311b9a9b..119fa723 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/TypeEvaluatorTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/TypeEvaluatorTests.cs @@ -18,6 +18,7 @@ public class TypeEvaluatorTests public TypeEvaluatorTests() { _services = new ServiceCollection(); + _services.AddFakeLogging(); _services.AddSingleton(new Logger()); _services.AddMediatR(cfg => { diff --git a/test/MediatR.Tests/MicrosoftExtensionsDI/TypeResolutionTests.cs b/test/MediatR.Tests/MicrosoftExtensionsDI/TypeResolutionTests.cs index 4438cafd..5465e55f 100644 --- a/test/MediatR.Tests/MicrosoftExtensionsDI/TypeResolutionTests.cs +++ b/test/MediatR.Tests/MicrosoftExtensionsDI/TypeResolutionTests.cs @@ -17,6 +17,7 @@ public class TypeResolutionTests public TypeResolutionTests() { IServiceCollection services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining(typeof(Ping))); _provider = services.BuildServiceProvider(); @@ -82,6 +83,7 @@ public void ShouldResolveIgnoreSecondDuplicateHandler() public void ShouldHandleKeyedServices() { IServiceCollection services = new ServiceCollection(); + services.AddFakeLogging(); services.AddSingleton(new Logger()); services.AddKeyedSingleton("Foo", "Foo"); services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining(typeof(Ping))); diff --git a/test/MediatR.Tests/NotificationPublisherTests.cs b/test/MediatR.Tests/NotificationPublisherTests.cs index caf6d84b..a355a896 100644 --- a/test/MediatR.Tests/NotificationPublisherTests.cs +++ b/test/MediatR.Tests/NotificationPublisherTests.cs @@ -34,6 +34,7 @@ public async Task Handle(Notification notification, CancellationToken cancellati public async Task Should_handle_sequentially_by_default() { var services = new ServiceCollection(); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblyContaining(); diff --git a/test/MediatR.Tests/Pipeline/RequestExceptionActionTests.cs b/test/MediatR.Tests/Pipeline/RequestExceptionActionTests.cs index c4e94f57..629859d7 100644 --- a/test/MediatR.Tests/Pipeline/RequestExceptionActionTests.cs +++ b/test/MediatR.Tests/Pipeline/RequestExceptionActionTests.cs @@ -1,12 +1,7 @@ -namespace MediatR.Tests.Pipeline; - -using System; -using System.Threading; -using System.Threading.Tasks; using MediatR.Pipeline; -using Shouldly; using Lamar; -using Xunit; + +namespace MediatR.Tests.Pipeline; public class RequestExceptionActionTests { @@ -99,7 +94,8 @@ public async Task Should_run_all_exception_actions_that_match_base_type() var pingExceptionAction = new PingExceptionAction(); var pongExceptionAction = new PongExceptionAction(); var pingPongExceptionAction = new PingPongExceptionAction(); - var container = new Container(cfg => + + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(_ => pingExceptionAction); @@ -123,7 +119,7 @@ public async Task Should_run_all_exception_actions_that_match_base_type() public async Task Should_run_matching_exception_actions_only_once() { var genericExceptionAction = new GenericExceptionAction(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(_ => genericExceptionAction); diff --git a/test/MediatR.Tests/Pipeline/RequestExceptionHandlerTests.cs b/test/MediatR.Tests/Pipeline/RequestExceptionHandlerTests.cs index 63ed413b..aef3c346 100644 --- a/test/MediatR.Tests/Pipeline/RequestExceptionHandlerTests.cs +++ b/test/MediatR.Tests/Pipeline/RequestExceptionHandlerTests.cs @@ -87,7 +87,7 @@ public Task Handle(Ping request, Exception exception, RequestExceptionHandlerSta [Fact] public async Task Should_run_exception_handler_and_allow_for_exception_not_to_throw() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(); @@ -106,7 +106,7 @@ public async Task Should_run_exception_handler_and_allow_for_exception_not_to_th [Fact] public async Task Should_run_exception_handler_and_allow_for_exception_to_be_still_thrown() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(); @@ -128,7 +128,7 @@ await Should.ThrowAsync(async () => [Fact] public async Task Should_run_exception_handler_and_unwrap_expections_thrown_in_the_handler() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(); @@ -149,7 +149,7 @@ await Should.ThrowAsync(async () => public async Task Should_run_matching_exception_handlers_only_once() { var genericPingExceptionHandler = new GenericPingExceptionHandler(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.For>().Use(); cfg.For>().Use(genericPingExceptionHandler); diff --git a/test/MediatR.Tests/Pipeline/RequestPostProcessorTests.cs b/test/MediatR.Tests/Pipeline/RequestPostProcessorTests.cs index f237bfc3..73a854e5 100644 --- a/test/MediatR.Tests/Pipeline/RequestPostProcessorTests.cs +++ b/test/MediatR.Tests/Pipeline/RequestPostProcessorTests.cs @@ -41,7 +41,7 @@ public Task Process(Ping request, Pong response, CancellationToken cancellationT [Fact] public async Task Should_run_postprocessors() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/Pipeline/RequestPreProcessorTests.cs b/test/MediatR.Tests/Pipeline/RequestPreProcessorTests.cs index 880d676b..a868b60f 100644 --- a/test/MediatR.Tests/Pipeline/RequestPreProcessorTests.cs +++ b/test/MediatR.Tests/Pipeline/RequestPreProcessorTests.cs @@ -41,7 +41,7 @@ public Task Process(Ping request, CancellationToken cancellationToken) [Fact] public async Task Should_run_preprocessors() { - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/Pipeline/Streams/StreamPipelineBehaviorTests.cs b/test/MediatR.Tests/Pipeline/Streams/StreamPipelineBehaviorTests.cs index 15a8145d..3145f320 100644 --- a/test/MediatR.Tests/Pipeline/Streams/StreamPipelineBehaviorTests.cs +++ b/test/MediatR.Tests/Pipeline/Streams/StreamPipelineBehaviorTests.cs @@ -1,4 +1,6 @@ using System.Threading; +using MediatR.Registration; +using Microsoft.Extensions.DependencyInjection; namespace MediatR.Tests.Pipeline.Streams; @@ -49,21 +51,17 @@ public async IAsyncEnumerable Handle(Sing request, StreamHandlerDelegate + var services = new ServiceCollection(); + services.AddFakeLogging(); + services.AddMediatR(opts => { - cfg.Scan(scanner => - { - scanner.AssemblyContainingType(typeof(PublishTests)); - scanner.IncludeNamespaceContainingType(); - scanner.WithDefaultConventions(); - scanner.AddAllTypesOf(typeof(IStreamRequestHandler<,>)); - scanner.AddAllTypesOf(typeof(IStreamPipelineBehavior<,>)); - }); - cfg.For(typeof(IStreamPipelineBehavior<,>)).Add(typeof(SingSongPipelineBehavior)); - cfg.For().Use(); + opts.RegisterServicesFromAssemblyContaining(); + opts.AddStreamBehavior(); }); - - var mediator = container.GetInstance(); + + var container = services.BuildServiceProvider(); + + var mediator = container.GetRequiredService(); var responses = mediator.CreateStream(new Sing { Message = "Sing" }); diff --git a/test/MediatR.Tests/PipelineTests.cs b/test/MediatR.Tests/PipelineTests.cs index 31241e5b..33a5b1b9 100644 --- a/test/MediatR.Tests/PipelineTests.cs +++ b/test/MediatR.Tests/PipelineTests.cs @@ -244,7 +244,7 @@ public class Logger public async Task Should_wrap_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -279,7 +279,7 @@ public async Task Should_wrap_with_behavior() public async Task Should_wrap_void_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -312,7 +312,7 @@ public async Task Should_wrap_void_with_behavior() public async Task Should_wrap_generics_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -349,7 +349,7 @@ public async Task Should_wrap_generics_with_behavior() public async Task Should_wrap_void_generics_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -385,7 +385,7 @@ public async Task Should_wrap_void_generics_with_behavior() public async Task Should_handle_constrained_generics() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -442,7 +442,7 @@ public async Task Should_handle_constrained_generics() public async Task Should_handle_concrete_and_open_generics() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/PublishTests.cs b/test/MediatR.Tests/PublishTests.cs index a9ca5873..1fae4b9c 100644 --- a/test/MediatR.Tests/PublishTests.cs +++ b/test/MediatR.Tests/PublishTests.cs @@ -54,7 +54,7 @@ public async Task Should_resolve_main_handler() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -82,7 +82,7 @@ public async Task Should_resolve_main_handler_when_object_is_passed() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -141,7 +141,7 @@ public async Task Should_override_with_sequential_firing() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -170,7 +170,7 @@ public async Task Should_override_with_sequential_firing_through_injection() var writer = new StringWriter(builder); var publisher = new SequentialPublisher(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -200,7 +200,7 @@ public async Task Should_resolve_handlers_given_interface() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -230,7 +230,7 @@ public async Task Should_resolve_main_handler_by_specific_interface() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/SendTests.cs b/test/MediatR.Tests/SendTests.cs index abbf7717..81b64285 100644 --- a/test/MediatR.Tests/SendTests.cs +++ b/test/MediatR.Tests/SendTests.cs @@ -1,112 +1,115 @@ -using System.Threading; - -using System; -using System.Threading.Tasks; -using Shouldly; -using Xunit; -using Microsoft.Extensions.DependencyInjection; +using System.Threading; + +using System; +using System.Threading.Tasks; +using Shouldly; +using Xunit; +using Microsoft.Extensions.DependencyInjection; using System.Reflection; - -namespace MediatR.Tests; -public class SendTests -{ +using MediatR.Pipeline; + +namespace MediatR.Tests; +public class SendTests +{ private readonly IServiceProvider _serviceProvider; - private Dependency _dependency; + private Dependency _dependency; private readonly IMediator _mediator; public SendTests() { _dependency = new Dependency(); var services = new ServiceCollection(); + services.AddFakeLogging(); services.AddMediatR(cfg => { cfg.RegisterServicesFromAssemblies(typeof(Ping).Assembly); + cfg.AddOpenBehavior(typeof(TimeoutBehavior<,>), ServiceLifetime.Transient); cfg.RegisterGenericHandlers = true; }); services.AddSingleton(_dependency); _serviceProvider = services.BuildServiceProvider(); _mediator = _serviceProvider.GetService()!; - } - - public class Ping : IRequest - { - public string? Message { get; set; } - } - - public class VoidPing : IRequest - { - } - - public class Pong - { - public string? Message { get; set; } - } - - public class PingHandler : IRequestHandler - { - public Task Handle(Ping request, CancellationToken cancellationToken) - { - return Task.FromResult(new Pong { Message = request.Message + " Pong" }); - } - } - - public class Dependency - { - public bool Called { get; set; } - public bool CalledSpecific { get; set; } - } - - public class VoidPingHandler : IRequestHandler - { - private readonly Dependency _dependency; - - public VoidPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(VoidPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - - return Task.CompletedTask; - } - } - - public class GenericPing : IRequest - where T : Pong - { - public T? Pong { get; set; } - } - - public class GenericPingHandler : IRequestHandler, T> - where T : Pong - { - private readonly Dependency _dependency; - - public GenericPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(GenericPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - request.Pong!.Message += " Pong"; - return Task.FromResult(request.Pong!); - } - } - - public class VoidGenericPing : IRequest - where T : Pong - { } - - public class VoidGenericPingHandler : IRequestHandler> - where T : Pong - { - private readonly Dependency _dependency; - public VoidGenericPingHandler(Dependency dependency) => _dependency = dependency; - - public Task Handle(VoidGenericPing request, CancellationToken cancellationToken) - { - _dependency.Called = true; - - return Task.CompletedTask; - } + } + + public class Ping : IRequest + { + public string? Message { get; set; } + } + + public class VoidPing : IRequest + { + } + + public class Pong + { + public string? Message { get; set; } + } + + public class PingHandler : IRequestHandler + { + public Task Handle(Ping request, CancellationToken cancellationToken) + { + return Task.FromResult(new Pong { Message = request.Message + " Pong" }); + } + } + + public class Dependency + { + public bool Called { get; set; } + public bool CalledSpecific { get; set; } + } + + public class VoidPingHandler : IRequestHandler + { + private readonly Dependency _dependency; + + public VoidPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(VoidPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + + return Task.CompletedTask; + } + } + + public class GenericPing : IRequest + where T : Pong + { + public T? Pong { get; set; } + } + + public class GenericPingHandler : IRequestHandler, T> + where T : Pong + { + private readonly Dependency _dependency; + + public GenericPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(GenericPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + request.Pong!.Message += " Pong"; + return Task.FromResult(request.Pong!); + } + } + + public class VoidGenericPing : IRequest + where T : Pong + { } + + public class VoidGenericPingHandler : IRequestHandler> + where T : Pong + { + private readonly Dependency _dependency; + public VoidGenericPingHandler(Dependency dependency) => _dependency = dependency; + + public Task Handle(VoidGenericPing request, CancellationToken cancellationToken) + { + _dependency.Called = true; + + return Task.CompletedTask; + } } public class PongExtension : Pong @@ -127,22 +130,22 @@ public Task Handle(VoidGenericPing request, CancellationToken can } } - public interface ITestInterface1 { } - public interface ITestInterface2 { } + public interface ITestInterface1 { } + public interface ITestInterface2 { } public interface ITestInterface3 { } public class TestClass1 : ITestInterface1 { } public class TestClass2 : ITestInterface2 { } public class TestClass3 : ITestInterface3 { } - public class MultipleGenericTypeParameterRequest : IRequest - where T1 : ITestInterface1 - where T2 : ITestInterface2 - where T3 : ITestInterface3 - { - public int Foo { get; set; } - } - + public class MultipleGenericTypeParameterRequest : IRequest + where T1 : ITestInterface1 + where T2 : ITestInterface2 + where T3 : ITestInterface3 + { + public int Foo { get; set; } + } + public class MultipleGenericTypeParameterRequestHandler : IRequestHandler, int> where T1 : ITestInterface1 where T2 : ITestInterface2 @@ -152,92 +155,141 @@ public class MultipleGenericTypeParameterRequestHandler : IRequestHa public MultipleGenericTypeParameterRequestHandler(Dependency dependency) => _dependency = dependency; - public Task Handle(MultipleGenericTypeParameterRequest request, CancellationToken cancellationToken) - { - _dependency.Called = true; - return Task.FromResult(1); - } - } - - [Fact] - public async Task Should_resolve_main_handler() - { - var response = await _mediator.Send(new Ping { Message = "Ping" }); - - response.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_void_handler() - { - await _mediator.Send(new VoidPing()); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_main_handler_via_dynamic_dispatch() - { - object request = new Ping { Message = "Ping" }; - var response = await _mediator.Send(request); - - var pong = response.ShouldBeOfType(); - pong.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_void_handler_via_dynamic_dispatch() - { - object request = new VoidPing(); - var response = await _mediator.Send(request); - - response.ShouldBeOfType(); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_main_handler_by_specific_interface() - { - var response = await _mediator.Send(new Ping { Message = "Ping" }); - - response.Message.ShouldBe("Ping Pong"); - } - - [Fact] - public async Task Should_resolve_main_handler_by_given_interface() - { - // wrap requests in an array, so this test won't break on a 'replace with var' refactoring - var requests = new IRequest[] { new VoidPing() }; - await _mediator.Send(requests[0]); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public Task Should_raise_execption_on_null_request() => Should.ThrowAsync(async () => await _mediator.Send(default!)); - - [Fact] - public async Task Should_resolve_generic_handler() + public Task Handle(MultipleGenericTypeParameterRequest request, CancellationToken cancellationToken) + { + _dependency.Called = true; + return Task.FromResult(1); + } + } + + public class TimeoutBehavior : IPipelineBehavior + where TRequest : notnull + { + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + using (var cts = new CancellationTokenSource(500)) + { + return await next(cts.Token); + } + } + } + + public class TimeoutRequest : IRequest + { + } + + public class TimeoutRequest2 : IRequest + { + } + + public class TimeoutRequestHandler : IRequestHandler + { + private readonly Dependency _dependency; + + public TimeoutRequestHandler(Dependency dependency) => _dependency = dependency; + + public async Task Handle(TimeoutRequest request, CancellationToken cancellationToken) + { + await Task.Delay(2000, cancellationToken); + + _dependency.Called = true; + } + } + + public class TimeoutRequest2Handler : IRequestHandler + { + private readonly Dependency _dependency; + + public TimeoutRequest2Handler(Dependency dependency) => _dependency = dependency; + + public async Task Handle(TimeoutRequest2 request, CancellationToken cancellationToken) + { + await Task.Delay(2000, cancellationToken); + + _dependency.Called = true; + return 1; + } + } + + [Fact] + public async Task Should_resolve_main_handler() + { + var response = await _mediator.Send(new Ping { Message = "Ping" }); + + response.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_void_handler() + { + await _mediator.Send(new VoidPing()); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_main_handler_via_dynamic_dispatch() + { + object request = new Ping { Message = "Ping" }; + var response = await _mediator.Send(request); + + var pong = response.ShouldBeOfType(); + pong.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_void_handler_via_dynamic_dispatch() + { + object request = new VoidPing(); + var response = await _mediator.Send(request); + + response.ShouldBeOfType(); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_main_handler_by_specific_interface() + { + var response = await _mediator.Send(new Ping { Message = "Ping" }); + + response.Message.ShouldBe("Ping Pong"); + } + + [Fact] + public async Task Should_resolve_main_handler_by_given_interface() + { + // wrap requests in an array, so this test won't break on a 'replace with var' refactoring + var requests = new IRequest[] { new VoidPing() }; + await _mediator.Send(requests[0]); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public Task Should_raise_execption_on_null_request() => Should.ThrowAsync(async () => await _mediator.Send(default!)); + + [Fact] + public async Task Should_resolve_generic_handler() { var request = new GenericPing { Pong = new Pong { Message = "Ping" } }; - var result = await _mediator.Send(request); - - var pong = result.ShouldBeOfType(); - pong.Message.ShouldBe("Ping Pong"); - - _dependency.Called.ShouldBeTrue(); - } - - [Fact] - public async Task Should_resolve_generic_void_handler() - { - var request = new VoidGenericPing(); - await _mediator.Send(request); - - _dependency.Called.ShouldBeTrue(); - } - + var result = await _mediator.Send(request); + + var pong = result.ShouldBeOfType(); + pong.Message.ShouldBe("Ping Pong"); + + _dependency.Called.ShouldBeTrue(); + } + + [Fact] + public async Task Should_resolve_generic_void_handler() + { + var request = new VoidGenericPing(); + await _mediator.Send(request); + + _dependency.Called.ShouldBeTrue(); + } + [Fact] public async Task Should_resolve_multiple_type_parameter_generic_handler() { @@ -245,9 +297,9 @@ public async Task Should_resolve_multiple_type_parameter_generic_handler() await _mediator.Send(request); _dependency.Called.ShouldBeTrue(); - } - - [Fact] + } + + [Fact] public async Task Should_resolve_closed_handler_if_defined() { var dependency = new Dependency(); @@ -268,9 +320,9 @@ public async Task Should_resolve_closed_handler_if_defined() dependency.Called.ShouldBeFalse(); dependency.CalledSpecific.ShouldBeTrue(); - } - - [Fact] + } + + [Fact] public async Task Should_resolve_open_handler_if_not_defined() { var dependency = new Dependency(); @@ -290,5 +342,31 @@ public async Task Should_resolve_open_handler_if_not_defined() dependency.Called.ShouldBeTrue(); dependency.CalledSpecific.ShouldBeFalse(); - } + } + + [Fact] + public async Task TimeoutBehavior_Void_Should_Cancel_Long_Running_Task_And_Throw_Exception() + { + var request = new TimeoutRequest(); + + var exception = await Should.ThrowAsync(() => _mediator.Send(request)); + + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); + _dependency.Called.ShouldBeFalse(); + } + + [Fact] + public async Task TimeoutBehavior_NonVoid_Should_Cancel_Long_Running_Task_And_Throw_Exception() + { + var request = new TimeoutRequest2(); + int result = 0; + + var exception = await Should.ThrowAsync(async () => { result = await _mediator.Send(request); }); + + exception.ShouldNotBeNull(); + exception.ShouldBeAssignableTo(); + _dependency.Called.ShouldBeFalse(); + result.ShouldBe(0); + } } \ No newline at end of file diff --git a/test/MediatR.Tests/SendVoidInterfaceTests.cs b/test/MediatR.Tests/SendVoidInterfaceTests.cs index 52bfb83d..ed5e537c 100644 --- a/test/MediatR.Tests/SendVoidInterfaceTests.cs +++ b/test/MediatR.Tests/SendVoidInterfaceTests.cs @@ -32,7 +32,7 @@ public async Task Should_resolve_main_void_handler() var builder = new StringBuilder(); var writer = new StringWriter(builder); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/ServiceFactoryTests.cs b/test/MediatR.Tests/ServiceFactoryTests.cs index 69d75542..dff293e3 100644 --- a/test/MediatR.Tests/ServiceFactoryTests.cs +++ b/test/MediatR.Tests/ServiceFactoryTests.cs @@ -2,6 +2,8 @@ using System.Collections; using System.Linq; using System.Threading.Tasks; +using MediatR.Licensing; +using MediatR.Registration; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -23,8 +25,11 @@ public class Pong public async Task Should_throw_given_no_handler() { var serviceCollection = new ServiceCollection(); + ServiceRegistrar.AddRequiredServices(serviceCollection, new MediatRServiceConfiguration()); + serviceCollection.AddFakeLogging(); var serviceProvider = serviceCollection.BuildServiceProvider(); + var mediator = new Mediator(serviceProvider); await Assert.ThrowsAsync( diff --git a/test/MediatR.Tests/StreamPipelineTests.cs b/test/MediatR.Tests/StreamPipelineTests.cs index 878ca18d..67242441 100644 --- a/test/MediatR.Tests/StreamPipelineTests.cs +++ b/test/MediatR.Tests/StreamPipelineTests.cs @@ -195,7 +195,7 @@ public class Logger public async Task Should_wrap_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -231,7 +231,7 @@ public async Task Should_wrap_with_behavior() public async Task Should_wrap_generics_with_behavior() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -269,7 +269,7 @@ public async Task Should_wrap_generics_with_behavior() public async Task Should_handle_constrained_generics() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { @@ -326,7 +326,7 @@ public async Task Should_handle_constrained_generics() public async Task Should_handle_concrete_and_open_generics() { var output = new Logger(); - var container = new Container(cfg => + var container = TestContainer.Create(cfg => { cfg.Scan(scanner => { diff --git a/test/MediatR.Tests/TestContainer.cs b/test/MediatR.Tests/TestContainer.cs new file mode 100644 index 00000000..2baa72de --- /dev/null +++ b/test/MediatR.Tests/TestContainer.cs @@ -0,0 +1,25 @@ +using Lamar; +using MediatR.Licensing; +using MediatR.Registration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace MediatR.Tests; + +public static class TestContainer +{ + public static Container Create(Action config) + { + Action configAction = cfg => + { + cfg.ForSingletonOf().Use(new NullLoggerFactory()); + + ServiceRegistrar.AddRequiredServices(cfg, new MediatRServiceConfiguration()); + + config(cfg); + }; + var container = new Container(configAction); + + return container; + } +} \ No newline at end of file