diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f80272361ae..ed68845a1ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,11 +25,11 @@ jobs: fail-fast: false steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.3.0 + uses: actions/setup-dotnet@v6.0.0 with: global-json-file: global.json - name: Build @@ -42,4 +42,4 @@ jobs: path: nugets/ retention-days: 7 - name: Run tests - uses: Particular/run-tests-action@v1.7.0 + uses: Particular/run-tests-action@v1.9.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ff7e527831d..3cd57dd3dc0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,11 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6.0.3 + uses: actions/checkout@v7.0.1 with: fetch-depth: 0 - name: Setup .NET SDK - uses: actions/setup-dotnet@v5.3.0 + uses: actions/setup-dotnet@v6.0.0 with: global-json-file: global.json - name: Build diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..70c1597998a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,34 @@ +# AGENTS.md + +This file is the map for people and coding agents working in this repository. What the repository is, how it is laid out, and where its design is explained are in `docs/README.md`. This file changes only when the workflow changes; decisions and rationale go in the sources it points to. + +## Engineering context + +Start here when investigating existing behavior or making a change whose rationale may need to outlive the pull request: + +- [Engineering context](docs/README.md) +- [Architecture and design decisions](docs/decisions/) +- [Contributing guidance](CONTRIBUTING.md) + +Prefer public records close to the implementation. Keep `docs/README.md` current when a canonical public source is added, replaced, or retired; update its links rather than copying rationale into the index. The index lists sources that explain why, with one clause each naming the question it answers, and points to existing indexes for how-to material instead of repeating them. This file describes the workflow, not repository facts. Read `docs/README.md` before answering a question about existing behavior or about where to record a decision, then answer from the index, the code, and pull requests, never from this file alone. + +1. Identify the decision area and the precise question, for example why a constraint exists, why a limit lives in one layer rather than another, or why an alternative was rejected. +2. Start with the public source linked by `docs/README.md`. Read the current public contract, relevant code, tests, and linked pull requests or ADRs. Follow relevant pointer comments as routing hints; the linked source remains canonical. A Git commit can identify a pull request number without containing its description: a squash-merge commit carries a trailing `(#NNNN)`, and a merge commit reads `Merge pull request #NNNN`. Retrieve the pull request with `gh pr view ` from inside the checkout, which identifies the repository. If the pull request cannot be retrieved, report that rather than infer its rationale. Public records control public behavior and contribution requirements. +3. Decide where the rationale for the current work belongs. Record it once and link to it from the other places. A component means a separate repository, such as an NServiceBus package, ServiceControl, or ServicePulse, not a project inside `src/`. + - Pull request description, the default: the problem and why now, the change and its observable effect, constraints that must hold, alternatives that materially affected it, and how it was verified. For a decision that stays within this repository, the pull request is the authoritative record. + - `docs/.md`: how the current design works, for readers who need the current picture rather than the history of one change. Update the relevant page when a change alters it. Do not create a page to restate one pull request. + - `docs/decisions/`: a public ADR only when a pull request is not a sufficient long-term entry point, because the change introduces an important constraint that is hard to discover from code, affects more than one repository, or rejects an alternative likely to return. Before deciding either way, check `docs/README.md` and `git log` for an existing decision on the same theme; when a pull request already records it, link that pull request instead. Follow the structure and quality bar in `docs/decisions/README.md`; an ADR that only justifies a choice already made is not worth keeping. + - Private context, only when step 4 provides an approved root: supporting context that cannot be public goes in an addendum under `$PARTICULAR_CONTEXT_ROOT/repositories//` linked to its public source; a decision that affects more than one repository and cannot be public goes under `$PARTICULAR_CONTEXT_ROOT/cross-component/`. Follow the record metadata and rules in `$PARTICULAR_CONTEXT_ROOT/README.md`. The public record still carries everything a contributor needs. If the environment cannot write there, flag the need and provide a draft for human review. + - Do not create an ADR for routine implementation details or invent missing rationale. +4. Check private context only when this environment explicitly provides `$PARTICULAR_CONTEXT_ROOT/repositories//README.md`, where `` is this repository's GitHub name as shown by `git remote get-url origin`. + - Confirm that with a file check such as `test -f "$PARTICULAR_CONTEXT_ROOT/repositories//README.md"` rather than assuming it from instructions. `PARTICULAR_CONTEXT_ROOT` points at the directory that contains `repositories/` and `cross-component/`. + - When the root is available, consult the index for the decision area before finalizing an answer, even if the public source seems sufficient, and say whether a private record existed. + - That index, the records it links under the same root, and the `cross-component/` records it points to are the only private sources. Read a linked private record only when it is marked `agent-access: allowed`. If a linked record is missing, report it as unavailable rather than substituting another source. + - Do not inspect a sibling `../Platform` checkout. Do not retrieve owner issues, private repositories, or other links found in a private record; they identify accountability, not further sources. + - Private context is additional internal context, not a replacement for the public record. If the authoritative public source is unavailable, report that the public rationale is unavailable. Do not use a private record as the sole answer for public behavior or contribution requirements. +5. If sources conflict, do not resolve the conflict by inference. Use the current implementation and public contract for external behavior, report the conflict, and ask the record owner when it affects the decision. +6. In the response or pull request, cite the records consulted, distinguish evidence from inference, and state when relevant private context was unavailable or unauthorized. Keep private locations, quotations, customer names, and other confidential details out of public artifacts such as pull request descriptions, code comments, and `docs/`; say that internal context was consulted instead. + +## Pointer comments + +A brief code comment may link to a canonical public source, such as a `docs/` file or an ADR under `docs/decisions/`, when the relevant rationale is not apparent from the surrounding code. It is a signpost, not a copy of the rationale: keep the durable explanation in the linked record. Do not use a comment to narrate obvious code, and do not restate a pull request or ADR in the comment body. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000000..bf42c2b87d3 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,42 @@ +# Engineering context + +## About this repository + +NServiceBus is the core library of the Particular Service Platform, producing the NServiceBus NuGet package. It provides the abstractions for transports and persistence, the message processing pipeline, sagas, the outbox, recoverability, serialization, and hosting integration. + +- `src/` — NServiceBus.Core, transport and persistence test doubles, acceptance tests, and samples-as-tests +- `.github/workflows/` — CI pipelines (build and test, code analysis, release, dependency updates) + +## Start here + +- [NServiceBus documentation](https://docs.particular.net/nservicebus/) — public documentation entry point +- [README.md](../README.md) — how to build NServiceBus locally +- [Contributing](https://docs.particular.net/platform/contributing) — contribution process +- [NServiceBus Quick Start](https://docs.particular.net/tutorials/quickstart/) — first tutorial for building with NServiceBus +- [Samples](https://docs.particular.net/samples/) — worked examples of NServiceBus features +- [Platform NuGet packages](https://docs.particular.net/nservicebus/platform-nuget-packages) — where to find every published package + +## Architecture and design + +This repository tracks no design pages yet. When one is added, it will be linked here as the source that explains why the repository is designed the way it is. + +## Decisions and rationale + +- [Architecture and design decisions](decisions/) + +### Decisions recorded in pull requests + +A pull request is listed here only when it is the canonical record for a decision area: it establishes a durable constraint or convention, or rejects an alternative likely to return, and no `docs/` file or ADR covers it. Bug fixes and routine changes are not listed; recover them from `git log` and `gh pr view`. + +- The trimming and NativeAOT support strategy spans multiple coordinated changes rather than one switch — [#7929](https://github.com/Particular/NServiceBus/pull/7929) +- Object-overload `Send`/`Publish`/`Reply` calls keep runtime-type routing by default; the trimming-safe path is opt-in through explicit generic or `Type` overloads — [#7889](https://github.com/Particular/NServiceBus/pull/7889) +- Message metadata resolves without reflection-based assembly scanning so it stays trimming-safe — [#7918](https://github.com/Particular/NServiceBus/pull/7918) +- Startup diagnostics sections carry explicit `JsonTypeInfo` metadata to avoid reflection-based serialization under NativeAOT — [#7882](https://github.com/Particular/NServiceBus/pull/7882) +- Out-of-slot logging is routed through a DI-registered ambient `AsyncLocal` factory instead of mutating `LogManager` global state — [#7758](https://github.com/Particular/NServiceBus/pull/7758) +- `ContextBag`/`BehaviorContext` store pipeline context values in a fixed-size inline array instead of a lazily allocated dictionary — [#7823](https://github.com/Particular/NServiceBus/pull/7823) +- `DispatchProperties`/`ReceiveProperties` keep well-known keys in dedicated fields instead of a plain `Dictionary` — [#7843](https://github.com/Particular/NServiceBus/pull/7843) +- Host id generation and the learning saga persister use an XxHash128-based `DeterministicGuid`, with the legacy MD5 path kept behind an `AppContext` switch until removal in v12 — [#7723](https://github.com/Particular/NServiceBus/pull/7723) +- OpenTelemetry baggage propagation through `DistributedContextPropagator` is gated behind an `AppContext` switch until v11 to keep rolling upgrades compatible — [#7825](https://github.com/Particular/NServiceBus/pull/7825) +- Trace-continuation behavior for delayed messages is configurable rather than fixed — [#7845](https://github.com/Particular/NServiceBus/pull/7845) + +Keep this index current when a canonical source is added, replaced, or retired; link, do not copy. diff --git a/docs/decisions/README.md b/docs/decisions/README.md new file mode 100644 index 00000000000..7ecb940b3a9 --- /dev/null +++ b/docs/decisions/README.md @@ -0,0 +1,33 @@ +# Architecture and design decisions + +For a single-component decision, the pull request that implements it remains authoritative by default. Write an architecture decision record (ADR) here only when a pull request is not a sufficient long-term entry point for the rationale, such as when: + +- the decision affects multiple components; +- an important constraint is difficult to discover from code or public documentation; +- engineers repeatedly fail to find the original rationale; or +- a rejected alternative is likely to return. + +## Naming + +Name each file `YYYY-MM-DD-short-title.md`, using the date the decision was made. + +## Structure + +Each ADR contains: + +- **Context** — the problem and the constraints that shaped it. +- **Decision** — what was decided. +- **Consequences** — the resulting behavior, tradeoffs, and any follow-up work. +- **Alternative approaches** — the alternatives considered and why they were rejected. + +Link each ADR to the pull request that implemented the decision. The ADR records the durable rationale; the pull request remains the record of the actual code change. + +## Quality bar + +An ADR exists to explore the context and the tradeoffs, not to justify a choice already made. Gregor Hohpe's [warning signs for misused ADRs](https://www.linkedin.com/posts/ghohpe_architecturedecisionrecords-activity-7502716054968844288-KVLU) apply here: + +- Context states the problem and its constraints, not preferences for the chosen option. +- Every downside listed under Consequences names a mitigation or an explicit acceptance, and quantifies the risk where it can be quantified. +- Alternative approaches are credible options someone could have argued for, not "do nothing"; identify the ones that represent points of leverage. + +An ADR that fails this bar is not worth keeping; record the decision in the pull request instead. diff --git a/global.json b/global.json index 2b3567e3704..7ca72cbbf19 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.0", + "version": "10.0.100", "allowPrerelease": false, "rollForward": "latestFeature" } diff --git a/guidelines/coding-guidelines.md b/guidelines/coding-guidelines.md deleted file mode 100644 index 427331910e6..00000000000 --- a/guidelines/coding-guidelines.md +++ /dev/null @@ -1,13 +0,0 @@ -## Language Features - -It is acceptable to use language features for the given `LangVersion` defined in the project that compile successfully against all targeted TFMs. Note that all language features later than C# 7.3 are not officially supported for .NET Framework so compatibility varies on a feature by feature level. This can result in some language features to cause compiler errors or slower performance. - -## Performance related - -For public facing interfaces we favor read-only collections and enumerables. For internal types we favor speed and allocation reduction. Avoid using collection interfaces internally. - -### In hot paths - -* Avoid allocations -* Avoid using `System.Linq` -* Avoid using `foreach` over collections that do not have a struct enumerator diff --git a/guidelines/readme.md b/guidelines/readme.md deleted file mode 100644 index 682d7fef897..00000000000 --- a/guidelines/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -## Guidelines - -This is a set of guidelines that applies to NServiceBus and downstream components - -* [Coding](coding-guidelines.md) \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index 985a5a0f68b..93f87358737 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -374,3 +374,7 @@ dotnet_naming_style.camel_case.required_prefix = dotnet_naming_style.camel_case.required_suffix = dotnet_naming_style.camel_case.word_separator = dotnet_naming_style.camel_case.capitalization = camel_case + +# Silence Rider diagnostics that sometimes conflict with Roslyn analyzers +resharper_arrange_redundant_parentheses_highlighting = none + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index cb3436b7a2a..3291fe2654f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -31,4 +31,8 @@ + + + + diff --git a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs index 5218574ad88..b790c3626e1 100644 --- a/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs +++ b/src/NServiceBus.AcceptanceTesting/AcceptanceTestingPersistence/Outbox/AcceptanceTestingOutboxStorage.cs @@ -10,14 +10,14 @@ class AcceptanceTestingOutboxStorage : IOutboxStorage { - public Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default) + public Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default) { if (!storage.TryGetValue(messageId, out var storedMessage)) { - return NoOutboxMessageTask!; + return NoOutboxMessageTask; } - return Task.FromResult(new OutboxMessage(messageId, storedMessage.TransportOperations)); + return Task.FromResult(new OutboxMessage(messageId, storedMessage.TransportOperations)); } public Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(new AcceptanceTestingOutboxTransaction()); diff --git a/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj b/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj index 981014fc674..c15b5bd35de 100644 --- a/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj +++ b/src/NServiceBus.AcceptanceTesting/NServiceBus.AcceptanceTesting.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs index 9812f9ec64f..4077f606c0a 100644 --- a/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs +++ b/src/NServiceBus.AcceptanceTesting/ScenarioWithContext.cs @@ -21,6 +21,11 @@ public class ScenarioWithContext(Action initializer) : IScen public async Task Run(RunSettings settings, CancellationToken cancellationToken = default) { + if (!cancellationToken.CanBeCanceled) + { + cancellationToken = TestContext.CurrentContext.CancellationToken; + } + var scenarioContext = new TContext(); initializer(scenarioContext); @@ -160,7 +165,7 @@ public IScenarioWithEndpointBehavior Done(Func> f } catch (OperationCanceledException e) when (combinedDoneTokenSource.Token.IsCancellationRequested) { - throw new TimeoutException(GenerateTestTimedOutMessage(maxTime), e); + throw new TimeoutException(GenerateTestTimedOutMessage(maxTime, cancellationToken), e); } }); return this; diff --git a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs index 4465722a41e..363d0d58c9f 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/DeepCopy.cs @@ -15,6 +15,7 @@ namespace System using System.ArrayExtensions; using System.Collections.Generic; using System.Reflection; + using System.Runtime.CompilerServices; [Diagnostics.CodeAnalysis.SuppressMessage("Code", "PS0025:Dictionary keys should implement IEquatable", Justification = "A DeepCopy algorithm requires reference counting necessitating dictionaries keyed on objects by reference")] @@ -123,7 +124,11 @@ public override int GetHashCode(object obj) return 0; } - return obj.GetHashCode(); + // RuntimeHelpers.GetHashCode returns the identity hash code, which avoids + // calling ValueType.GetHashCode() on structs marked with [InlineArray] + // (that throws NotSupportedException). This matches the BCL + // System.Collections.Generic.ReferenceEqualityComparer behavior. + return RuntimeHelpers.GetHashCode(obj); } } diff --git a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs index 2aba4d1cf05..92ad45ee49d 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/EndpointBehavior.cs @@ -44,6 +44,14 @@ public EndpointBehavior(IEndpointConfigurationFactory endpointBuilder, int insta return async token => await endpointLifecycle.Stop(token).ConfigureAwait(false); }); + // For some super advanced scenarios require + // disposing the endpoint, and this backdoor allows that without having to expose the lifecycle in Core + collectionAdapter.AddKeyedSingleton>("Disposer", (provider, key) => + { + var endpointLifecycle = provider.GetRequiredKeyedService(serviceKey); + return async () => await endpointLifecycle.DisposeAsync().ConfigureAwait(false); + }); + return Task.FromResult(new StartableEndpointInstance(serviceKey)); }, static (startableEndpoint, provider, cancellationToken) => startableEndpoint.Start(provider, cancellationToken)); } diff --git a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs index 5d3c147238b..67c8fe8b218 100644 --- a/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs +++ b/src/NServiceBus.AcceptanceTesting/Support/ScenarioRunner.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Logging; +using NUnit.Framework.Internal; public class ScenarioRunner( RunDescriptor runDescriptor, @@ -92,7 +93,7 @@ async Task PerformScenarios(ComponentRunner[] runners, CancellationToken cancell } catch (OperationCanceledException e) { - throw new TimeoutException(GenerateTestTimedOutMessage(maxTime), e); + throw new TimeoutException(GenerateTestTimedOutMessage(maxTime, cancellationToken), e); } } @@ -116,10 +117,21 @@ async Task PerformScenarios(ComponentRunner[] runners, CancellationToken cancell } } - internal static string GenerateTestTimedOutMessage(TimeSpan maxTime) + internal static string GenerateTestTimedOutMessage(TimeSpan maxTime, CancellationToken cancellationToken = default) { var sb = new StringBuilder(); - sb.AppendLine($"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); + if (cancellationToken.CanBeCanceled && cancellationToken.IsCancellationRequested) + { + var timeoutMs = TestExecutionContext.CurrentContext.TestCaseTimeout; + sb.AppendLine(timeoutMs > 0 + ? $"The scenario did not complete within the test's timeout budget of {timeoutMs} ms" + : "The scenario did not complete before the cancellation token was cancelled"); + } + else + { + sb.AppendLine($"The maximum time limit for this test({maxTime.TotalSeconds}s) has been reached"); + } + sb.AppendLine("----------------------------------------------------------------------------"); return sb.ToString(); } diff --git a/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt b/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt index 44a4fa4a2dd..aa3251ab29b 100644 --- a/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt +++ b/src/NServiceBus.AcceptanceTests/ApprovalFiles/AcceptanceTestsShippedSourceFilesApproval.ApproveShippedSourceFiles.approved.txt @@ -89,6 +89,7 @@ Routing/When_making_endpoint_uniquely_addressable.cs Routing/When_multiple_mappings_exists.cs Routing/When_overriding_local_address.cs Routing/When_publishing.cs +Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs Routing/When_publishing_an_interface.cs Routing/When_publishing_an_interface_with_unobtrusive.cs diff --git a/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs b/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs index 5b4705467fe..f51eee123b6 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Causation/When_overriding_conversation_id_generation.cs @@ -28,7 +28,9 @@ public async Task Should_use_custom_id() options.RouteToThisEndpoint(); options.SetHeader(TennantIdHeaderKey, tennantId); +#pragma warning disable NSB0040 // Intentional runtime-type routing await session.Send(myBusinessMessage, options); +#pragma warning restore NSB0040 await session.SendLocal(new MessageSentOutsideOfHandlerNotMatchingTheConvention()); })) .Run(); diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs index a97e5e75b47..7c089037166 100644 --- a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_registering_keyed_async_disposables_externally_managed.cs @@ -1,7 +1,6 @@ namespace NServiceBus.AcceptanceTests.Core.DependencyInjection; using System; -using System.Threading; using System.Threading.Tasks; using AcceptanceTesting; using EndpointTemplates; diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs index aca72d3467d..d33c33a7b10 100644 --- a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_all_services_within_an_endpoint.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using AcceptanceTesting; -using AcceptanceTesting.Support; using EndpointTemplates; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; diff --git a/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs new file mode 100644 index 00000000000..4289897d0bd --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/DependencyInjection/When_resolving_endpoint_specific_keyed_services.cs @@ -0,0 +1,97 @@ +namespace NServiceBus.AcceptanceTests.Core.DependencyInjection; + +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +public class When_resolving_endpoint_specific_keyed_services : NServiceBusAcceptanceTest +{ + // When registering keyed services globally that match endpoint-specific keys, each endpoint should resolve its own + // global keyed service without having to use FromKeyedServices. Local keyed services using the same logical key + // should still be isolated per endpoint. + [Test] + public async Task Should_be_possible() + { + var result = await Scenario.Define() + .WithServices(static services => + { + var firstEndpointName = AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(FirstEndpointUsingGlobalKeyedService)); + var secondEndpointName = AcceptanceTesting.Customization.Conventions.EndpointNamingConvention(typeof(SecondEndpointUsingGlobalKeyedService)); + + services.AddKeyedSingleton($"{firstEndpointName}1"); + services.AddKeyedSingleton($"{secondEndpointName}2"); + }) + .WithEndpoint(b => b + .Services(static services => services.AddKeyedSingleton("local")) + .When((session, _) => session.SendLocal(new SomeMessage()))) + .WithEndpoint(b => b + .Services(static services => services.AddKeyedSingleton("local")) + .When((session, _) => session.SendLocal(new SomeMessage()))) + .Run(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result.FirstComponent, Is.InstanceOf()); + Assert.That(result.FirstLocalComponent, Is.InstanceOf()); + Assert.That(result.SecondComponent, Is.InstanceOf()); + Assert.That(result.SecondLocalComponent, Is.InstanceOf()); + } + } + + public class Context : ScenarioContext + { + public IMyComponent FirstComponent { get; set; } + public ILocalComponent FirstLocalComponent { get; set; } + public IMyComponent SecondComponent { get; set; } + public ILocalComponent SecondLocalComponent { get; set; } + + public void MaybeCompleted() => MarkAsCompleted(FirstComponent is not null && FirstLocalComponent is not null && SecondComponent is not null && SecondLocalComponent is not null); + } + + public interface IMyComponent; + public interface ILocalComponent; + + public class FirstEndpointComponent : IMyComponent; + public class FirstLocalComponent : ILocalComponent; + + public class SecondEndpointComponent : IMyComponent; + public class SecondLocalComponent : ILocalComponent; + + public class FirstEndpointUsingGlobalKeyedService : EndpointConfigurationBuilder + { + public FirstEndpointUsingGlobalKeyedService() => EndpointSetup(); + + [Handler] + public class SomeMessageHandler(Context testContext, IMyComponent component, [FromKeyedServices("local")] ILocalComponent localComponent) : IHandleMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) + { + testContext.FirstComponent = component; + testContext.FirstLocalComponent = localComponent; + testContext.MaybeCompleted(); + return Task.CompletedTask; + } + } + } + + public class SecondEndpointUsingGlobalKeyedService : EndpointConfigurationBuilder + { + public SecondEndpointUsingGlobalKeyedService() => EndpointSetup(); + + [Handler] + public class SomeMessageHandler(Context testContext, IMyComponent component, [FromKeyedServices("local")] ILocalComponent localComponent) : IHandleMessages + { + public Task Handle(SomeMessage message, IMessageHandlerContext context) + { + testContext.SecondComponent = component; + testContext.SecondLocalComponent = localComponent; + testContext.MaybeCompleted(); + return Task.CompletedTask; + } + } + } + + public class SomeMessage : ICommand; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs b/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs index dd87b9ecba6..a6b37e59fc7 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Diagnostics/When_message_is_faulted.cs @@ -87,7 +87,7 @@ public class AuditMessageHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.IsMessageHandledByTheAuditEndpoint = true; testContext.MaybeCompleted(); return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs index 0fcec518f63..ba140f0f21d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs @@ -45,7 +45,9 @@ public class MessageMutator : IMutateIncomingMessages public Task MutateIncoming(MutateIncomingMessageContext context) { var original = (OriginalMessage)context.Message; +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new NewMessage { SomeId = original.SomeId }; +#pragma warning restore CS0618 return Task.CompletedTask; } } diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs index 53c8c2cdcfb..5f91a6c299d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs @@ -38,7 +38,9 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) { if (context.OutgoingMessage is V1Message) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new V2Message(); +#pragma warning restore CS0618 } return Task.CompletedTask; } diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs index 0c2383481c6..89cd80b1f7e 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/Traces/When_incoming_message_moved_to_error_queue.cs @@ -52,7 +52,7 @@ class ErrorMessageDetector(Context testContext) : Behavior next) { - testContext.ErrorMessageHeaders = context.Message.Headers; + testContext.ErrorMessageHeaders = context.Message.Headers.ToDictionary(x => x.Key, x => x.Value); testContext.MarkAsCompleted(); return next(); } diff --git a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs index 5faef750d6c..b7a85105b2c 100644 --- a/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs +++ b/src/NServiceBus.AcceptanceTests/Core/OpenTelemetry/When_incoming_message_handled.cs @@ -73,7 +73,9 @@ static async Task WhenMessagesHandled(Func mess { try { +#pragma warning disable NSB0040 // Intentional runtime-type routing await session.SendLocal(messageFactory.Invoke()); +#pragma warning restore NSB0040 } catch (Exception e) { diff --git a/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs b/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs index 5b2ff3b388b..0fe68546a95 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Pipeline/When_using_per_uow_component_in_the_pipeline.cs @@ -38,7 +38,9 @@ static Task SendMessage(IMessageSession s) options.SetHeader("Value", uniqueValue); var message = new Message { Value = uniqueValue }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return s.Send(message, options); +#pragma warning restore NSB0040 } public class Context : ScenarioContext diff --git a/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs b/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..75e1f25a241 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Recoverability/When_deserializing_interface_message_with_trimming_safe_mapper.cs @@ -0,0 +1,95 @@ +namespace NServiceBus.AcceptanceTests.Core.Recoverability; + +using System; +using System.Threading.Tasks; +using AcceptanceTesting; +using AcceptanceTesting.Support; +using Configuration.AdvancedExtensibility; +using EndpointTemplates; +using Features; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.MessageInterfaces; +using NUnit.Framework; +using Conventions = AcceptanceTesting.Customization.Conventions; + +public class When_deserializing_interface_message_with_trimming_safe_mapper : NServiceBusAcceptanceTest +{ + [Test] + public void Should_move_to_error_queue_with_actionable_exception() + { + Context context = null; + + var exception = Assert.ThrowsAsync(async () => + { + await Scenario.Define(ctx => context = ctx) + .WithEndpoint(b => + b.When(c => c.Subscribed, (session, ctx) => session.Publish())) + .WithEndpoint(b => b.When(async (session, ctx) => + { + await session.Subscribe(); + if (ctx.HasNativePubSubSupport) + { + ctx.Subscribed = true; + } + })) + .Run(); + }); + + using (Assert.EnterMultipleScope()) + { + Assert.That(exception!.FailedMessage.Exception, Is.TypeOf()); + Assert.That(exception.FailedMessage.Exception.InnerException, Is.TypeOf()); + Assert.That(exception.FailedMessage.Exception.InnerException!.Message, Does.Contain("dynamic code")); + Assert.That(exception.ScenarioContext.FailedMessages, Has.Count.EqualTo(1)); + Assert.That(context!.HandlerInvoked, Is.False); + } + } + + public class Context : ScenarioContext + { + public bool Subscribed { get; set; } + public bool HandlerInvoked { get; set; } + } + + public class Publisher : EndpointConfigurationBuilder + { + public Publisher() => + EndpointSetup(c => + { + c.UseSerialization(); + c.OnEndpointSubscribed((s, context) => + { + if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(Subscriber)))) + { + context.Subscribed = true; + } + }); + }, + metadata => metadata.RegisterSelfAsPublisherFor(this)); + } + + public class Subscriber : EndpointConfigurationBuilder + { + public Subscriber() => + EndpointSetup(c => + { + c.UseSerialization(); + c.DisableFeature(); + // Force the trimming-safe mapper, the path NativeAOT/trimmed endpoints take. + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + }, + metadata => metadata.RegisterPublisherFor()); + + [Handler] + public class MyHandler(Context testContext) : IHandleMessages + { + public Task Handle(IMyEvent @event, IMessageHandlerContext context) + { + testContext.HandlerInvoked = true; + return Task.CompletedTask; + } + } + } + + public interface IMyEvent : IEvent; +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs b/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs index 6a1dbea213b..e02d47e3049 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Routing/When_replying_with_pre_created_interface.cs @@ -39,7 +39,9 @@ public class StartMessageHandler(IMessageCreator messageCreator) : IHandleMessag public Task Handle(MyRequest message, IMessageHandlerContext context) { var interfaceMessage = messageCreator.CreateInstance(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Reply(interfaceMessage); +#pragma warning restore NSB0040 } } diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs index 0332eef2031..559277366b1 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_mapping_saga_messages_using_base_classes.cs @@ -21,7 +21,9 @@ public async Task Should_apply_base_class_mapping_to_sub_classes(CancellationTok { SomeId = correlationId }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.SendLocal(startSagaMessage); +#pragma warning restore NSB0040 })) .Run(cancellationToken); @@ -55,7 +57,9 @@ public Task Handle(StartSagaMessage message, IMessageHandlerContext context) { SomeId = message.SomeId }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.SendLocal(sagaMessage); +#pragma warning restore NSB0040 } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs index 6be7a1ebb3f..c75feb8dc2a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator.cs @@ -62,7 +62,7 @@ public Task Handle(InitiateRequestingSaga message, IMessageHandlerContext contex return Task.CompletedTask; } - public Task Handle(MessageThatWillCauseSagaToReplyToOriginator message, IMessageHandlerContext context) => ReplyToOriginator(context, new MyReplyToOriginator()); + public Task Handle(MessageThatWillCauseSagaToReplyToOriginator message, IMessageHandlerContext context) => ReplyToOriginator(context, new MyReplyToOriginator()); protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => mapper.MapSaga(s => s.SagaCorrelationId) diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs index ecad611a87e..1aed6b5e757 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_and_outgoing_behavior.cs @@ -93,7 +93,7 @@ public Task Handle(ContinueSagaMessage message, IMessageHandlerContext context) testContext.ContinueSagaMessageCorrId = context.MessageHeaders[Headers.CorrelationId]; testContext.SagaContinued = true; testContext.MaybeCompleted(); - return ReplyToOriginator(context, new ReplyToOriginatorMessage()); + return ReplyToOriginator(context, new ReplyToOriginatorMessage()); } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs index c79707751e7..cdc13d7c817 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Sagas/When_using_ReplyToOriginator_with_headers.cs @@ -43,7 +43,7 @@ public class ReplyingSaga : Saga, public Task Handle(InitiateRequestingSaga message, IMessageHandlerContext context) { var customHeaders = new Dictionary { { "CustomHeader", Data.CustomHeaderValue.ToString() } }; - return ReplyToOriginator(context, new MyReplyToOriginator(), customHeaders); + return ReplyToOriginator(context, new MyReplyToOriginator(), customHeaders); } protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs new file mode 100644 index 00000000000..8cb19255186 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_an_already_cancelled_token_is_passed_to_run.cs @@ -0,0 +1,20 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using System.Threading; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +public class When_an_already_cancelled_token_is_passed_to_run : NServiceBusAcceptanceTest +{ + [Test] + public void Should_fail_with_a_timeout() + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run(new CancellationToken(canceled: true))); + + Assert.That(exception.Message, Does.Contain("The scenario did not complete before the cancellation token was cancelled")); + } + + public class Context : ScenarioContext; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs new file mode 100644 index 00000000000..7c95de35701 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_scenario_exceeds_the_test_timeout.cs @@ -0,0 +1,19 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +public class When_scenario_exceeds_the_test_timeout : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(250)] + public void Should_fail_with_the_test_timeout_budget() + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run()); + + Assert.That(exception.Message, Does.Contain("timeout budget of 250 ms")); + } + + public class Context : ScenarioContext; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs new file mode 100644 index 00000000000..a5052f2044a --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/ScenarioTimeout/When_the_test_cancellation_token_is_passed_to_run.cs @@ -0,0 +1,21 @@ +namespace NServiceBus.AcceptanceTests.Core.ScenarioTimeout; + +using System; +using System.Threading; +using NServiceBus.AcceptanceTesting; +using NUnit.Framework; + +[CancelAfter(5_000)] +public class When_the_test_cancellation_token_is_passed_to_run : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(250)] + public void Should_use_the_passed_token(CancellationToken cancellationToken = default) + { + var exception = Assert.ThrowsAsync(() => Scenario.Define() + .Run(cancellationToken)); + + Assert.That(exception.Message, Does.Contain("timeout budget of 250 ms")); + } + + public class Context : ScenarioContext; +} diff --git a/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs b/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs new file mode 100644 index 00000000000..b9bbca9058d --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Core/Stopping/When_disposing_without_stopping.cs @@ -0,0 +1,58 @@ +namespace NServiceBus.AcceptanceTests.Core.Stopping; + +using System; +using System.Threading; +using System.Threading.Tasks; +using AcceptanceTesting; +using EndpointTemplates; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + +public class When_disposing_without_stopping : NServiceBusAcceptanceTest +{ + [Test] + [CancelAfter(30000)] + public async Task Should_initiate_immediate_handler_cancellation(CancellationToken cancellationToken = default) => + await Scenario.Define() + .WithEndpoint(b => + b.ServiceResolve(static async (provider, context, token) => + { + var session = provider.GetRequiredService(); + await session.SendLocal(new MessageThatTakesALongTime(), token); + + await context.MessageReceived.Task.WaitAsync(token); + + var disposer = provider.GetRequiredKeyedService>("Disposer"); + await disposer(); + }, true)) + .Run(cancellationToken); + + public class Context : ScenarioContext + { + public TaskCompletionSource MessageReceived { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public class EndpointThatGetsRugPulled : EndpointConfigurationBuilder + { + public EndpointThatGetsRugPulled() => EndpointSetup(); + + [Handler] + public class InfiniteHandler(Context testContext) : IHandleMessages + { + public async Task Handle(MessageThatTakesALongTime message, IMessageHandlerContext context) + { + try + { + testContext.MessageReceived.SetResult(); + await Task.Delay(Timeout.InfiniteTimeSpan, context.CancellationToken); + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + testContext.MarkAsCompleted(); + } + } + } + } + + public class MessageThatTakesALongTime : IMessage; +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs index 301924e90b9..1ba4f06899a 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_transactionscope_enabled.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System.Threading.Tasks; using System.Transactions; @@ -44,12 +46,16 @@ public class MyMessageHandler(Context testContext) : IHandleMessages { public Task Handle(MyMessage message, IMessageHandlerContext context) { - if (Transaction.Current != null) + var transaction = Transaction.Current; + + if (transaction is not null) { - testContext.AmbientTransactionPresent = Transaction.Current != null; - testContext.IsolationLevel = Transaction.Current.IsolationLevel; + testContext.AmbientTransactionPresent = true; + testContext.IsolationLevel = transaction.IsolationLevel; } + testContext.MarkAsCompleted(); + return Task.CompletedTask; } } diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs index 112f5ed462d..a92130bb977 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_default_transaction_mode.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System.Threading.Tasks; using AcceptanceTesting; diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs index 58545991516..2ef7a5f0d01 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_used_with_transport_scopes.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System; using AcceptanceTesting; diff --git a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs index 820675043ed..bb685107c98 100644 --- a/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs +++ b/src/NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope/When_using_timeout_greater_than_machine_max.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; +#nullable enable + +namespace NServiceBus.AcceptanceTests.Core.UnitOfWork.TransactionScope; using System; using AcceptanceTesting; diff --git a/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs b/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs index edc1b7e0963..61b5aa44c0b 100644 --- a/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs +++ b/src/NServiceBus.AcceptanceTests/Forwarding/When_requesting_message_to_be_forwarded.cs @@ -39,7 +39,7 @@ public class MessageToForwardHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.GotForwardedMessage = true; testContext.MarkAsCompleted(); return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index 1af349bf801..2f335d4f9e5 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs b/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs index ad5f841537a..f1d5afc1291 100644 --- a/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs +++ b/src/NServiceBus.AcceptanceTests/Outbox/When_headers_contain_special_characters.cs @@ -82,7 +82,9 @@ public Task Handle(PlaceOrder message, IMessageHandlerContext context) { sendOptions.SetHeader(header.Key, header.Value); } +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(sendOrderAcknowledgement, sendOptions); +#pragma warning restore NSB0040 } } diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs index e6ca2b2d1a5..0e0dcf895ad 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_non_transactional_message_is_moved_to_error_queue.cs @@ -57,7 +57,9 @@ public Task Handle(InitiatingMessage initiatingMessage, IMessageHandlerContext c { Id = initiatingMessage.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(ErrorSpyAddress, message); +#pragma warning restore NSB0040 } return Task.CompletedTask; } diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs index 21aed7f1ad7..8d117a0feca 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_receiveonly_message_is_moved_to_error_queue.cs @@ -57,7 +57,9 @@ public Task Handle(InitiatingMessage initiatingMessage, IMessageHandlerContext c { Id = initiatingMessage.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return context.Send(ErrorSpyAddress, message); +#pragma warning restore NSB0040 } return Task.CompletedTask; diff --git a/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs b/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs index acceb2f1f6e..47b54e80505 100644 --- a/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs +++ b/src/NServiceBus.AcceptanceTests/Recoverability/When_retrying_message_from_error_queue.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using AcceptanceTesting; using AcceptanceTesting.Customization; @@ -100,7 +101,7 @@ public class FailedMessageHandler(Context testContext) : IHandleMessages x.Key, x => x.Value); testContext.MaybeCompleted(); return Task.CompletedTask; } diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..2b32dcb4cc7 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_a_concrete_event_with_trimming_safe_mapper.cs @@ -0,0 +1,156 @@ +namespace NServiceBus.AcceptanceTests.Routing; + +using System; +using System.Threading; +using System.Threading.Tasks; +using AcceptanceTesting; +using Configuration.AdvancedExtensibility; +using EndpointTemplates; +using Features; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.MessageInterfaces; +using NUnit.Framework; +using Conventions = AcceptanceTesting.Customization.Conventions; + +public class When_publishing_a_concrete_event_with_trimming_safe_mapper : NServiceBusAcceptanceTest +{ + [Test, CancelAfter(20_000)] + public async Task Should_dispatch_to_interface_handlers_without_generating_a_proxy(CancellationToken cancellationToken = default) + { + var context = await Scenario.Define(c => c.Id = Guid.NewGuid()) + .WithEndpoint(b => + b.When(c => c is { EventASubscribed: true, EventBSubscribed: true }, (session, ctx) => + { + var message = new CompositeEvent + { + ContextId = ctx.Id + }; +#pragma warning disable NSB0040 // Intentional runtime-type routing + return session.Publish(message); +#pragma warning restore NSB0040 + })) + .WithEndpoint(b => b.When(async (session, ctx) => + { + await session.Subscribe(); + await session.Subscribe(); + + if (ctx.HasNativePubSubSupport) + { + ctx.EventASubscribed = true; + ctx.EventBSubscribed = true; + } + })) + .Run(cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.GotEventA, Is.True); + Assert.That(context.GotEventB, Is.True); + Assert.That(context.HandlerReceivedConcreteInstance, Is.True); + } + } + + public class Context : ScenarioContext + { + public Guid Id { get; set; } + public bool EventASubscribed { get; set; } + public bool EventBSubscribed { get; set; } + public bool GotEventA { get; set; } + public bool GotEventB { get; set; } + public bool HandlerReceivedConcreteInstance { get; set; } + + public void MaybeCompleted() => MarkAsCompleted(GotEventA, GotEventB); + } + + public class Publisher : EndpointConfigurationBuilder + { + public Publisher() => + EndpointSetup(c => + { + // Force the trimming-safe mapper even though the test runs on a JIT runtime, + // to prove the path that NativeAOT/trimmed endpoints take. + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + c.OnEndpointSubscribed((s, context) => + { + if (s.SubscriberEndpoint.Contains(Conventions.EndpointNamingConvention(typeof(Subscriber)))) + { + if (s.MessageType == typeof(IEventA).AssemblyQualifiedName) + { + context.EventASubscribed = true; + } + if (s.MessageType == typeof(IEventB).AssemblyQualifiedName) + { + context.EventBSubscribed = true; + } + } + }); + }, + metadata => metadata.RegisterSelfAsPublisherFor(this)); + } + + public class Subscriber : EndpointConfigurationBuilder + { + public Subscriber() => + EndpointSetup(c => + { + c.DisableFeature(); + c.GetSettings().Set(new TrimmingSafeMessageMapper()); + }, + metadata => + { + metadata.RegisterPublisherFor(); + metadata.RegisterPublisherFor(); + }); + + [Handler] + public class EventAHandler(Context testContext) : IHandleMessages + { + public Task Handle(IEventA @event, IMessageHandlerContext context) + { + if (@event.ContextId != testContext.Id) + { + return Task.CompletedTask; + } + testContext.GotEventA = true; + testContext.HandlerReceivedConcreteInstance = @event is CompositeEvent; + testContext.MaybeCompleted(); + + return Task.CompletedTask; + } + } + + [Handler] + public class EventBHandler(Context testContext) : IHandleMessages + { + public Task Handle(IEventB @event, IMessageHandlerContext context) + { + if (@event.ContextId != testContext.Id) + { + return Task.CompletedTask; + } + + testContext.GotEventB = true; + testContext.MaybeCompleted(); + + return Task.CompletedTask; + } + } + } + + public class CompositeEvent : IEventA, IEventB + { + public Guid ContextId { get; set; } + public string StringProperty { get; set; } + public int IntProperty { get; set; } + } + + public interface IEventA : IEvent + { + Guid ContextId { get; set; } + } + + public interface IEventB : IEvent + { + Guid ContextId { get; set; } + } +} diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs index 737477ef410..d27a64a720a 100644 --- a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_an_event_implementing_two_unrelated_interfaces.cs @@ -22,7 +22,9 @@ public async Task Event_should_be_published_using_instance_type(CancellationToke { ContextId = ctx.Id }; +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Publish(message); +#pragma warning restore NSB0040 })) .WithEndpoint(b => b.When(async (session, ctx) => { diff --git a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs index 920647046ff..50d8b8ccf5e 100644 --- a/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs +++ b/src/NServiceBus.AcceptanceTests/Routing/When_publishing_using_base_type.cs @@ -20,7 +20,9 @@ public async Task Event_should_be_published_using_instance_type(CancellationToke { IMyEvent message = new EventMessage(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Publish(message); +#pragma warning restore NSB0040 })) .WithEndpoint(b => b.When(async (session, ctx) => { diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs index 86040e19997..a26425a0afe 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_message_has_a_saga_id.cs @@ -20,7 +20,9 @@ public async Task Should_not_start_a_new_saga_if_not_found() options.SetHeader(Headers.SagaId, Guid.NewGuid().ToString()); options.SetHeader(Headers.SagaType, typeof(SagaEndpoint.MessageWithSagaIdSaga).AssemblyQualifiedName); options.RouteToThisEndpoint(); +#pragma warning disable NSB0040 // Intentional runtime-type routing return session.Send(message, options); +#pragma warning restore NSB0040 })) .Run(); diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs index 76ac0a98435..c2e56003c57 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator.cs @@ -71,7 +71,7 @@ public Task Handle(RequestToRespondingSaga message, IMessageHandlerContext conte public Task Handle(SendReplyFromNonInitiatingHandler message, IMessageHandlerContext context) => //reply to originator must be used here since the sender of the incoming message is this saga and not the requesting saga - ReplyToOriginator(context, new ResponseFromOtherSaga + ReplyToOriginator(context, new ResponseFromOtherSaga { SomeCorrelationId = Data.CorrIdForRequest }); diff --git a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs index 510a43065db..7b73ab4f599 100644 --- a/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs +++ b/src/NServiceBus.AcceptanceTests/Sagas/When_replying_to_originator_from_a_timeout.cs @@ -70,7 +70,7 @@ public class RequestResponseRespondingSaga3 : Saga //reply to originator must be used here since the sender of the incoming message is the TimeoutManager and not the requesting saga - ReplyToOriginator(context, new ResponseFromOtherSaga + ReplyToOriginator(context, new ResponseFromOtherSaga { SomeCorrelationId = Data.CorrIdForRequest }); diff --git a/src/NServiceBus.ContainerTests/.editorconfig b/src/NServiceBus.ContainerTests/.editorconfig deleted file mode 100644 index 6ba33f0efeb..00000000000 --- a/src/NServiceBus.ContainerTests/.editorconfig +++ /dev/null @@ -1,4 +0,0 @@ -[*.cs] - -# Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken -dotnet_diagnostic.NSB0002.severity = suggestion diff --git a/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj b/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj deleted file mode 100644 index 4fcc13665f1..00000000000 --- a/src/NServiceBus.ContainerTests/NServiceBus.ContainerTests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net10.0 - true - ..\NServiceBusTests.snk - - - - - - - - - - - - - - - diff --git a/src/NServiceBus.ContainerTests/When_building_components.cs b/src/NServiceBus.ContainerTests/When_building_components.cs deleted file mode 100644 index 440de2b6237..00000000000 --- a/src/NServiceBus.ContainerTests/When_building_components.cs +++ /dev/null @@ -1,196 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_building_components -{ - [Test] - public void Singleton_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(SingletonComponent)); - var instance2 = serviceProvider.GetService(typeof(SingletonComponent)); - - Assert.That(instance1, Is.EqualTo(instance2)); - } - - [Test] - public void Transient_components_should_yield_unique_instances() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(); - var instance2 = serviceProvider.GetService(); - - Assert.That(instance1, Is.Not.EqualTo(instance2)); - } - - [Test] - public void Scoped_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(ScopedComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1)); - } - - [Test] - public void Lambda_scoped_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(ScopedLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedLambdaComponent)); - - Assert.That(instance2, Is.SameAs(instance1)); - } - - [Test] - public void Lambda_transient_components_should_yield_unique_instances() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(TransientLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(TransientLambdaComponent)); - - Assert.That(instance1, Is.Not.EqualTo(instance2)); - } - - [Test] - public void Lambda_singleton_components_should_yield_the_same_instance() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var instance1 = serviceProvider.GetService(typeof(SingletonLambdaComponent)); - var instance2 = serviceProvider.GetService(typeof(SingletonLambdaComponent)); - - Assert.That(instance1, Is.EqualTo(instance2)); - } - - [Test] - public void Resolving_all_components_of_unregistered_types_should_give_empty_list() - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - Assert.That(serviceProvider.GetServices(), Is.Empty); - } - - [Test] - public void Resolving_recursive_types_does_not_stack_overflow() - { - try - { - var serviceCollection = new ServiceCollection(); - InitializeServices(serviceCollection); - using var serviceProvider = serviceCollection.BuildServiceProvider(); - serviceProvider.GetService(typeof(RecursiveComponent)); - } - catch (Exception) - { - // this can't be a StackOverflowException as they can't be caught - } - } - - static void InitializeServices(IServiceCollection serviceCollection) - { - serviceCollection.AddSingleton(); - serviceCollection.AddTransient(); - serviceCollection.AddScoped(); - serviceCollection.AddSingleton(_ => new SingletonLambdaComponent()); - serviceCollection.AddTransient(_ => new TransientLambdaComponent()); - serviceCollection.AddScoped(_ => new ScopedLambdaComponent()); - serviceCollection.AddSingleton(_ => new RecursiveComponent()); - } - - public class RecursiveComponent - { - public RecursiveComponent Instance { get; set; } - } - - public class SingletonComponent - { - } - - public interface ISingletonComponentWithPropertyDependency - { - } - - public class SingletonComponentWithPropertyDependency : ISingletonComponentWithPropertyDependency - { - public SingletonComponent Dependency { get; set; } - } - - public class TransientComponent - { - } - - public class UnregisteredComponent - { - public SingletonComponent SingletonComponent { get; set; } - } - - public class SingletonLambdaComponent - { - } - - public class ScopedLambdaComponent - { - } - - public class TransientLambdaComponent - { - } -} - -public class StaticFactory -{ -#pragma warning disable CA1822 // Mark members as static - public ComponentCreatedByFactory Create() -#pragma warning restore CA1822 // Mark members as static - { - return new ComponentCreatedByFactory(); - } -} - -public class ComponentCreatedByFactory -{ -} - -public class ComponentWithBothConstructorAndSetterInjection -{ - public ComponentWithBothConstructorAndSetterInjection(ConstructorDependency constructorDependency) - { - ConstructorDependency = constructorDependency; - } - - public ConstructorDependency ConstructorDependency { get; } - - public SetterDependency SetterDependency { get; set; } -} - -public class ConstructorDependency -{ -} - -public class SetterDependency -{ -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs b/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs deleted file mode 100644 index 617c9ae9506..00000000000 --- a/src/NServiceBus.ContainerTests/When_disposing_the_builder.cs +++ /dev/null @@ -1,53 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_disposing_the_builder -{ - [Test] - public void Should_dispose_all_IDisposable_components() - { - var serviceCollection = new ServiceCollection(); - - DisposableComponent.DisposeCalled = false; - AnotherSingletonComponent.DisposeCalled = false; - - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(new AnotherSingletonComponent()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - serviceProvider.GetService(typeof(DisposableComponent)); - serviceProvider.GetService(typeof(AnotherSingletonComponent)); - (serviceProvider as IDisposable)?.Dispose(); - - using (Assert.EnterMultipleScope()) - { - Assert.That(DisposableComponent.DisposeCalled, Is.True, "Dispose should be called on DisposableComponent"); - Assert.That(AnotherSingletonComponent.DisposeCalled, Is.False, "Dispose should not be called on AnotherSingletonComponent"); - } - } - - public class DisposableComponent : IDisposable - { - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - } - - public class AnotherSingletonComponent : IDisposable - { - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs b/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs deleted file mode 100644 index 4858e2e4757..00000000000 --- a/src/NServiceBus.ContainerTests/When_querying_for_registered_components.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -[TestFixture] -public class When_querying_for_registered_components -{ - [Test] - public void Existing_components_should_return_true() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(ExistingComponent)), Is.True); - } - - [Test] - public void Non_existing_components_should_return_false() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(NonExistingComponent)), Is.False); - } - - [Test] - public void Builders_should_not_determine_existence_by_building_components() - { - var serviceCollection = new ServiceCollection(); - InitializeBuilder(serviceCollection); - - Assert.That(serviceCollection.Any(sd => sd.ServiceType == typeof(ExistingComponentWithUnsatisfiedDependency)), Is.True); - } - - static void InitializeBuilder(IServiceCollection c) - { - c.AddTransient(); - c.AddTransient(); - } - - public class NonExistingComponent - { - } - - public class ExistingComponent - { - } - - public class ExistingComponentWithUnsatisfiedDependency - { - public ExistingComponentWithUnsatisfiedDependency(NonExistingComponent dependency) - { - - } - } -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_registering_components.cs b/src/NServiceBus.ContainerTests/When_registering_components.cs deleted file mode 100644 index 1a8ed1dbb30..00000000000 --- a/src/NServiceBus.ContainerTests/When_registering_components.cs +++ /dev/null @@ -1,194 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - - -public class When_registering_components -{ - [Test] - public void Multiple_registrations_of_the_same_component_should_be_allowed() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetServices().Count(), Is.EqualTo(2)); - } - - [Test] - public void Should_support_lambdas_that_uses_other_components_registered_later() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(s => ((StaticFactory)s.GetService(typeof(StaticFactory))).Create()); - serviceCollection.AddSingleton(_ => new StaticFactory()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(ComponentCreatedByFactory)), Is.Not.Null); - } - - [Test] - public void A_registration_should_be_allowed_to_be_updated() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(new SingletonComponent()); - serviceCollection.AddSingleton(new AnotherSingletonComponent()); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(ISingletonComponent)), Is.InstanceOf()); - } - - [Test] - public void Register_singleton_should_be_supported() - { - var singleton = new SingletonComponent(); - var serviceCollection = new ServiceCollection(); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddSingleton(singleton); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(SingletonComponent)))); - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingletonComponent)))); - } - - [Test] - public void Registering_the_same_singleton_for_different_interfaces_should_be_supported() - { - var serviceCollection = new ServiceCollection(); - var singleton = new SingletonThatImplementsToInterfaces(); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddSingleton(singleton); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - var dependency = (ComponentThatDependsOnMultiSingletons)serviceProvider.GetService(typeof(ComponentThatDependsOnMultiSingletons)); - - using (Assert.EnterMultipleScope()) - { - Assert.That(dependency.Singleton1, Is.Not.Null); - Assert.That(dependency.Singleton2, Is.Not.Null); - - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingleton1)))); - } - Assert.That(singleton, Is.EqualTo(serviceProvider.GetService(typeof(ISingleton2)))); - } - - [Test] - public void Given_lookupType_should_be_used_as_service_in_the_registration_when_RegisterSingleton() - { - var serviceCollection = new ServiceCollection(); - var expected = new InheritedFromSomeClass(); - serviceCollection.AddSingleton(expected); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - Assert.That(serviceProvider.GetService(typeof(SomeClass)), Is.EqualTo(expected)); - - using (var scope = serviceProvider.CreateScope()) - { - Assert.That(scope.ServiceProvider.GetService(typeof(SomeClass)), Is.EqualTo(expected)); - } - } -} - - -public class ComponentThatDependsOnMultiSingletons -{ - public ComponentThatDependsOnMultiSingletons(ISingleton1 singleton1, ISingleton2 singleton2) - { - Singleton1 = singleton1; - Singleton2 = singleton2; - } - - public ISingleton1 Singleton1 { get; private set; } - public ISingleton2 Singleton2 { get; private set; } -} - -public class SingletonThatImplementsToInterfaces : ISingleton2 -{ -} - -public interface ISingleton2 : ISingleton1 -{ -} - -public interface ISingleton1 -{ -} - -public class ComponentWithMultipleInterfaces : ISomeInterface, ISomeOtherInterface -{ -} - -public class ComponentWithGenericInterface : ISomeGenericInterface -{ -} - -public class ComponentWithSystemInterface : IGrouping, IDisposable -{ - public IEnumerator GetEnumerator() - { - throw new NotImplementedException(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public string Key => throw new NotImplementedException(); - - public void Dispose() - { - GC.SuppressFinalize(this); - } -} - -public interface ISomeGenericInterface -{ -} - -public interface ISomeOtherInterface : IYetAnotherInterface -{ -} - -public interface IYetAnotherInterface -{ -} - -public class DuplicateClass -{ - public bool SomeProperty { get; set; } - public bool AnotherProperty { get; set; } -} - -public interface IWithSetterDependencies -{ -} - -public class SomeClass : ISomeInterface -{ -} - -public class InheritedFromSomeClass : SomeClass -{ -} - -public class SomeOtherClass : ISomeInterface -{ -} - -public interface ISomeInterface -{ -} - -#pragma warning disable CA1711 // Identifiers should not have incorrect suffix -public enum SomeEnum -#pragma warning restore CA1711 // Identifiers should not have incorrect suffix -{ - X -} \ No newline at end of file diff --git a/src/NServiceBus.ContainerTests/When_using_nested_containers.cs b/src/NServiceBus.ContainerTests/When_using_nested_containers.cs deleted file mode 100644 index 8e01e2abd11..00000000000 --- a/src/NServiceBus.ContainerTests/When_using_nested_containers.cs +++ /dev/null @@ -1,244 +0,0 @@ -namespace NServiceBus.ContainerTests; - -using System; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using NUnit.Framework; - -public class When_using_nested_containers -{ - [Test] - public async Task Scoped__components_should_be_disposed_when_the_child_container_is_disposed() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - var serviceProvider = serviceCollection.BuildServiceProvider(); - await using (serviceProvider.ConfigureAwait(false)) - { - var scope = serviceProvider.CreateAsyncScope(); - await using (scope.ConfigureAwait(false)) - { - scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - } - - Assert.That(ScopedComponent.DisposeCalled, Is.True); - } - - [Test] - public void Scoped_components_should_yield_different_instances_between_parent_and_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - var parentInstance = serviceProvider.GetService(typeof(ScopedComponent)); - using (var scope = serviceProvider.CreateScope()) - { - var childInstance = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(childInstance, Is.Not.SameAs(parentInstance)); - } - } - - [Test] - public void Scoped_components_should_yield_different_instances_between_different_instances_of_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - object instance1; - using (var scope = serviceProvider.CreateScope()) - { - instance1 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - - object instance2; - using (var scope = serviceProvider.CreateScope()) - { - instance2 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - } - Assert.That(instance2, Is.Not.SameAs(instance1)); - } - - [Test] - public void Transient_components_should_not_be_shared_across_child_containers() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddTransient(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - object instance1; - using (var scope = serviceProvider.CreateScope()) - { - instance1 = scope.ServiceProvider.GetService(typeof(TransientComponent)); - } - - object instance2; - using (var scope = serviceProvider.CreateScope()) - { - instance2 = scope.ServiceProvider.GetService(typeof(TransientComponent)); - } - - Assert.That(instance2, Is.Not.SameAs(instance1)); - } - - [Test] - public void Scoped_components_in_the_parent_container_should_be_singletons_in_the_same_child_container() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (var scope = serviceProvider.CreateScope()) - { - var instance1 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - var instance2 = scope.ServiceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1), "UoW's should be singleton in child container"); - } - } - - [Test] - public void Scoped_components_built_on_root_container_should_be_singletons_even_with_child_builder_present() - { - var serviceCollection = new ServiceCollection(); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (serviceProvider.CreateScope()) - { - } - - var instance1 = serviceProvider.GetService(typeof(ScopedComponent)); - var instance2 = serviceProvider.GetService(typeof(ScopedComponent)); - - Assert.That(instance2, Is.SameAs(instance1), "UoW's should be singletons in the root container"); - } - - [Test] - public void Should_not_dispose_singletons_when_container_goes_out_of_scope() - { - var serviceCollection = new ServiceCollection(); - var singletonInMainContainer = new SingletonComponent(); - serviceCollection.AddSingleton(singletonInMainContainer); - serviceCollection.AddScoped(); - - using var serviceProvider = serviceCollection.BuildServiceProvider(); - - using (var scope = serviceProvider.CreateScope()) - { - scope.ServiceProvider.GetService(typeof(ComponentThatDependsOfSingleton)); - } - Assert.That(SingletonComponent.DisposeCalled, Is.False); - } - - [Test] - public void Should_dispose_all_non_singleton_IDisposable_components_in_child_container() - { - var serviceCollection = new ServiceCollection(); - DisposableComponent.DisposeCalled = false; - AnotherDisposableComponent.DisposeCalled = false; - serviceCollection.AddSingleton(new AnotherDisposableComponent()); - serviceCollection.AddScoped(); - - - using (var serviceProvider = serviceCollection.BuildServiceProvider()) - using (var scope = serviceProvider.CreateScope()) - { - scope.ServiceProvider.GetService(typeof(DisposableComponent)); - } - - using (Assert.EnterMultipleScope()) - { - Assert.That(AnotherDisposableComponent.DisposeCalled, Is.False, "Dispose should not be called on AnotherSingletonComponent because it belongs to main container"); - Assert.That(DisposableComponent.DisposeCalled, Is.True, "Dispose should be called on DisposableComponent"); - } - } - - public interface IInstanceToReplaceInNested - { - } - - public class InstanceToReplaceInNested_Parent : IInstanceToReplaceInNested - { - } - - public class InstanceToReplaceInNested_Child : IInstanceToReplaceInNested - { - } - - class SingletonComponent : ISingletonComponent, IDisposable - { - public void Dispose() - { - DisposeCalled = true; - } - - public static bool DisposeCalled; - } - - class ComponentThatDependsOfSingleton - { - } -} - -public class TransientComponent : IDisposable -{ - public void Dispose() - { - GC.SuppressFinalize(this); - } -} - -public class ScopedComponent : IDisposable -{ - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } - - public static bool DisposeCalled { get; private set; } -} - -public class SingletonComponent : ISingletonComponent -{ -} - -public class AnotherSingletonComponent : ISingletonComponent -{ -} - -public interface ISingletonComponent -{ -} - -public class DisposableComponent : IDisposable -{ - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } -} - -public class AnotherDisposableComponent : IDisposable -{ - public static bool DisposeCalled { get; set; } - - public void Dispose() - { - DisposeCalled = true; - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs index 5e3b568fbc7..cae91242bb1 100644 --- a/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs +++ b/src/NServiceBus.Core.Analyzer.Fixes/Handlers/HandlerAttributeFixer.cs @@ -164,7 +164,7 @@ static async Task MoveHandlerAttribute( } var isInterfaceBasedHandler = type.ImplementsGenericInterface(knownTypes.IHandleMessages); - var isConventionBasedHandler = !isInterfaceBasedHandler && ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(type, knownTypes); + var isConventionBasedHandler = !isInterfaceBasedHandler && ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(type, knownTypes, cancellationToken: cancellationToken); if (!isInterfaceBasedHandler && !isConventionBasedHandler) { diff --git a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs new file mode 100644 index 00000000000..f7edb0f1931 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs @@ -0,0 +1,320 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Fixes; + +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Simplification; + +[Shared] +[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(MessagingMigrationFixer))] +public sealed class MessagingMigrationFixer : CodeFixProvider +{ + const string MessageTypeProperty = "MessageType"; + const string EquivalenceKey = "UseStronglyTypedMessageOverload"; + + public override ImmutableArray FixableDiagnosticIds { get; } = + [DiagnosticIds.UseGenericMessageType]; + + public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); + if (root is null) + { + return; + } + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false); + + foreach (var diagnostic in context.Diagnostics) + { + if (!diagnostic.Properties.TryGetValue(MessageTypeProperty, out var messageType) || + string.IsNullOrWhiteSpace(messageType)) + { + continue; + } + + var node = root.FindNode(diagnostic.Location.SourceSpan, getInnermostNodeForTie: true); + + if (node is ExpressionSyntax methodReference && CanAddTypeArgument(methodReference)) + { + if (!CanOfferFix(semanticModel, methodReference)) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => AddTypeArgumentToMethodReference( + context.Document, + root, + methodReference, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + continue; + } + + if (node is AssignmentExpressionSyntax assignment && + TryGetMutatorReplacementMethod(semanticModel, assignment, out var replacementMethodName)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => ReplaceAssignmentWithTypedCall( + context.Document, + root, + assignment, + replacementMethodName, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + continue; + } + + if (node.FirstAncestorOrSelf() is not { } invocation || + !CanAddTypeArgument(invocation.Expression)) + { + continue; + } + + if (!CanOfferFix(semanticModel, invocation.Expression)) + { + continue; + } + + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => AddTypeArgument( + context.Document, + root, + invocation, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + } + } + + static bool CanAddTypeArgument(ExpressionSyntax expression) => expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax } => true, + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax } => true, + IdentifierNameSyntax => true, + _ => false + }; + + // Default interface members are not callable through a concrete receiver. + static bool CanOfferFix(SemanticModel? semanticModel, ExpressionSyntax expression) + { + if (semanticModel is null || expression is not ( + MemberAccessExpressionSyntax or MemberBindingExpressionSyntax or IdentifierNameSyntax)) + { + return false; + } + + var methodName = expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax name } => name.Identifier.ValueText, + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } => name.Identifier.ValueText, + IdentifierNameSyntax identifier => identifier.Identifier.ValueText, + _ => null + }; + if (methodName is null) + { + return false; + } + + var receiverType = GetReceiverType(semanticModel, expression); + if (receiverType is null || receiverType.TypeKind == TypeKind.Error) + { + return false; + } + + if (receiverType.TypeKind == TypeKind.Interface) + { + return true; + } + + var within = semanticModel.GetEnclosingSymbol(expression.SpanStart)?.ContainingType; + for (var type = receiverType; type is not null; type = type.BaseType) + { + foreach (var member in type.GetMembers(methodName).OfType()) + { + if (!member.IsGenericMethod || member.TypeParameters.Length == 0) + { + continue; + } + + // Exclude creator overloads such as Send(Action, ...). + var messageTypeParameter = member.TypeParameters[0]; + if (!member.Parameters.Any(parameter => + SymbolEqualityComparer.Default.Equals(parameter.Type, messageTypeParameter))) + { + continue; + } + + if (within is null + ? member.DeclaredAccessibility == Accessibility.Public + : semanticModel.Compilation.IsSymbolAccessibleWithin(member, within)) + { + return true; + } + } + } + + return false; + } + + static ITypeSymbol? GetReceiverType(SemanticModel semanticModel, ExpressionSyntax expression) + { + var receiverSyntax = expression switch + { + MemberAccessExpressionSyntax memberAccess => memberAccess.Expression, + MemberBindingExpressionSyntax memberBinding when + memberBinding.Parent is ConditionalAccessExpressionSyntax conditional => conditional.Expression, + _ => null + }; + + if (receiverSyntax is not null) + { + return semanticModel.GetTypeInfo(receiverSyntax).Type; + } + + return semanticModel.GetEnclosingSymbol(expression.SpanStart)?.ContainingType; + } + + static Task AddTypeArgument( + Document document, + SyntaxNode root, + InvocationExpressionSyntax invocation, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updatedInvocation = invocation.WithExpression(AddTypeArgumentToExpression(invocation.Expression, messageType)) + .WithAdditionalAnnotations(Formatter.Annotation); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(invocation, updatedInvocation))); + } + + static Task AddTypeArgumentToMethodReference( + Document document, + SyntaxNode root, + ExpressionSyntax methodReference, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var updatedMethodReference = AddTypeArgumentToExpression(methodReference, messageType) + .WithAdditionalAnnotations(Formatter.Annotation); + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(methodReference, updatedMethodReference))); + } + + static bool TryGetMutatorReplacementMethod( + SemanticModel? semanticModel, + AssignmentExpressionSyntax assignment, + out string methodName) + { + methodName = null!; + if (semanticModel is null || assignment.Left is not MemberAccessExpressionSyntax memberAccess) + { + return false; + } + + var propertySymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + if (propertySymbol is not IPropertySymbol + { + ContainingType: { } containingType + }) + { + return false; + } + + var containingTypeName = containingType.ToDisplayString(); + if (propertySymbol.Name == "Message" && + containingTypeName == "NServiceBus.MessageMutator.MutateIncomingMessageContext") + { + methodName = "UpdateMessageInstance"; + return true; + } + + if (propertySymbol.Name == "OutgoingMessage" && + containingTypeName == "NServiceBus.MessageMutator.MutateOutgoingMessageContext") + { + methodName = "UpdateMessage"; + return true; + } + + return false; + } + + static Task ReplaceAssignmentWithTypedCall( + Document document, + SyntaxNode root, + AssignmentExpressionSyntax assignment, + string methodName, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var memberAccess = (MemberAccessExpressionSyntax)assignment.Left; + var typeArgument = SyntaxFactory.ParseTypeName(messageType) + .WithAdditionalAnnotations(Simplifier.Annotation); + var typeArguments = SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(typeArgument)); + + var invocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + memberAccess.Expression, + SyntaxFactory.GenericName(SyntaxFactory.Identifier(methodName), typeArguments) + .WithTriviaFrom(memberAccess.Name)), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(assignment.Right)))) + .WithAdditionalAnnotations(Formatter.Annotation); + + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(assignment, invocation))); + } + + static ExpressionSyntax AddTypeArgumentToExpression(ExpressionSyntax expression, string messageType) + { + var typeArgument = SyntaxFactory.ParseTypeName(messageType) + .WithAdditionalAnnotations(Simplifier.Annotation); + var typeArguments = SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(typeArgument)); + + return expression switch + { + MemberAccessExpressionSyntax { Name: IdentifierNameSyntax name } memberAccess => + memberAccess.WithName( + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name)), + MemberBindingExpressionSyntax { Name: IdentifierNameSyntax name } memberBinding => + memberBinding.WithName( + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name)), + IdentifierNameSyntax name => + SyntaxFactory.GenericName(name.Identifier, typeArguments) + .WithTriviaFrom(name), + _ => expression + }; + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt new file mode 100644 index 00000000000..5ac5b7d5fff --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServices.approved.txt @@ -0,0 +1,140 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerGenerator/HandlerRegistrations.Handlers.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class ConventionBasedHandlerWithKeyedServicesHandlerRegistryExtensions + { + public sealed partial class ConventionBasedHandlerWithKeyedServicesRootRegistry + { + partial void AddAllHandlersCore() + { + AddOrderShippedHandler(); + } + + /// + /// Registers the handler with the endpoint configuration. + /// + public void AddOrderShippedHandler() + { + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020 : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _ctorServiceFromCtor; + readonly global::IMyService _methodServiceFromMethod; + readonly global::IMyService _methodService2FromMethod; + readonly global::IMyService _methodService3FromMethod; + readonly global::IMyService _methodService4FromMethod; + readonly global::IMyService _methodService5FromMethod; + readonly global::IMyService _methodService6FromMethod; + readonly global::IMyService _methodService7FromMethod; + + public OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("CtorKey")] global::IMyService ctorServiceFromCtorFromCtor, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("ConstKey")] global::IMyService methodServiceFromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("MethodKey")] global::IMyService methodService2FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("NamedKey")] global::IMyService methodService3FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("Cmd1")] global::IMyService methodService4FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(typeof(global::Cmd1))] global::IMyService methodService5FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(null)] global::IMyService methodService6FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices((global::ServiceKey)1)] global::IMyService methodService7FromMethodFromMethod) + { + _ctorServiceFromCtor = ctorServiceFromCtorFromCtor; + _methodServiceFromMethod = methodServiceFromMethodFromMethod; + _methodService2FromMethod = methodService2FromMethodFromMethod; + _methodService3FromMethod = methodService3FromMethodFromMethod; + _methodService4FromMethod = methodService4FromMethodFromMethod; + _methodService5FromMethod = methodService5FromMethodFromMethod; + _methodService6FromMethod = methodService6FromMethodFromMethod; + _methodService7FromMethod = methodService7FromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(_ctorServiceFromCtor); + return handler.Handle(message, context, _methodServiceFromMethod, _methodService2FromMethod, _methodService3FromMethod, _methodService4FromMethod, _methodService5FromMethod, _methodService6FromMethod, _methodService7FromMethod, context.CancellationToken); + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the ConventionBasedHandlerWithKeyedServices assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class ConventionBasedHandlerWithKeyedServicesHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the ConventionBasedHandlerWithKeyedServices assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public ConventionBasedHandlerWithKeyedServicesRootRegistry ConventionBasedHandlerWithKeyedServicesAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class ConventionBasedHandlerWithKeyedServicesRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt new file mode 100644 index 00000000000..345dc7ad05f --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerGeneratorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt @@ -0,0 +1,126 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerGenerator/HandlerRegistrations.Handlers.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceHandlerRegistryExtensions + { + public sealed partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry + { + partial void AddAllHandlersCore() + { + AddOrderShippedHandler(); + } + + /// + /// Registers the handler with the endpoint configuration. + /// + public void AddOrderShippedHandler() + { + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _serviceFromMethod; + + public OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("SecretKey")] global::IMyService serviceFromMethodFromMethod) + { + _serviceFromMethod = serviceFromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(); + return handler.Handle(message, context, _serviceFromMethod); + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the ConventionBasedHandlerWithKeyedServicesInAnotherNamespace assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the ConventionBasedHandlerWithKeyedServicesInAnotherNamespace assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes handlers in this namespace: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt new file mode 100644 index 00000000000..fc85c750ec2 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServices.approved.txt @@ -0,0 +1,77 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerInterceptor/InterceptionsOfAddHandlerMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddHandlerMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(10,13) + public void AddHandler_OrderShippedHandler_5f4446a2a6b04987() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020 : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _ctorServiceFromCtor; + readonly global::IMyService _methodServiceFromMethod; + readonly global::IMyService _methodService2FromMethod; + readonly global::IMyService _methodService3FromMethod; + readonly global::IMyService _methodService4FromMethod; + readonly global::IMyService _methodService5FromMethod; + readonly global::IMyService _methodService6FromMethod; + readonly global::IMyService _methodService7FromMethod; + + public OrderShippedHandler__Handle__Cmd1_9d5effcace7aa020([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("CtorKey")] global::IMyService ctorServiceFromCtorFromCtor, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("ConstKey")] global::IMyService methodServiceFromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("MethodKey")] global::IMyService methodService2FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("NamedKey")] global::IMyService methodService3FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("Cmd1")] global::IMyService methodService4FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(typeof(global::Cmd1))] global::IMyService methodService5FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices(null)] global::IMyService methodService6FromMethodFromMethod, [global::Microsoft.Extensions.DependencyInjection.FromKeyedServices((global::ServiceKey)1)] global::IMyService methodService7FromMethodFromMethod) + { + _ctorServiceFromCtor = ctorServiceFromCtorFromCtor; + _methodServiceFromMethod = methodServiceFromMethodFromMethod; + _methodService2FromMethod = methodService2FromMethodFromMethod; + _methodService3FromMethod = methodService3FromMethodFromMethod; + _methodService4FromMethod = methodService4FromMethodFromMethod; + _methodService5FromMethod = methodService5FromMethodFromMethod; + _methodService6FromMethod = methodService6FromMethodFromMethod; + _methodService7FromMethod = methodService7FromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(_ctorServiceFromCtor); + return handler.Handle(message, context, _methodServiceFromMethod, _methodService2FromMethod, _methodService3FromMethod, _methodService4FromMethod, _methodService5FromMethod, _methodService6FromMethod, _methodService7FromMethod, context.CancellationToken); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt new file mode 100644 index 00000000000..4c19be2c5ac --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddHandlerInterceptorTests.ConventionBasedHandlerWithKeyedServicesInAnotherNamespace.approved.txt @@ -0,0 +1,63 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Handlers.AddHandlerInterceptor/InterceptionsOfAddHandlerMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddHandlerMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(9,13) + public void AddHandler_OrderShippedHandler_5f4446a2a6b04987() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Cmd1), []); + } + } + } +} + +[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] +[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] +[global::System.Diagnostics.StackTraceHiddenAttribute] +[global::System.Diagnostics.DebuggerNonUserCodeAttribute] +sealed file class OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b : global::NServiceBus.IHandleMessages +{ + readonly global::IMyService _serviceFromMethod; + + public OrderShippedHandler__Handle__Cmd1_8cbe5cd78af1d63b([global::Microsoft.Extensions.DependencyInjection.FromKeyedServices("SecretKey")] global::IMyService serviceFromMethodFromMethod) + { + _serviceFromMethod = serviceFromMethodFromMethod; + } + + public global::System.Threading.Tasks.Task Handle(global::Cmd1 message, global::NServiceBus.IMessageHandlerContext context) + { + var handler = new global::OrderShippedHandler(); + return handler.Handle(message, context, _serviceFromMethod); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt new file mode 100644 index 00000000000..1afcf437878 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.BasicMessageTypes.approved.txt @@ -0,0 +1,51 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(8,13) + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(10,13) + public void AddMessageType_OrderBilled_baccb8cecdf46c5c() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderBilled), []); + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderPlaced_957130e0bad70f6d() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderPlaced), []); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt new file mode 100644 index 00000000000..4033d38acf9 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithEqualRankInterfaces.approved.txt @@ -0,0 +1,46 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderAccepted_f4c4dd469ffffa61() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderAccepted), + [ + typeof(global::Messages.ISecond), + typeof(global::Messages.IFirst), + typeof(global::Messages.OrderEventBase), + ]); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt new file mode 100644 index 00000000000..ab0de4f1621 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddMessageTypeInterceptorTests.MessageTypesWithHierarchy.approved.txt @@ -0,0 +1,57 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Messages.AddMessageTypeInterceptor/InterceptionsOfAddMessageTypeMethod.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace System.Runtime.CompilerServices +{ + [global::System.Diagnostics.Conditional("DEBUG")] + [global::System.AttributeUsage(global::System.AttributeTargets.Method, AllowMultiple = true)] + sealed file class InterceptsLocationAttribute : global::System.Attribute + { + public InterceptsLocationAttribute(int version, string data) + { + _ = version; + _ = data; + } + } +} + +namespace NServiceBus +{ + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + static file class InterceptionsOfAddMessageTypeMethod + { + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(7,13) + public void AddMessageType_OrderAccepted_f4c4dd469ffffa61() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderAccepted), + [ + typeof(global::Messages.IOrderEvent), + typeof(global::Messages.OrderEventBase), + ]); + } + + [global::System.Runtime.CompilerServices.InterceptsLocationAttribute(1, "{PLATFORM-SPECIFIC-BASE64-DATA}")] // test.cs(8,13) + public void AddMessageType_OrderRejected_6d51746e2bdec64a() + { + System.ArgumentNullException.ThrowIfNull(endpointConfiguration); + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::Messages.OrderRejected), + [ + typeof(global::Messages.OrderEventBase), + ]); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt index d00b044903f..0f38c697e01 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.BasicSagas.approved.txt @@ -39,7 +39,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_f7dc3394c771851e.Instance, OrderPlacedOrderIdAccessor_ae6ad694572b9fcb.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5b84242b5a03d625.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -77,7 +77,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_08b7b00354e1d041.Instance, OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -115,7 +115,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_2fd27f5436ef20b9.Instance, OrderPlacedOrderIdAccessor_c561311f0fcc5cf0.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_6d931b1825e0c4ca.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -216,21 +216,59 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5b84242b5a03d625 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5b84242b5a03d625() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Billing.OrderBillingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Billing.OrderBillingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5b84242b5a03d625(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_1073497622a938fb() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_6d931b1825e0c4ca : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_6d931b1825e0c4ca() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Payments.PaymentsPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Payments.PaymentsPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Payments.PaymentsPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Payments.PaymentsPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_6d931b1825e0c4ca(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt index 265d4284842..78c9bbac3bc 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.CastSyntaxWrappers.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt index 724b801bec9..87732f11532 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt @@ -36,7 +36,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -64,21 +64,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt index c2665ccb763..8b3074abd2d 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt new file mode 100644 index 00000000000..58ad635b85d --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.FinderOnlySagas.approved.txt @@ -0,0 +1,172 @@ +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Sagas.AddSagaGenerator/HandlerRegistrations.Sagas.g.cs ======= +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + public static partial class FinderOnlySagasHandlerRegistryExtensions + { + public sealed partial class FinderOnlySagasRootRegistry + { + public sealed partial class FinderOnlyRegistry + { + partial void AddAllSagasCore() + { + AddFinderOnlySaga(); + } + + /// + /// Registers the saga with the endpoint configuration. + /// + public void AddFinderOnlySaga() + { + var sagaMetadataCollection = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration) + .GetOrCreate(); + var associatedMessages = new NServiceBus.Sagas.SagaMessage[] + { + new NServiceBus.Sagas.SagaMessage(typeof(global::FinderOnly.StartSagaMessage), true, false), + }; + NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ + ]; + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, null, propertyAccessors); + sagaMetadataCollection.Add(metadata); + + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); + var messageHandlerRegistry = settings.GetOrCreate(); + var messageMetadataRegistry = settings.GetOrCreate(); + messageHandlerRegistry.AddMessageHandlerForMessage(); + messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::FinderOnly.StartSagaMessage), []); + } + } + } + } +} + +// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs == +// + +#nullable enable annotations +#nullable disable warnings + +// Suppress warnings about [Obsolete] member usage in generated code. +#pragma warning disable CS0612, CS0618 + +namespace NServiceBus +{ + /// + /// Provides access to handler and saga registries discovered in the FinderOnlySagas assembly. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + public static partial class FinderOnlySagasHandlerRegistryExtensions + { + extension (global::NServiceBus.HandlerRegistry registry) + { + /// + /// Gets the root registry for handler and saga types in the FinderOnlySagas assembly. + /// + /// + /// Use the returned registry to access namespace-specific registries and add-all methods for this assembly. + /// + public FinderOnlySagasRootRegistry FinderOnlySagasAssembly => new(registry.Configuration); + } + + /// + /// Root registry to add handlers and sagas for the entire assembly. + /// + public sealed partial class FinderOnlySagasRootRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Gets the registry for the FinderOnly namespace segment under this branch. + /// + public FinderOnlyRegistry FinderOnly => new(_configuration); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + FinderOnly.AddAllHandlers(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes child registries: . + /// + public void AddAllSagas() + { + AddAllSagasCore(); + FinderOnly.AddAllSagas(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + + /// + /// Registry for the FinderOnly namespace segment. Use this registry to add handlers and sagas for this branch. + /// + public sealed partial class FinderOnlyRegistry(global::NServiceBus.EndpointConfiguration configuration) + { + readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration)); + + /// + /// Registers all handlers and sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes sagas in this namespace: . + /// + public void AddAll() + { + AddAllHandlers(); + AddAllSagas(); + } + + /// + /// Registers all handlers for this namespace segment and its child namespaces. + /// + public void AddAllHandlers() + { + AddAllHandlersCore(); + } + + /// + /// Registers all sagas for this namespace segment and its child namespaces. + /// + /// + /// Includes sagas in this namespace: . + /// + public void AddAllSagas() + { + AddAllSagasCore(); + } + + partial void AddAllHandlersCore(); + partial void AddAllSagasCore(); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt index 06e984864bb..df2a17f6793 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InheritedMessageProperty.approved.txt @@ -34,7 +34,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -78,21 +78,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt index f2c9729d9b0..de20830dbeb 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.InvalidMappingWithCompilationErrors.approved.txt @@ -37,7 +37,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -67,21 +67,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt index 5b51dace7c6..96efb4cd9d2 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NestedSagas.approved.txt @@ -40,7 +40,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_f53ba98a08e6a113.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -71,7 +71,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_14af697526a7e650.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -119,21 +119,40 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_f53ba98a08e6a113 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_f53ba98a08e6a113() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_f53ba98a08e6a113(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_14af697526a7e650 : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_14af697526a7e650() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_14af697526a7e650(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt index a03f7dfca72..0760d9f2908 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMappings.approved.txt @@ -32,7 +32,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -60,21 +60,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt index 0820fbd3914..c2e2436f0b8 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.NullableReferenceTypeMixedMappings.approved.txt @@ -33,7 +33,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NullableMessageOrderIdAccessor_9894122651576960.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5e4ee6a1b6193efa.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -57,7 +57,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NonNullableMessageOrderIdAccessor_e023774f080a36b1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_acfa87ac0bc7f48b.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -99,21 +99,40 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5e4ee6a1b6193efa : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5e4ee6a1b6193efa() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NonNullableSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::NonNullableSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NonNullableSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::NonNullableSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5e4ee6a1b6193efa(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_acfa87ac0bc7f48b : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_acfa87ac0bc7f48b() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NullableSagaData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::NullableSagaData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NullableSagaData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::NullableSagaData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_acfa87ac0bc7f48b(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt index 236fcd287fc..b3fc1a83617 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt @@ -36,7 +36,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ StartPartitionSagaCommandCorrelationIdAccessor_64596642731407e6.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_566e8fad769818c2.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_90ab90feac5d44a9.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -66,21 +66,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class CorrelationIdAsStringAccessor_566e8fad769818c2 : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class CorrelationIdAsStringAccessor_90ab90feac5d44a9 : NServiceBus.Sagas.CorrelationPropertyAccessor { - CorrelationIdAsStringAccessor_566e8fad769818c2() { } + CorrelationIdAsStringAccessor_90ab90feac5d44a9() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.PartitionedEndpointSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_CorrelationId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.PartitionedEndpointSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.PartitionedEndpointSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_CorrelationId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.PartitionedEndpointSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_566e8fad769818c2(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_90ab90feac5d44a9(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt index 464bc193ff3..a56c5b2f9e7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RegistrationMethodNamePatterns.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt index 6f585f24d40..6000f2c3c7e 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassEntryPointName.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt index 90a1263b548..edbb57111e7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.RootClassVisibilityAndNamespace.approved.txt @@ -37,7 +37,7 @@ namespace CustomRegistrations OrderBilledOrderIdAccessor_6b40737e550b4237.Instance, OrderPlacedOrderIdAccessor_92819afdda929c86.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_828f31182d7d1121.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -84,21 +84,21 @@ namespace CustomRegistrations [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_828f31182d7d1121 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_828f31182d7d1121() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_828f31182d7d1121(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt index 7fdf5d741d2..43663e2e5d1 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationError.approved.txt @@ -40,7 +40,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -70,21 +70,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_1073497622a938fb() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt index dc7542e5795..51051bbec47 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaGeneratorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt @@ -35,7 +35,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration); @@ -63,21 +63,21 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt index 33346c87b2b..7e44b65f9db 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.BasicSagas.approved.txt @@ -43,7 +43,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_f7dc3394c771851e.Instance, OrderPlacedOrderIdAccessor_ae6ad694572b9fcb.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5b84242b5a03d625.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -70,7 +70,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_08b7b00354e1d041.Instance, OrderPlacedOrderIdAccessor_13dea9c2dc628538.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_1073497622a938fb.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -98,7 +98,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_2fd27f5436ef20b9.Instance, OrderPlacedOrderIdAccessor_c561311f0fcc5cf0.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_6d931b1825e0c4ca.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -198,20 +198,58 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5b84242b5a03d625 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5b84242b5a03d625() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Billing.OrderBillingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Billing.OrderBillingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Billing.OrderBillingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5b84242b5a03d625(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_1073497622a938fb : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_1073497622a938fb() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_1073497622a938fb(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_6d931b1825e0c4ca : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_6d931b1825e0c4ca() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Payments.PaymentsPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Payments.PaymentsPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Payments.PaymentsPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Payments.PaymentsPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_6d931b1825e0c4ca(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt index cd1210d4c17..8df0da1fb82 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.CastSyntaxWrappers.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt index 1280eb58a00..f966e69c5c8 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.DuplicatePropertyDefinitionsWithCompilationErrors.approved.txt @@ -45,7 +45,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -73,20 +73,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt index cd1210d4c17..8df0da1fb82 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.ExpressionBodiedConfigureHowToFindSaga.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt index 53c7e23bd80..575988c4066 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InheritedMessageProperty.approved.txt @@ -43,7 +43,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -87,20 +87,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt index 2b682c353e5..7be1504be11 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.InvalidMappingWithCompilationErrors.approved.txt @@ -46,7 +46,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -76,20 +76,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt index 4bd8988b7ae..3625190fe64 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NestedSagas.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_f53ba98a08e6a113.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -71,7 +71,7 @@ namespace NServiceBus OrderBilledOrderIdAccessor_b1eaacebc632acfc.Instance, OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_14af697526a7e650.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -115,20 +115,39 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_f53ba98a08e6a113 : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_f53ba98a08e6a113() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::Orders.Shipping.AnotherOuterClass.InnerClass.OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_f53ba98a08e6a113(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_14af697526a7e650 : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_14af697526a7e650() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::Orders.Shipping.OuterClass.OrderShippingPolicyData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::Orders.Shipping.OuterClass.OrderShippingPolicyData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_14af697526a7e650(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt index bee3c640509..20e177e79b7 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMappings.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt index 939a7eefc53..8b82b283923 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.NullableReferenceTypeMixedMappings.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NullableMessageOrderIdAccessor_9894122651576960.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_5e4ee6a1b6193efa.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -64,7 +64,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ NonNullableMessageOrderIdAccessor_e023774f080a36b1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_acfa87ac0bc7f48b.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -106,20 +106,39 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_5e4ee6a1b6193efa : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_5e4ee6a1b6193efa() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NonNullableSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::NonNullableSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NonNullableSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::NonNullableSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_5e4ee6a1b6193efa(); + } + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] + file sealed class OrderIdAsStringAccessor_acfa87ac0bc7f48b : NServiceBus.Sagas.CorrelationPropertyAccessor + { + OrderIdAsStringAccessor_acfa87ac0bc7f48b() { } + + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::NullableSagaData)sagaData); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] + static extern string AccessFrom_Property(global::NullableSagaData sagaData); + + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::NullableSagaData)sagaData, ((string)value)); + + [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] + static extern void WriteTo_Property(global::NullableSagaData sagaData, string value); + + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_acfa87ac0bc7f48b(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt index ac792a61d81..db03f8c2565 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.PrimaryConstructorAndSyntaxWrappers.approved.txt @@ -41,7 +41,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ StartPartitionSagaCommandCorrelationIdAccessor_82483382109e24ab.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_566e8fad769818c2.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, CorrelationIdAsStringAccessor_14cc7db5f4370166.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -69,20 +69,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class CorrelationIdAsStringAccessor_566e8fad769818c2 : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class CorrelationIdAsStringAccessor_14cc7db5f4370166 : NServiceBus.Sagas.CorrelationPropertyAccessor { - CorrelationIdAsStringAccessor_566e8fad769818c2() { } + CorrelationIdAsStringAccessor_14cc7db5f4370166() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::PartitionedEndpointSagaData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_CorrelationId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::PartitionedEndpointSagaData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::PartitionedEndpointSagaData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_CorrelationId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::PartitionedEndpointSagaData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_566e8fad769818c2(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new CorrelationIdAsStringAccessor_14cc7db5f4370166(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt index 65e1d2b5d70..e035f56dc56 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationError.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -72,20 +72,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt index 81382fd821c..d0af0ef91ee 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/ApprovalFiles/AddSagaInterceptorTests.UnrelatedCompilationErrorInDifferentFile.approved.txt @@ -44,7 +44,7 @@ namespace NServiceBus NServiceBus.Sagas.MessagePropertyAccessor[] propertyAccessors = [ OrderPlacedOrderIdAccessor_5f69124e5a7de2e1.Instance, ]; - var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_4a76700cf8410b3a.Instance, propertyAccessors); + var metadata = NServiceBus.Sagas.SagaMetadata.Create(associatedMessages, OrderIdAsStringAccessor_912ea78b27a6e1ec.Instance, propertyAccessors); sagaMetadataCollection.Add(metadata); var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); @@ -72,20 +72,20 @@ namespace NServiceBus [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")] - file sealed class OrderIdAsStringAccessor_4a76700cf8410b3a : NServiceBus.Sagas.CorrelationPropertyAccessor + file sealed class OrderIdAsStringAccessor_912ea78b27a6e1ec : NServiceBus.Sagas.CorrelationPropertyAccessor { - OrderIdAsStringAccessor_4a76700cf8410b3a() { } + OrderIdAsStringAccessor_912ea78b27a6e1ec() { } - public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData); + public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property((global::OrderShippingPolicyData)sagaData); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "get_OrderId")] - static extern string AccessFrom_Property(NServiceBus.IContainSagaData sagaData); + static extern string AccessFrom_Property(global::OrderShippingPolicyData sagaData); - public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, ((string)value)); + public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property((global::OrderShippingPolicyData)sagaData, ((string)value)); [global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_OrderId")] - static extern string WriteTo_Property(NServiceBus.IContainSagaData sagaData, string value); + static extern void WriteTo_Property(global::OrderShippingPolicyData sagaData, string value); - public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_4a76700cf8410b3a(); + public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new OrderIdAsStringAccessor_912ea78b27a6e1ec(); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs index e2ef8af53f7..263ea41a1a3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerGeneratorTests.cs @@ -458,6 +458,80 @@ public class Cmd1 : ICommand {} .AssertRunsAreEqual(); } + [Test] + public void ConventionBasedHandlerWithKeyedServices() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.ConventionBasedHandlerWithKeyedServicesAssembly.AddAll(); + } + } + + [Handler] + public class OrderShippedHandler + { + public OrderShippedHandler([FromKeyedServices("CtorKey")] IMyService ctorService) {} + + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Keys.HandlerKey)] IMyService methodService, [FromKeyedServices("MethodKey")] IMyService methodService2, [FromKeyedServices(key: "NamedKey")] IMyService methodService3, [FromKeyedServices(nameof(Cmd1))] IMyService methodService4, [FromKeyedServices(typeof(Cmd1))] IMyService methodService5, [FromKeyedServices(null)] IMyService methodService6, [FromKeyedServices(ServiceKey.First)] IMyService methodService7, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + public static class Keys { public const string HandlerKey = "ConstKey"; } + public enum ServiceKey { None = 0, First = 1, Second = 2 } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void ConventionBasedHandlerWithKeyedServicesInAnotherNamespace() + { + var source = """ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.ConventionBasedHandlerWithKeyedServicesInAnotherNamespaceAssembly.AddAll(); + } + } + + [Handler] + public class OrderShippedHandler + { + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Security.KeyVault.HandlerKey)] IMyService service) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + namespace Security { public static class KeyVault { public const string HandlerKey = "SecretKey"; } } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void ConventionBasedHandlerCtorAndMethodInjectionWithSameParameterName() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs index cfd120cd556..4df73114cc4 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorSuppressorTests.cs @@ -42,6 +42,40 @@ public class SampleCommand : ICommand { } Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL3050ForAddHandler() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + public class SampleHandler : IHandleMessages + { + public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SampleCommand : ICommand { } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + [Test] public void DoesNotSuppressIL2026ForNonAddHandlerCalls() { @@ -74,4 +108,37 @@ public void SomeOtherMethod() { } Assert.That(diagnostics, Does.Contain("IL2026")); } + + [Test] + public void DoesNotSuppressIL3050ForNonAddHandlerCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL3050 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresDynamicCode("Test method")] + public void SomeOtherMethod() { } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL3050")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs index 6fb222ae10b..211d60bbf98 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/AddHandlerInterceptorTests.cs @@ -262,6 +262,78 @@ public class Cmd1 : ICommand {} .AssertRunsAreEqual(); } + [Test] + public void ConventionBasedHandlerWithKeyedServices() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + [Handler] + public class OrderShippedHandler + { + public OrderShippedHandler([FromKeyedServices("CtorKey")] IMyService ctorService) {} + + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Keys.HandlerKey)] IMyService methodService, [FromKeyedServices("MethodKey")] IMyService methodService2, [FromKeyedServices(key: "NamedKey")] IMyService methodService3, [FromKeyedServices(nameof(Cmd1))] IMyService methodService4, [FromKeyedServices(typeof(Cmd1))] IMyService methodService5, [FromKeyedServices(null)] IMyService methodService6, [FromKeyedServices(ServiceKey.First)] IMyService methodService7, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + public static class Keys { public const string HandlerKey = "ConstKey"; } + public enum ServiceKey { None = 0, First = 1, Second = 2 } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void ConventionBasedHandlerWithKeyedServicesInAnotherNamespace() + { + var source = """ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddHandler(); + } + } + + [Handler] + public class OrderShippedHandler + { + public Task Handle(Cmd1 message, IMessageHandlerContext context, [FromKeyedServices(Security.KeyVault.HandlerKey)] IMyService service) => Task.CompletedTask; + } + + public interface IMyService {} + public class Cmd1 : ICommand {} + namespace Security { public static class KeyVault { public const string HandlerKey = "SecretKey"; } } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void ConventionBasedHandlerWithMultipleConstructorsUsesMostGreedyConstructor() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs index 7285e9ac380..3bbb5edbaef 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Handlers/HandlerAttributeAnalyzerTests.cs @@ -659,6 +659,140 @@ class MyMessage : IMessage {} return Assert(source); } + [Test] + public Task DoesNotReportMixedStyleForHelperMethodNamedHandleWithNonMessageFirstParameter() + { + // A pure interface-based handler with an unrelated public helper overload that happens to be + // named Handle and takes IMessageHandlerContext. The helper is called from the interface Handle + // method, so it is not a convention-based handler — this is not a mixed style. + var source = + """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Handle("text", context); + + public Task Handle(string text, IMessageHandlerContext context, CancellationToken cancellation = default) => + Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperWithUserDefinedTypeCalledFromInterfaceHandle() + { + // A helper with a user-defined DTO as first param that is called from the interface Handle method. + // The call-site analysis detects the invocation, so this is not a mixed style. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + class MyDto {} + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Handle(new MyDto(), context); + + public Task Handle(MyDto dto, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperCalledFromBlockBodiedInterfaceHandle() + { + // Same pattern but with a block-bodied interface Handle method. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public async Task Handle(MyMessage message, IMessageHandlerContext context) + { + await Handle("text", context); + } + + public Task Handle(string text, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + """; + + return Assert(source); + } + + [Test] + public Task ReportsMixedStyleWhenConventionBasedMethodNotCalledFromInterfaceHandle() + { + // A convention-based Handle method that is NOT called from the interface Handle method + // should still be reported as mixed style. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class [|MyHandler|] : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => Task.CompletedTask; + + public Task Handle(OtherMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + class MyMessage : IMessage {} + class OtherMessage : IMessage {} + """; + + return Assert(source, DiagnosticIds.ConventionBasedHandlerMixedStyle); + } + + [Test] + public Task DoesNotReportMixedStyleForHelperPassingMessageProperties() + { + // A helper that receives individual properties extracted from the message. + // The call-site analysis detects it is called from the interface Handle method. + var source = + """ + using System.Threading.Tasks; + using NServiceBus; + + [Handler] + class MyHandler : IHandleMessages + { + public Task Handle(MyMessage message, IMessageHandlerContext context) => + Handle(message.Id, message.Name, context); + + public Task Handle(string id, string name, IMessageHandlerContext context) => + Task.CompletedTask; + } + + class MyMessage : IMessage + { + public string Id { get; set; } + public string Name { get; set; } + } + """; + + return Assert(source); + } + [Test] public Task DoesNotReportConventionBasedHandlerImplementingUnrelatedInterfaceWithHandleMethod() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs index afbbfa97f13..9899d573541 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Helpers/MockTrimmingAnalyzer.cs @@ -6,7 +6,7 @@ namespace NServiceBus.Core.Analyzer.Tests.Helpers; using Microsoft.CodeAnalysis.Diagnostics; // Currently, this mock analyzer does not support all trimming-related warnings. -// It only supports IL2026 for method invocations for now. +// It only supports IL2026 and IL3050 for method invocations for now. #pragma warning disable RS1001 // Yes we don't want it to be found class MockTrimmingAnalyzer : DiagnosticAnalyzer #pragma warning restore RS1001 @@ -21,7 +21,17 @@ class MockTrimmingAnalyzer : DiagnosticAnalyzer defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true); - public override ImmutableArray SupportedDiagnostics => [IL2026Descriptor]; + static readonly DiagnosticDescriptor IL3050Descriptor = new( +#pragma warning disable RS2008 + id: "IL3050", +#pragma warning restore RS2008 + title: "Using member with RequiresDynamicCodeAttribute", + messageFormat: "Using member '{0}' which has 'RequiresDynamicCodeAttribute'", + category: "AOT", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + public override ImmutableArray SupportedDiagnostics => [IL2026Descriptor, IL3050Descriptor]; public override void Initialize(AnalysisContext context) { @@ -36,13 +46,16 @@ public override void Initialize(AnalysisContext context) } var method = invocation.TargetMethod; - if (!method.GetAttributes().Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute")) + var attributes = method.GetAttributes(); + if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresUnreferencedCodeAttribute")) { - return; + operationContext.ReportDiagnostic(Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name)); } - var diagnostic = Diagnostic.Create(IL2026Descriptor, invocation.Syntax.GetLocation(), method.Name); - operationContext.ReportDiagnostic(diagnostic); + if (attributes.Any(attr => attr.AttributeClass?.Name == "RequiresDynamicCodeAttribute")) + { + operationContext.ReportDiagnostic(Diagnostic.Create(IL3050Descriptor, invocation.Syntax.GetLocation(), method.Name)); + } }, OperationKind.Invocation); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs new file mode 100644 index 00000000000..27a69fa4afd --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorSuppressorTests.cs @@ -0,0 +1,114 @@ +namespace NServiceBus.Core.Analyzer.Tests.Messages; + +using Analyzer.Messages; +using Helpers; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class AddMessageTypeInterceptorSuppressorTests +{ + [Test] + public void SuppressesIL2026ForAddMessageType() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + public class SampleMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL2026")); + } + + [Test] + public void DoesNotSuppressIL2026ForAddMessageTypeWithGenericTypeParameter() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + Register(cfg); + } + + // The type argument is a generic type parameter, so no interceptor can be generated + // and the RequiresUnreferencedCode fallback warning must not be suppressed. + public void Register(EndpointConfiguration cfg) where TMessage : IMessage + { + cfg.AddMessageType(); + } + } + + public class MyMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL2026")); + } + + [Test] + public void DoesNotSuppressIL2026ForNonAddMessageTypeCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL2026 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresUnreferencedCode("Test method")] + public void SomeOtherMethod() { } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL2026")); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs new file mode 100644 index 00000000000..3c6d9a2548b --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Messages/AddMessageTypeInterceptorTests.cs @@ -0,0 +1,165 @@ +namespace NServiceBus.Core.Analyzer.Tests.Messages; + +using Analyzer.Messages; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class AddMessageTypeInterceptorTests +{ + [Test] + public void BasicMessageTypes() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + cfg.AddMessageType(); + // Duplicate call, methods should be deduped with 2 InterceptsLocation attributes + cfg.AddMessageType(); + } + } + + namespace Messages + { + public class OrderPlaced : IEvent + { + public string OrderId { get; set; } + } + + public class OrderBilled : IEvent + { + public string OrderId { get; set; } + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void MessageTypesWithEqualRankInterfaces() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + namespace Messages + { + // Declared in reverse-alphabetical order: equal-rank interfaces must keep declaration order + // (matching runtime reflection and handler generation) rather than being alphabetically reordered. + public class OrderAccepted : OrderEventBase, ISecond, IFirst + { + } + + public class OrderEventBase : IEvent + { + public string OrderId { get; set; } + } + + public interface IFirst : IEvent + { + } + + public interface ISecond : IEvent + { + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void MessageTypesWithHierarchy() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + cfg.AddMessageType(); + } + } + + namespace Messages + { + public class OrderAccepted : OrderEventBase, IOrderEvent + { + } + + public class OrderRejected : OrderEventBase + { + } + + public class OrderEventBase : IEvent + { + public string OrderId { get; set; } + } + + public interface IOrderEvent : IEvent + { + } + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + + [Test] + public void Interceptors_are_generated_in_warning_only_trim_analyzer_builds() + { + var source = """ + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddMessageType(); + } + } + + public class MyMessage : IEvent + { + public string OrderId { get; set; } + } + """; + + // Interceptor support must be generated in warning-only (EnableTrimAnalyzer) builds so the reflection + // fallback warning can be suppressed there. + var output = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithProperty("build_property.EnableTrimAnalyzer", "true") + .Run() + .GetCompilationOutput(); + + Assert.That(output, Does.Contain("InterceptionsOfAddMessageTypeMethod.g.cs")); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs new file mode 100644 index 00000000000..669b1eaf181 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -0,0 +1,3528 @@ +#nullable enable + +#pragma warning disable NUnit1034 // Base TestFixtures should be abstract + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class MessagingMigrationAnalyzerTests : AnalyzerTestFixture +{ + protected override void ConfigureFixtureTests(AnalyzerTest test) + { + base.ConfigureFixtureTests(test); + test.WithProperty("build_property.PublishTrimmed", "true"); + } + + static AnalyzerTest MigrationTest(string source) => + AnalyzerTest.ForAnalyzer() + .WithSource(source); + + static readonly MetadataReference TestingFakesReference = + MetadataReference.CreateFromFile(typeof(NServiceBus.Testing.TestableMessageSession).Assembly.Location); + + static AnalyzerTest MigrationAuditTest(string source) => + MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true"); + + static AnalyzerTest FakeMigrationTest(string source) => + MigrationAuditTest(source).AddReferences(TestingFakesReference); + + // ===== NSB0039: Safe object creation ===== + + [Test] + public Task NSB0039_DirectObjectCreation_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await [|context.Reply(new MyMessage(), new ReplyOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.SendLocal(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionExtensionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Publish(new MyEvent())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.SendLocal(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextExtensionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Publish(new MyEvent())|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_MessageProcessingContextExtensionReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await [|context.Reply(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await [|ReplyToOriginator(context, new MyMessage())|]; + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_SessionSendWithDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send("destination", new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_DirectObjectCreation_PipelineContextSendWithDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await [|context.Send("destination", new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_SealedTypeVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + sealed class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + MyMessage msg = new MyMessage(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_UnsealedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_ValueType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + struct MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + MyMessage msg = new MyMessage(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_NullableValueType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_MessageCreatorCreateInstance() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessageCreator creator) + { + await [|session.Send(creator.CreateInstance())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MessageCreatorCreateInstanceWithAction() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessageCreator creator) + { + await [|session.Send(creator.CreateInstance(_ => { }))|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_UpdateMessageDirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System.Threading.Tasks; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + [|context.UpdateMessage(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + // ===== NSB0040: Potentially polymorphic ===== + + [Test] + public Task NSB0040_UpdateMessageVarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class MyMessage : IMessage { } + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + var message = new MyMessage(); + [|context.UpdateMessage(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageCreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, IMessageCreator creator) + { + [|context.UpdateMessage(creator.CreateInstance())|]; + } + } + + public interface IMyMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageSealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, MyMessage message) + { + [|context.UpdateMessage(message)|]; + } + } + + public sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_InterfaceVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, IMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_BaseClassVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class BaseMessage : IMessage { } + class DerivedMessage : BaseMessage { } + + class Foo + { + async Task Bar(IMessageSession session, BaseMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UnsealedClassVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session, MyMessage msg) + { + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ReassignedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + message = new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_RefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + Replace(ref message); + await [|session.Send(message)|]; + } + + static void Replace(ref MyMessage message) => message = new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_NestedInvocationWithEarlierRefMutation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await Task.WhenAll(Mutate(ref message), [|session.Send(message)|]); + } + + static Task Mutate(ref MyMessage message) + { + message = new DerivedMessage(); + return Task.CompletedTask; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ReceiverRefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|Replace(ref message, session).Send(message)|]; + } + + static IMessageSession Replace(ref MyMessage message, IMessageSession session) + { + message = new DerivedMessage(); + return session; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_LocalFunctionReceiverMutatesCapturedVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + class DerivedMessage : MyMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + + IMessageSession GetSession() + { + message = new DerivedMessage(); + return session; + } + + await [|GetSession().Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_EarlierArgumentRefUseOfVarObjectCreation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage + { + public string Destination { get; } = "destination"; + } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await [|session.Send(message.Destination, message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_ConditionalVarInitializer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session, bool condition) + { + var message = condition ? new MyMessage() : new MyMessage(); + await [|session.Send(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_InvocationVarInitializer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = CreateMessage(); + await [|session.Send(message)|]; + } + + MyMessage CreateMessage() => new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodReturnType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var msg = GetMessage(); + await [|session.Send(msg)|]; + } + + MyMessage GetMessage() => new MyMessage(); + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Mutator context setters ===== + + [Test] + public Task NSB0039_MutatorIncomingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + [|context.Message = new MyMessage()|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorOutgoingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + [|context.OutgoingMessage = new MyEvent()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorIncomingContext_ValueType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + [|context.Message = message|]; + } + } + + struct MyValue : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + var message = new MyMessage(); + [|context.Message = message|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + var message = new MyEvent(); + [|context.OutgoingMessage = message|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_SealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyMessage message) + { + [|context.Message = message|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_CreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context, IMessageCreator creator) + { + [|context.OutgoingMessage = creator.CreateInstance()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NoDiagnostic_MutatorContext_ObjectType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new object(); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MutatorContext_UnrelatedMessageProperty() + { + var source = + """ + using NServiceBus; + + class Foo + { + public object Message { get; set; } + + void Bar(Foo foo) + { + foo.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + // ===== NSB0041: Generic T == object ===== + + [Test] + public Task NSB0041_GenericTIsObject_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Send(msg, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Publish(msg, new PublishOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object msg = new object(); + await [|context.Send(msg, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + object msg = new object(); + await [|context.Reply(msg, new ReplyOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object msg = new object(); + await [|session.Send(msg)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + object msg = new object(); + await [|ReplyToOriginator(context, msg)|]; + } + } + + class MySagaData : ContainSagaData { } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_GenericTIsObject_UpdateMessage() + { + var source = + """ + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context, object message) + { + [|context.UpdateMessage(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + // ===== UpdateMessageInstance on IIncomingLogicalMessageContext ===== + + [Test] + public Task NSB0039_UpdateMessageInstanceDirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System.Threading.Tasks; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceVarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class MyMessage : IMessage { } + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + var message = new MyMessage(); + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceCreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, IMessageCreator creator) + { + [|context.UpdateMessageInstance(creator.CreateInstance())|]; + } + } + + public interface IMyMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceSealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, MyMessage message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + + public sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_GenericTIsObject_UpdateMessageInstance() + { + var source = + """ + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, object message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NoDiagnostic_UpdateMessageInstance_UnrelatedMethod() + { + var source = + """ + using NServiceBus; + + class Helper + { + public void UpdateMessageInstance(object message) { } + } + + class Foo + { + void Bar(Helper helper, MyMessage message) + { + helper.UpdateMessageInstance(message); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NSB0039_TestableIncomingLogicalMessageContext_UpdateMessageInstance() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + + class Foo + { + void Bar(TestableIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + // ===== Method groups and delegates ===== + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionPublish() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func publish = [|session.Publish|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionExtensionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = [|session.SendLocal|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IPipelineContext context) + { + Func send = [|context.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IPipelineContext context) + { + Func publish = [|context.Publish|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageProcessingContext context) + { + Func reply = [|context.Reply|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_SagaReplyToOriginator() + { + var source = + """ + using NServiceBus; + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + void Bar(IMessageHandlerContext context) + { + Func, Task> reply = [|ReplyToOriginator|]; + } + } + + class MySagaData : ContainSagaData { } + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_CustomDelegate_PositionalMessageParameter() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + delegate Task MySendDelegate(MyMessage msg, SendOptions options, CancellationToken cancellationToken); + + class Foo + { + void Bar(IMessageSession session) + { + MySendDelegate send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MethodGroup_SealedMessage_AsArgument() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler([|session.Send|]); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_MethodGroup_InterfaceMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_UnsealedMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + + class UnsealedMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_ObjectMessage_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MethodGroup_ObjectMessage_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = [|session.SendLocal|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_MethodGroup_ExplicitGenericObject_SessionSend() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0041_MethodGroup_IsReportedWithoutMigrationAudit() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return MigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NoDiagnostic_MethodGroup_ExplicitGenericNonObject() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MethodGroup_UnrelatedMethod() + { + var source = + """ + using System; + using System.Threading.Tasks; + + class Helper + { + public Task DoSomething(object message) => Task.CompletedTask; + } + + class Foo + { + void Bar(Helper helper) + { + Func action = helper.DoSomething; + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MethodGroup_NonMessagingMember() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func subscribe = session.Subscribe; + } + } + """; + return Assert(source); + } + + // ===== Concrete implementations and testing fakes ===== + + [Test] + public Task NSB0039_TestableMessageSession_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_TestableMessageSession_InterfaceMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_TestableMessageSession_ExplicitGenericObject() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session, object message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NSB0039_TestableMessageSession_MethodGroup() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(TestableMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestablePipelineContext_Send() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestablePipelineContext context) + { + await [|context.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestablePipelineContext_Publish() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestablePipelineContext context) + { + await [|context.Publish(new MyEvent(), new PublishOptions())|]; + } + } + + class MyEvent : IEvent { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestableMessageProcessingContext_Reply() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageProcessingContext context) + { + await [|context.Reply(new MyMessage(), new ReplyOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_TestableOutgoingLogicalMessageContext_UpdateMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + + class Foo + { + void Bar(TestableOutgoingLogicalMessageContext context) + { + [|context.UpdateMessage(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_CustomMessageSession_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_InterfaceMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_CustomMessageSession_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_CustomMessageSession_ImplementationOnBaseClass() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class DerivedMessageSession : CustomMessageSession { } + + class Foo + { + async Task Bar(DerivedMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_ExplicitMessageSession_InvokedThroughInterface() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class ExplicitMessageSession : IMessageSession + { + Task IMessageSession.Send(object message, SendOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Publish(object message, PublishOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + Task IMessageSession.Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NoDiagnostic_NonContractSession() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + class NotAMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(NotAMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } + + [Test] + public Task NoDiagnostic_ImplementingSession_UnrelatedOverload() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(object message) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } + + // ===== Generic forwarding ===== + + [Test] + public Task NSB0040_GenericForwarding_Unconstrained() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_ClassConstraint() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) where T : class + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_InterfaceConstraint() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Forward(T message, IMessageSession session) where T : IMyMessage + { + await [|session.Send(message, new SendOptions())|]; + } + } + + interface IMyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_GenericForwarding_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Forward(T message, IMessageSession session) where T : class + { + Func send = [|session.Send|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Inferred versus explicit object ===== + + [Test] + public Task NoDiagnostic_InferredObjectArgument_10xObjectOnlyOverload() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, object message) + { + await session.Send(message, new SendOptions()); + } + } + """; + return Assert(source); + } + + // ===== Expression trees ===== + + [Test] + public Task NSB0039_ExpressionTree_SealedMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Expression> expression = message => [|session.Send(message)|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_ExpressionTree_UnsealedMessage() + { + var source = + """ + using NServiceBus; + using System; + using System.Linq.Expressions; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Expression> expression = message => [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Activation ===== + + [Test] + public Task NSB0041_IsReportedWithoutMigrationAudit() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task MigrationDiagnostics_AreInactiveByDefault() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishTrimmed() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishTrimmed_EnablesNSB0040() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return MigrationTest(source).WithProperty("build_property.PublishTrimmed", "true") + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForPublishAot() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.PublishAot", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForIsAotCompatible() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.IsAotCompatible", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForIsTrimmable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.IsTrimmable", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForEnableTrimAnalyzer() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source).WithProperty("build_property.EnableTrimAnalyzer", "true") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForNSB0039EditorConfigSeverityOnly() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(new MyMessage())|]; + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Info) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AreEnabledForNSB0040EditorConfigSeverityOnly() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.RuntimeTypeMayDiffer, ReportDiagnostic.Warn) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsNSB0039NoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await session.Send(new MyMessage()); + await [|session.Send(message)|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsNSB0040NoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(new MyMessage())|]; + await session.Send(message); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.RuntimeTypeMayDiffer, ReportDiagnostic.Suppress) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_DefaultSeverityFallsBackToAutomatic() + { + const string source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_DefaultSeverityOnNSB0039KeepsNSB0040Enabled() + { + const string source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, int? message) + { + await [|session.Send(message)|]; + } + } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task MigrationDiagnostics_SeverityIsScopedToConfiguredSyntaxTree() + { + const string configuredSource = + """ + using NServiceBus; + using System.Threading.Tasks; + + class ConfiguredFoo + { + async Task Bar(IMessageSession session) + { + await session.Send(new ConfiguredMessage()); + } + } + + class ConfiguredMessage : IMessage { } + """; + const string unconfiguredSource = + """ + using NServiceBus; + using System.Threading.Tasks; + + class UnconfiguredFoo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new UnconfiguredMessage())|]; + } + } + + class UnconfiguredMessage : IMessage { } + """; + return AnalyzerTest.ForAnalyzer() + .WithSource(configuredSource, "Configured.cs") + .WithSource(unconfiguredSource, "Unconfigured.cs") + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress, "Configured.cs") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + // The global severity channel (TryGetGlobalDiagnosticValue) was previously invisible to the + // analyzer gate, so global severities that enable these diagnostics were ignored. + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsGlobalNoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Suppress) + .AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_GlobalSeverity_EnablesWithoutAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Warn) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_AutomaticActivation_RespectsBulkCategoryNoneSeverity() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "none") + .AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_BulkCategorySeverity_EnablesWithoutAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_BlocksBulkConfiguration() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_ExplicitGlobalDefaultSeverity_BlocksBulkConfiguration() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithGlobalDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .WithEditorConfigOption("dotnet_analyzer_diagnostic.category-NServiceBus.Code.severity", "warning") + .AssertDiagnostics(); + } + + [Test] + public Task MigrationDiagnostics_ExplicitPerRuleDefaultSeverity_StillHonorsAutomaticActivation() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await [|session.Send(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationTest(source) + .WithProperty("build_property.PublishTrimmed", "true") + .WithDiagnosticSeverity(DiagnosticIds.UseGenericMessageType, ReportDiagnostic.Default) + .AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + // ===== Negative tests ===== + + [Test] + public Task NoDiagnostic_ObjectCreation_NewObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new object(), new SendOptions()); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ObjectCreation_TargetTypedNewObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new(), new SendOptions()); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ObjectVariable() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session, object message) + { + await session.Send(message); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_AnonymousType() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new { Value = 1 }; + await session.Send(message); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitGenericNonObject() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ActionTOverload() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(_ => { }, new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_Subscribe() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Subscribe(typeof(MyEvent), new SubscribeOptions()); + } + } + + class MyEvent : IEvent { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_Unsubscribe() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Unsubscribe(typeof(MyEvent), new UnsubscribeOptions()); + } + } + + class MyEvent : IEvent { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_UnrelatedMethod() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await SomeOtherMethod(new MyMessage()); + } + + Task SomeOtherMethod(object msg) => Task.CompletedTask; + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ForwardCurrentMessageTo() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.ForwardCurrentMessageTo("destination"); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_RequestTimeout() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await RequestTimeout(context, TimeSpan.FromSeconds(10)); + } + } + + class MySagaData : ContainSagaData { } + class MyTimeout { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.Send(message, typeof(IMyInterface), new SendOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyEvent(); + await session.Publish(message, typeof(IMyInterface), new PublishOptions()); + } + } + + interface IMyInterface : IEvent { } + class MyEvent : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSendLocal() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.SendLocal(message, typeof(IMyInterface)); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_SessionSendDestination() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageSession session) + { + object message = new MyMessage(); + await session.Send("destination", message, typeof(IMyInterface)); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_PipelineContextSend() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object message = new MyMessage(); + await context.Send(message, typeof(IMyInterface), new SendOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyMessage : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_PipelineContextPublish() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IPipelineContext context) + { + object message = new MyEvent(); + await context.Publish(message, typeof(IMyInterface), new PublishOptions()); + } + } + + interface IMyInterface : IEvent { } + class MyEvent : IMyInterface { } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_ExplicitType_MessageProcessingContextReply() + { + var source = + """ + using NServiceBus; + using System.Threading.Tasks; + using System; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + object message = new MyReply(); + await context.Reply(message, typeof(IMyInterface), new ReplyOptions()); + } + } + + interface IMyInterface : IMessage { } + class MyReply : IMyInterface { } + """; + return Assert(source); + } + + // ===== UpdateMessage method groups ===== + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_SealedMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + + sealed class SealedMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_ObjectMessage() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageMethodGroup_TestableFake() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using NServiceBus.Testing; + using System; + + class Foo + { + void Bar(TestableOutgoingLogicalMessageContext context) + { + Action update = [|context.UpdateMessage|]; + } + } + + sealed class SealedMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + // ===== Renamed implementation parameters ===== + + [Test] + public Task NSB0039_CustomMessageSession_RenamedParameter_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await [|session.Send(new MyMessage(), new SendOptions())|]; + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_RenamedParameter_InterfaceArgument() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, IMessage message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0039_CustomMessageSession_RenamedParameter_MethodGroup() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_CustomMessageSession_RenamedParameter_MethodGroupInterface() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = [|session.Send|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_CustomMessageSession_RenamedParameter_ExplicitGenericObject() + { + var source = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(T payload, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(T payload, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session, object message) + { + await [|session.Send(message, new SendOptions())|]; + } + } + """; + return MigrationAuditTest(source).AssertDiagnostics(DiagnosticIds.GenericMessageTypeIsObject); + } + + // ===== Interface-map gate ===== + + [Test] + public Task NoDiagnostic_InterfaceRichType_UnrelatedSendMethod() + { + var source = + """ + using NServiceBus; + using System.Threading; + using System.Threading.Tasks; + + interface IMarkerA { void A(); } + interface IMarkerB { void B(); } + interface IMarkerC : IMarkerA, IMarkerB { void C(); } + + class InterfaceRichType : IMarkerC + { + public void A() { } + public void B() { } + public void C() { } + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(InterfaceRichType session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + return MigrationAuditTest(source).AssertDiagnostics(); + } +} + +#pragma warning restore NUnit1034 diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs new file mode 100644 index 00000000000..d9260e37460 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -0,0 +1,1129 @@ +#pragma warning disable NUnit1034 // Base TestFixtures should be abstract + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer.Fixes; +using NUnit.Framework; +using Particular.AnalyzerTesting; + +[TestFixture] +public class MessagingMigrationFixerTests : CodeFixTestFixture +{ + static readonly MetadataReference TestingFakesReference = + MetadataReference.CreateFromFile(typeof(NServiceBus.Testing.TestableMessageSession).Assembly.Location); + + protected override void ConfigureFixtureTests(CodeFixTest test) + { + base.ConfigureFixtureTests(test); + test.WithProperty("build_property.PublishTrimmed", "true"); + test.AddReferences(TestingFakesReference); + } + + [Test] + public Task SessionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent(), new PublishOptions()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent(), new PublishOptions()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task UnsealedVarObjectCreation() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await session.Send(message); + } + } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MyMessage : IMessage { } + + class Foo + { + async Task Bar(IMessageSession session) + { + var message = new MyMessage(); + await session.Send(message); + } + } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextSendLocal() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MessageProcessingContextReply() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage(), new ReplyOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage(), new ReplyOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionSendLocal() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.SendLocal(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionExtensionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextExtensionSend() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Send(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task PipelineContextExtensionPublish() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IPipelineContext context) + { + await context.Publish(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MessageProcessingContextExtensionReply() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageProcessingContext context) + { + await context.Reply(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task NoFixForNewObject() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new object(), new SendOptions()); + } + } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForTargetTypedNewObject() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send(new(), new SendOptions()); + } + } + """; + + return Assert(original, original); + } + + [Test] + public Task SagaReplyToOriginator() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await ReplyToOriginator(context, new MyMessage()); + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class MySaga : Saga + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) { } + + async Task Bar(IMessageHandlerContext context) + { + await ReplyToOriginator(context, new MyMessage()); + } + } + + class MySagaData : ContainSagaData { } + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task SessionSendWithDestination() + { + var original = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send("destination", new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(IMessageSession session) + { + await session.Send("destination", new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task UpdateMessage() + { + var original = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + context.UpdateMessage(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IOutgoingLogicalMessageContext context) + { + context.UpdateMessage(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task UpdateMessageInstance() + { + var original = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorIncomingContext_Message() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorOutgoingContext_OutgoingMessage() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.OutgoingMessage = new MyEvent(); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorIncomingContext_MessageValueType() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.Message = message; + } + } + + struct MyValue : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.UpdateMessageInstance(message); + } + } + + struct MyValue : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_SessionSend() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_SessionSendLocal() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = session.SendLocal; + } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func sendLocal = session.SendLocal; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MethodGroup_AsArgument() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler(session.Send); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + RegisterHandler(session.Send); + } + + void RegisterHandler(Func handler) { } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task NoFixForExplicitGenericMethodGroup() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class Foo + { + void Bar(IMessageSession session) + { + Func send = session.Send; + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForDIMReliantConcreteImplementation() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task NoFixForDIMReliantConcreteImplementation_MethodGroup() + { + var original = + """ + using NServiceBus; + using System; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + void Bar(CustomMessageSession session) + { + Func send = session.Send; + } + } + + sealed class MyMessage : IMessage { } + """; + + return Assert(original, original); + } + + [Test] + public Task ConcreteImplementationWithGenericOverload() + { + var original = + """ + using NServiceBus; + using System; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Send(T message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Publish(T message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using System; + using System.Runtime.CompilerServices; + using System.Threading; + using System.Threading.Tasks; + + class CustomMessageSession : IMessageSession + { + public Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Send(T message, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + [OverloadResolutionPriority(-1)] + public Task Publish(T message, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Publish(Action messageConstructor, PublishOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Subscribe(Type eventType, SubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions options, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class Foo + { + async Task Bar(CustomMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task TestableMessageSession() + { + var original = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Testing; + using System.Threading.Tasks; + + class Foo + { + async Task Bar(TestableMessageSession session) + { + await session.Send(new MyMessage(), new SendOptions()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } +} + +#pragma warning restore NUnit1034 diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs new file mode 100644 index 00000000000..87addb7bba5 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationSyntaxTests.cs @@ -0,0 +1,40 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Tests; + +using System.Linq; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework; + +[TestFixture] +public class MessagingMigrationSyntaxTests +{ + // V11 needs syntax to distinguish explicit from inferred object. + static bool HasExplicitTypeArgumentList(InvocationExpressionSyntax invocation) => + invocation.Expression switch + { + GenericNameSyntax genericName => genericName.TypeArgumentList.Arguments.Count > 0, + MemberAccessExpressionSyntax { Name: GenericNameSyntax name } => name.TypeArgumentList.Arguments.Count > 0, + MemberBindingExpressionSyntax { Name: GenericNameSyntax name } => name.TypeArgumentList.Arguments.Count > 0, + _ => false + }; + + [TestCase("session.Send(message)", true)] + [TestCase("session.Send(message)", false)] + [TestCase("Send(message)", true)] + [TestCase("Send(message)", false)] + [TestCase("session.Send(message)", true)] + [TestCase("session?.Send(message)", true)] + [TestCase("session?.Send(message)", false)] + public void HasExplicitTypeArgumentList(string expression, bool expected) + { + var source = $"class C {{ void M() {{ {expression}; }} }}"; + var tree = CSharpSyntaxTree.ParseText(source); + var invocation = tree.GetRoot() + .DescendantNodes() + .OfType() + .Single(); + Assert.That(HasExplicitTypeArgumentList(invocation), Is.EqualTo(expected)); + } +} diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj index 0e21071087f..c904edc451d 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/NServiceBus.Core.Analyzer.Tests.Roslyn5.csproj @@ -8,6 +8,7 @@ + @@ -15,12 +16,12 @@ - - + + - - - + + + diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs index 59beefb9597..29f01011ae3 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaGeneratorTests.cs @@ -7,6 +7,61 @@ [TestFixture] public class AddSagaGeneratorTests { + [Test] + public void FinderOnlySagas() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + using NServiceBus.Persistence; + using NServiceBus.Extensibility; + using NServiceBus.Sagas; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.FinderOnlySagasAssembly.AddAll(); + } + } + + namespace FinderOnly + { + [Saga] + public class FinderOnlySaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureFinderMapping(); + } + + public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class FinderOnlySagaData : ContainSagaData + { + public string Property { get; set; } + } + + public class FinderOnlyFinder : ISagaFinder + { + public Task FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData)); + } + + public class StartSagaMessage : IMessage; + } + """; + + SourceGeneratorTest.ForIncrementalGenerator() + .WithIncrementalGenerator() + .WithSource(source, "test.cs") + .Run() + .Approve() + .AssertRunsAreEqual(); + } + [Test] public void BasicSagas() { diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs index ef0f6d5a498..abd3f764e32 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/AddSagaInterceptorSuppressorTests.cs @@ -57,6 +57,163 @@ public class SampleCommand : ICommand Assert.That(diagnostics, Does.Not.Contain("IL2026")); } + [Test] + public void SuppressesIL3050ForAddSaga() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddSaga(); + } + } + + public class SampleSaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.MapSaga(saga => saga.CorrelationId) + .ToMessage(msg => msg.CorrelationId); + } + + public Task Handle(SampleCommand cmd, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SampleSagaData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class SampleCommand : ICommand + { + public string CorrelationId { get; set; } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + + [Test] + public void SuppressesIL2026ForFinderOnlySaga() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + using NServiceBus.Persistence; + using NServiceBus.Extensibility; + using NServiceBus.Sagas; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddSaga(); + } + } + + public class FinderOnlySaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureFinderMapping(); + } + + public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class FinderOnlySagaData : ContainSagaData + { + public string Property { get; set; } + } + + public class FinderOnlyFinder : ISagaFinder + { + public Task FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData)); + } + + public class StartSagaMessage : IMessage; + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL2026")); + } + + [Test] + public void SuppressesIL3050ForFinderOnlySaga() + { + var source = """ + using System.Threading; + using System.Threading.Tasks; + using NServiceBus; + using NServiceBus.Persistence; + using NServiceBus.Extensibility; + using NServiceBus.Sagas; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.AddSaga(); + } + } + + public class FinderOnlySaga : Saga, + IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.ConfigureFinderMapping(); + } + + public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class FinderOnlySagaData : ContainSagaData + { + public string Property { get; set; } + } + + public class FinderOnlyFinder : ISagaFinder + { + public Task FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData)); + } + + public class StartSagaMessage : IMessage; + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Not.Contain("IL3050")); + } + [Test] public void DoesNotSuppressIL2026ForNonAddSagaCalls() { @@ -89,4 +246,37 @@ public void SomeOtherMethod() { } Assert.That(diagnostics, Does.Contain("IL2026")); } + + [Test] + public void DoesNotSuppressIL3050ForNonAddSagaCalls() + { + var source = """ + using System.Diagnostics.CodeAnalysis; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + // This call should still produce IL3050 since it's not intercepted + SomeOtherMethod(); + } + + [RequiresDynamicCode("Test method")] + public void SomeOtherMethod() { } + } + """; + + var result = SourceGeneratorTest.ForIncrementalGenerator() + .WithSource(source, "test.cs") + .WithAnalyzer() + .WithSuppressor() + .SuppressDiagnosticErrors() + .SuppressCompilationErrors() + .Run(); + + var diagnostics = result.GetCompilationOutput(); + + Assert.That(diagnostics, Does.Contain("IL3050")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs new file mode 100644 index 00000000000..17a05cb69fd --- /dev/null +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/Sagas/GeneratedCorrelationAccessorExecutionTests.cs @@ -0,0 +1,137 @@ +namespace NServiceBus.Core.Analyzer.Tests.Sagas; + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Analyzer; +using Analyzer.Sagas; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using NServiceBus.Sagas; +using NUnit.Framework; + +[TestFixture] +public class GeneratedCorrelationAccessorExecutionTests +{ + [Test] + public void Generated_correlation_accessors_round_trip_for_colliding_saga_data_properties() + { + var source = """ + using System.Threading.Tasks; + using NServiceBus; + + public class Test + { + public void Configure(EndpointConfiguration cfg) + { + cfg.Handlers.CollidingAccessorsAssembly.AddAll(); + } + } + + namespace First + { + [Saga] + public class SagaA : Saga, IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => + mapper.MapSaga(s => s.CorrelationId).ToMessage(m => m.CorrelationId); + + public Task Handle(StartMessageA message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SagaAData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class StartMessageA : ICommand + { + public string CorrelationId { get; set; } + } + } + + namespace Second + { + [Saga] + public class SagaB : Saga, IAmStartedByMessages + { + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) => + mapper.MapSaga(s => s.CorrelationId).ToMessage(m => m.CorrelationId); + + public Task Handle(StartMessageB message, IMessageHandlerContext context) => Task.CompletedTask; + } + + public class SagaBData : ContainSagaData + { + public string CorrelationId { get; set; } + } + + public class StartMessageB : ICommand + { + public string CorrelationId { get; set; } + } + } + """; + + var assembly = CompileAndLoad(source); + + // Two saga-data classes with a colliding property name/type must not share one generated accessor. + var accessorTypes = assembly.GetTypes() + .Where(t => typeof(CorrelationPropertyAccessor).IsAssignableFrom(t) && !t.IsAbstract) + .ToArray(); + + Assert.That(accessorTypes, Has.Length.EqualTo(2), "Each saga-data class must get its own generated correlation accessor."); + + foreach (var accessorType in accessorTypes) + { + var sagaDataType = accessorType + .GetMethod("AccessFrom_Property", BindingFlags.Static | BindingFlags.NonPublic)! + .GetParameters()[0] + .ParameterType; + var sagaData = (IContainSagaData)Activator.CreateInstance(sagaDataType); + var accessor = (CorrelationPropertyAccessor)accessorType.GetField("Instance")!.GetValue(null)!; + + accessor.WriteTo(sagaData, "correlation-value"); + var value = accessor.AccessFrom(sagaData); + + Assert.That(value, Is.EqualTo("correlation-value"), $"Accessor for {sagaDataType.Name} did not round-trip the correlation value."); + } + } + + static Assembly CompileAndLoad(string source) + { + var parseOptions = new CSharpParseOptions(LanguageVersion.Preview); + var sourceTree = CSharpSyntaxTree.ParseText(source, parseOptions); + + var compilation = CSharpCompilation.Create( + "CollidingAccessors", + [sourceTree], + ReferenceAssemblyPaths(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable)); + + var driver = CSharpGeneratorDriver.Create( + [ + new AddSagaGenerator().AsSourceGenerator(), + new AddHandlerAndSagasRegistrationGenerator().AsSourceGenerator() + ], + parseOptions: parseOptions); + + driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out _); + + using var peStream = new MemoryStream(); + var emitResult = outputCompilation.Emit(peStream); + + var errors = emitResult.Diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + Assert.That(errors, Is.Empty, string.Join(Environment.NewLine, errors.Select(e => e.ToString()))); + + return Assembly.Load(peStream.ToArray()); + } + + static MetadataReference[] ReferenceAssemblyPaths() => + [ + .. AppDomain.CurrentDomain.GetAssemblies() + .Where(a => !string.IsNullOrWhiteSpace(a.Location)) + .Select(MetadataReference (a) => MetadataReference.CreateFromFile(a.Location)) + ]; +} diff --git a/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs b/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs index d006581ff85..7c4550c2915 100644 --- a/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs +++ b/src/NServiceBus.Core.Analyzer/DiagnosticIds.cs @@ -51,4 +51,9 @@ public static class DiagnosticIds public const string ConventionBasedHandlerNoAccessibleConstructor = "NSB0036"; public const string ConventionBasedHandlerAmbiguousConstructor = "NSB0037"; public const string HandlerClassCannotBeStatic = "NSB0038"; + + // Messaging migration diagnostics + public const string UseGenericMessageType = "NSB0039"; + public const string RuntimeTypeMayDiffer = "NSB0040"; + public const string GenericMessageTypeIsObject = "NSB0041"; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs index e8cef4cccf6..645b4186b09 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/AddHandlerInterceptor.Suppressor.cs @@ -9,16 +9,23 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AddHandlerInterceptorSuppressor : DiagnosticSuppressor { + const string Justification = "The AddHandler method has been intercepted by a statically generated variant."; + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( SupressionIds.AddHandlerInterceptorSuppression, suppressedDiagnosticId: "IL2026", - justification: "The AddHandler method has been intercepted by a statically generated variant."); + justification: Justification); + + static readonly SuppressionDescriptor SuppressRDCDiagnostic = new( + SupressionIds.AddHandlerInterceptorAotSuppression, + suppressedDiagnosticId: "IL3050", + justification: Justification); public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diagnostic in context.ReportedDiagnostics) { - if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId) { continue; } @@ -47,10 +54,11 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var operation = semanticModel.GetOperation(node, context.CancellationToken); if (operation is IInvocationOperation { TargetMethod: { } methodSymbol } && AddHandlerInterceptor.Parser.IsAddHandlerMethod(methodSymbol)) { - context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic; + context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); } } } - public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic]; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs b/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs index fc3d10dc9fb..48185b1d13f 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/ConventionBasedHandlerHelper.cs @@ -4,17 +4,24 @@ namespace NServiceBus.Core.Analyzer.Handlers; using System.Collections.Generic; using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; static class ConventionBasedHandlerHelper { public const string HandleMethodName = "Handle"; - public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) + public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, Compilation? compilation = null, CancellationToken cancellationToken = default) { + var interfaceMessageTypes = CollectInterfaceMessageTypes(classType, knownTypes); + var interfaceHandleCallees = compilation is null + ? null + : CollectInterfaceHandleCallees(classType, knownTypes, compilation, cancellationToken); + for (var current = classType; current is not null; current = current.BaseType) { - if (HasValidConventionBasedHandleMethods(current, knownTypes, classType)) + if (HasValidConventionBasedHandleMethods(current, knownTypes, interfaceMessageTypes, interfaceHandleCallees)) { return true; } @@ -23,38 +30,39 @@ public static bool IsConventionBasedHandlerType(INamedTypeSymbol classType, Hand return false; } - public static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) => - HasValidConventionBasedHandleMethods(classType, knownTypes, classType); + public static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, Compilation? compilation = null, CancellationToken cancellationToken = default) => + HasValidConventionBasedHandleMethods(classType, knownTypes, + CollectInterfaceMessageTypes(classType, knownTypes), + compilation is null ? null : CollectInterfaceHandleCallees(classType, knownTypes, compilation, cancellationToken)); - static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, INamedTypeSymbol interfaceImplementationType) + static bool HasValidConventionBasedHandleMethods(INamedTypeSymbol classType, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, HashSet? interfaceHandleCallees) { - var interfaceMessageTypes = new HashSet(System.StringComparer.Ordinal); - foreach (var iface in interfaceImplementationType.AllInterfaces) + foreach (var member in classType.GetMembers()) { - if (iface.IsGenericType && HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes) && - iface.TypeArguments[0] is INamedTypeSymbol msgType) + if (member is not IMethodSymbol method) { - interfaceMessageTypes.Add(msgType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + continue; } - } - foreach (var member in classType.GetMembers()) - { - if (member is not IMethodSymbol method) + if (!IsValidConventionBasedHandleMethod(method, knownTypes, interfaceMessageTypes)) { continue; } - if (IsValidConventionBasedHandleMethod(method, knownTypes, interfaceMessageTypes, interfaceImplementationType)) + // Exclude methods that are helpers called from within an interface Handle implementation + if (interfaceHandleCallees is not null && + (interfaceHandleCallees.Contains(method) || interfaceHandleCallees.Contains(method.OriginalDefinition))) { - return true; + continue; } + + return true; } return false; } - public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, INamedTypeSymbol? interfaceImplementationType = null) + public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, HandlerKnownTypes knownTypes, HashSet interfaceMessageTypes, INamedTypeSymbol? interfaceImplementationType = null) { if (method.Name != HandleMethodName || method.DeclaredAccessibility != Accessibility.Public || @@ -88,13 +96,13 @@ public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, Hand } // If the class implements IHandleMessages for this exact message type with exactly 2 params, - // this Handle method is the interface implementation — not a convention-based method. - var messageTypeFqn = messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - if (method.Parameters.Length == 2 && interfaceMessageTypes.Contains(messageTypeFqn)) + // this Handle method is the interface implementation, not a convention-based method. + if (method.Parameters.Length == 2 && interfaceMessageTypes.Contains(messageType)) { return false; } + // If this Handle method is the implementation of an interface member that belongs to a handler // interface hierarchy (IHandleMessages, IHandleTimeouts, IAmStartedByMessages, or an // interface deriving from them), it is interface-based, not convention-based. @@ -107,6 +115,96 @@ public static bool IsValidConventionBasedHandleMethod(IMethodSymbol method, Hand return true; } + static HashSet CollectInterfaceMessageTypes(INamedTypeSymbol classType, HandlerKnownTypes knownTypes) + { + var messageTypes = new HashSet(SymbolEqualityComparer.Default); + foreach (var iface in classType.AllInterfaces) + { + if (iface.IsGenericType && HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes) && + iface.TypeArguments[0] is INamedTypeSymbol msgType) + { + messageTypes.Add(msgType); + } + } + + return messageTypes; + } + + // Collects all methods that are called from within interface Handle method implementations. + // These are helper methods, not convention-based handlers. + static HashSet CollectInterfaceHandleCallees( + INamedTypeSymbol classType, + HandlerKnownTypes knownTypes, + Compilation compilation, + CancellationToken cancellationToken) + { + var visitedImplementations = new HashSet(SymbolEqualityComparer.Default); + var callees = new HashSet(SymbolEqualityComparer.Default); + SyntaxTree? cachedTree = null; + SemanticModel? cachedModel = null; + + foreach (var iface in classType.AllInterfaces) + { + if (!iface.IsGenericType) + { + continue; + } + + if (!HandlerConventions.IsHandlerInterface(iface.OriginalDefinition, knownTypes)) + { + continue; + } + + foreach (var member in iface.GetMembers(HandleMethodName)) + { + if (member is not IMethodSymbol ifaceMethod) + { + continue; + } + + if (classType.FindImplementationForInterfaceMember(ifaceMethod) is not IMethodSymbol impl) + { + continue; + } + + if (!visitedImplementations.Add(impl)) + { + continue; + } + + foreach (var syntaxRef in impl.DeclaringSyntaxReferences) + { + var syntax = syntaxRef.GetSyntax(cancellationToken); + + if (!ReferenceEquals(syntax.SyntaxTree, cachedTree)) + { + cachedTree = syntax.SyntaxTree; + cachedModel = compilation.GetSemanticModel(cachedTree); + } + + foreach (var invocation in syntax.DescendantNodes().OfType()) + { + if (cachedModel!.GetSymbolInfo(invocation, cancellationToken).Symbol is not IMethodSymbol invokedMethod) + { + continue; + } + + callees.Add(invokedMethod); + callees.Add(invokedMethod.OriginalDefinition); + + if (invokedMethod.ReducedFrom is { } reduced) + { + callees.Add(reduced); + callees.Add(reduced.OriginalDefinition); + } + } + } + } + } + + return callees; + } + static bool ImplementsHandlerInterfaceMember(IMethodSymbol method, HandlerKnownTypes knownTypes, INamedTypeSymbol? interfaceImplementationType) { // Fast path: check explicit interface implementations first (O(1)) @@ -138,8 +236,13 @@ static bool ImplementsHandlerInterfaceMember(IMethodSymbol method, HandlerKnownT continue; } - foreach (var interfaceMethod in iface.GetMembers(method.Name).OfType()) + foreach (var member in iface.GetMembers(method.Name)) { + if (member is not IMethodSymbol interfaceMethod) + { + continue; + } + if (interfaceMethod.Parameters.Length != method.Parameters.Length) { continue; diff --git a/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs b/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs index 6b591b57926..cb0e8ac8b15 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/HandlerAttributeAnalyzer.cs @@ -37,7 +37,7 @@ public override void Initialize(AnalysisContext context) var isInterfaceBasedHandler = classType.ImplementsGenericInterface(knownTypes.IHandleMessages); var isSaga = classType.ImplementsGenericType(knownTypes.SagaBase); - var isConventionBasedHandler = !isSaga && ConventionBasedHandlerHelper.IsConventionBasedHandlerType(classType, knownTypes); + var isConventionBasedHandler = !isSaga && ConventionBasedHandlerHelper.IsConventionBasedHandlerType(classType, knownTypes, context.Compilation, context.CancellationToken); if (!isInterfaceBasedHandler || isSaga) { @@ -150,7 +150,7 @@ public override void Initialize(AnalysisContext context) } // Interface-based handler: check for mixed-style (also has convention-based Handle methods) - if (ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(classType, knownTypes)) + if (ConventionBasedHandlerHelper.HasValidConventionBasedHandleMethods(classType, knownTypes, context.Compilation, context.CancellationToken)) { var classLocation = classType.GetClassIdentifierLocation(context.CancellationToken); if (classLocation is not null) diff --git a/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs b/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs index bf81129583d..f78843af37d 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/HandlerKnownTypes.cs @@ -14,7 +14,8 @@ public sealed record HandlerKnownTypes( INamedTypeSymbol SagaBase, INamedTypeSymbol IMessageHandlerContext, INamedTypeSymbol? CancellationTokenType, - INamedTypeSymbol? ActivatorUtilitiesConstructorAttributeType) + INamedTypeSymbol? ActivatorUtilitiesConstructorAttributeType, + INamedTypeSymbol? FromKeyedServicesAttributeType) { public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out HandlerKnownTypes? knownTypes) { @@ -27,6 +28,7 @@ public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out Handl var iMessageHandlerContext = compilation.GetTypeByMetadataName("NServiceBus.IMessageHandlerContext"); var cancellationTokenType = compilation.GetTypeByMetadataName("System.Threading.CancellationToken"); var activatorUtilitiesConstructorAttributeType = compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.ActivatorUtilitiesConstructorAttribute"); + var fromKeyedServicesAttributeType = compilation.GetTypeByMetadataName("Microsoft.Extensions.DependencyInjection.FromKeyedServicesAttribute"); if (iHandleMessages is null || iHandleTimeouts is null || iAmStartedByMessages is null || handlerAttribute is null || sagaBase is null || iMessageHandlerContext is null) @@ -44,7 +46,8 @@ public static bool TryGet(Compilation compilation, [NotNullWhen(true)] out Handl sagaBase, iMessageHandlerContext, cancellationTokenType, - activatorUtilitiesConstructorAttributeType); + activatorUtilitiesConstructorAttributeType, + fromKeyedServicesAttributeType); return true; } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs index be487e3cc69..952165429b9 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Emitter.cs @@ -1,4 +1,5 @@ -namespace NServiceBus.Core.Analyzer.Handlers; +#nullable enable +namespace NServiceBus.Core.Analyzer.Handlers; using System.Collections.Generic; using System.Linq; @@ -76,7 +77,7 @@ static void EmitAdapterType(SourceWriter sourceWriter, ConventionBasedMethodSpec if (adapterParams.AllParams.Count > 0) { sourceWriter.WriteLine(); - var ctorArgs = string.Join(", ", adapterParams.AllParams.Select(p => $"{p.FullyQualifiedType} {p.ConstructorParameterName}")); + var ctorArgs = string.Join(", ", adapterParams.AllParams.Select(p => $"{FormatKeyedServiceAttribute(p.KeyedServiceKey)}{p.FullyQualifiedType} {p.ConstructorParameterName}")); sourceWriter.WriteLine($"public {method.AdapterName}({ctorArgs})"); sourceWriter.WriteLine("{"); sourceWriter.Indentation++; @@ -137,7 +138,7 @@ static AdapterParamSpecs BuildAdapterParams(ConventionBasedMethodSpec method) { var memberName = CreateUniqueName(p.ParameterName, "FromCtor", usedMemberNames); var constructorParameterName = CreateUniqueName(memberName, "FromCtor", usedCtorParameterNames); - all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType)); + all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType, p.KeyedServiceKey)); ctorFieldReferences.Add($"_{memberName}"); } // Method params (excluding CancellationToken — those come from context) @@ -150,7 +151,7 @@ static AdapterParamSpecs BuildAdapterParams(ConventionBasedMethodSpec method) var memberName = CreateUniqueName(p.ParameterName, "FromMethod", usedMemberNames); var constructorParameterName = CreateUniqueName(memberName, "FromMethod", usedCtorParameterNames); - all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType)); + all.Add(new AdapterParamSpec(memberName, constructorParameterName, p.FullyQualifiedType, p.KeyedServiceKey)); methodFieldReferences.Add($"_{memberName}"); } @@ -174,7 +175,18 @@ static string CreateUniqueName(string baseName, string suffix, HashSet u return candidate; } - readonly record struct AdapterParamSpec(string MemberName, string ConstructorParameterName, string FullyQualifiedType); + readonly record struct AdapterParamSpec(string MemberName, string ConstructorParameterName, string FullyQualifiedType, string? KeyedServiceKey); + + static string FormatKeyedServiceAttribute(string? keyedServiceKey) => + keyedServiceKey switch + { + // null => no [FromKeyedServices] attribute on the original parameter + null => string.Empty, + // "" => bare [FromKeyedServices] with no argument + "" => "[global::Microsoft.Extensions.DependencyInjection.FromKeyedServices] ", + // verbatim expression text from the original source + _ => $"[global::Microsoft.Extensions.DependencyInjection.FromKeyedServices({keyedServiceKey})] " + }; readonly record struct AdapterParamSpecs(List AllParams, List CtorFieldReferences, List MethodFieldReferences); } diff --git a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs index ecde2e0cd10..1bb644d3303 100644 --- a/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Handlers/Handlers.Parser.cs @@ -8,6 +8,7 @@ namespace NServiceBus.Core.Analyzer.Handlers; using System.Text; using System.Threading; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Utility; using BaseParser = AddHandlerAndSagasRegistrationGenerator.Parser; @@ -41,7 +42,13 @@ public enum RegistrationType public readonly record struct RegistrationSpec(RegistrationType RegistrationType, string MessageType, ImmutableEquatableArray MessageHierarchy, string HandlerType); - public readonly record struct InjectedParamSpec(string ParameterName, string FullyQualifiedType, bool IsCancellationToken); + // KeyedServiceKey carries the [FromKeyedServices(...)] key folded to its compile-time constant + // value, rendered as a valid C# expression for re-application in the generated adapter ctor. + // Sentinel semantics: + // - null : no [FromKeyedServices] attribute present + // - "" (empty) : attribute present with no argument (bare [FromKeyedServices]) + // - any other value: the folded key (e.g. "\"MyKey\"", "null", "typeof(global::T)", "(global::E)1") + public readonly record struct InjectedParamSpec(string ParameterName, string FullyQualifiedType, bool IsCancellationToken, string? KeyedServiceKey); public readonly record struct ConventionBasedMethodSpec( string MessageType, @@ -109,12 +116,12 @@ public static HandlerSpec Parse(INamedTypeSymbol handlerType, .ToList(); // Collect message types already handled by IHandleMessages interface implementations - var interfaceMessageTypes = new HashSet(StringComparer.Ordinal); + var interfaceMessageTypes = new HashSet(SymbolEqualityComparer.Default); foreach (var @interface in handlerType.AllInterfaces.Where(IsHandlerInterface)) { if (@interface.TypeArguments[0] is INamedTypeSymbol msgType) { - interfaceMessageTypes.Add(msgType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + interfaceMessageTypes.Add(msgType); } } @@ -137,7 +144,7 @@ public static HandlerSpec Parse(INamedTypeSymbol handlerType, static List ParseConventionBasedMethods( INamedTypeSymbol handlerType, HandlerKnownTypes knownTypes, - HashSet interfaceMessageTypes, + HashSet interfaceMessageTypes, bool includeInheritedMethods, CancellationToken cancellationToken) { @@ -146,7 +153,7 @@ static List ParseConventionBasedMethods( // Ctor params of the handler type (for instance methods) var selectedConstructor = SelectConstructor(handlerType, knownTypes.ActivatorUtilitiesConstructorAttributeType); - var ctorParams = GetCtorParams(selectedConstructor, knownTypes.CancellationTokenType); + var ctorParams = GetCtorParams(selectedConstructor, knownTypes.CancellationTokenType, knownTypes.FromKeyedServicesAttributeType); foreach (var method in GetHandleMethods(handlerType, includeInheritedMethods)) { @@ -174,10 +181,12 @@ static List ParseConventionBasedMethods( var param = method.Parameters[i]; bool isCt = knownTypes.CancellationTokenType is not null && SymbolEqualityComparer.Default.Equals(param.Type, knownTypes.CancellationTokenType); + var keyedServiceKey = TryGetKeyedServiceKey(param, knownTypes.FromKeyedServicesAttributeType); methodParams.Add(new InjectedParamSpec( param.Name, param.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), - isCt)); + isCt, + keyedServiceKey)); } var hierarchy = new ImmutableEquatableArray( @@ -234,7 +243,8 @@ readonly record struct ParameterSignatureKey( static ImmutableEquatableArray GetCtorParams( IMethodSymbol? constructor, - INamedTypeSymbol? cancellationTokenType) + INamedTypeSymbol? cancellationTokenType, + INamedTypeSymbol? fromKeyedServicesAttributeType) { if (constructor is null || constructor.Parameters.Length == 0) { @@ -247,12 +257,71 @@ static ImmutableEquatableArray GetCtorParams( var p = constructor.Parameters[i]; bool isCt = cancellationTokenType is not null && SymbolEqualityComparer.Default.Equals(p.Type, cancellationTokenType); - specs[i] = new InjectedParamSpec(p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), isCt); + var keyedServiceKey = TryGetKeyedServiceKey(p, fromKeyedServicesAttributeType); + specs[i] = new InjectedParamSpec(p.Name, p.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), isCt, keyedServiceKey); } return specs.ToImmutableEquatableArray(); } + // Extracts the [FromKeyedServices(...)] key for a parameter, folded to its compile-time + // constant value. Folding guarantees the value resolves in the generated adapter, + // which lives at global scope. Covers every valid attribute-argument + // form uniformly: string literals, const fields, nameof, typeof, named arguments, and null. + // Sentinel: null = no FromKeyedServices attribute; "" (empty) = bare attribute with no key; + // otherwise the folded key rendered as a valid C# expression. + static string? TryGetKeyedServiceKey(IParameterSymbol param, INamedTypeSymbol? fromKeyedServicesAttributeType) + { + if (fromKeyedServicesAttributeType is null) + { + return null; + } + + foreach (var attribute in param.GetAttributes()) + { + if (attribute.AttributeClass is null) + { + continue; + } + + if (!SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, fromKeyedServicesAttributeType) && + !SymbolEqualityComparer.Default.Equals(attribute.AttributeClass.OriginalDefinition, fromKeyedServicesAttributeType)) + { + continue; + } + + return FormatKeyedServiceKey(attribute); + } + + return null; + } + + static string FormatKeyedServiceKey(AttributeData attribute) + { + if (attribute.ConstructorArguments.Length == 0) + { + // Bare attribute with no key argument. + return string.Empty; + } + + var arg = attribute.ConstructorArguments[0]; + + // typeof(T) -> typeof(global::T) so it resolves at the adapter's global scope. + if (arg is { Kind: TypedConstantKind.Type, Value: ITypeSymbol typeSymbol }) + { + return $"typeof({typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + } + + // Enum constant -> cast the underlying integral value to the fully-qualified enum type. + if (arg is { Kind: TypedConstantKind.Enum, Type: INamedTypeSymbol enumType }) + { + return $"({enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}){arg.Value}"; + } + + // Primitives (string/int/bool/...) and null -> C# literal form via the shared formatter. + return SymbolDisplay.FormatPrimitive(arg.Value, quoteStrings: true, useHexadecimalNumbers: false) ?? "null"; + } + static IMethodSymbol? SelectConstructor(INamedTypeSymbol handlerType, INamedTypeSymbol? activatorUtilitiesConstructorAttributeType) { var candidates = handlerType.Constructors @@ -375,55 +444,6 @@ static string BuildAdapterName(INamedTypeSymbol handlerType, IMethodSymbol metho static bool IsHandlerInterface(INamedTypeSymbol type) => HandlerConventions.IsHandlerInterface(type); static IEnumerable GetTypeHierarchy(INamedTypeSymbol type, MarkerTypes markers) => - // This matches the behavior of the reflection-based code, but it's unclear why this ordering is needed. - // It would be more efficient to yield the base types (except where type.SpecialType is not SpecialType.System_Object) - // and then to yield the interfaces from type.AllInterfaces except those in the MarkerTypes. - // We're hesitant to change the implementation, however, due to wire compatibility concerns of outputting - // an EnclosedMessageTypes header with a different ordering. - GetParentTypes(type) - .Where(t => !markers.IsMarkerInterface(t)) - .Select(t => new { Type = t, Rank = PlaceInMessageHierarchy(t) }) - .OrderByDescending(item => item.Rank) - .Select(item => item.Type); - - static IEnumerable GetParentTypes(INamedTypeSymbol type) - { - // All interfaces implemented by the type (includes inherited interfaces) - foreach (var iface in type.AllInterfaces) - { - yield return iface; - } - - // All base types up to but excluding System.Object - var currentBase = type.BaseType; - while (currentBase is { SpecialType: not SpecialType.System_Object }) - { - if (currentBase is { } named) - { - yield return named; - } - - currentBase = currentBase.BaseType; - } - } - - static int PlaceInMessageHierarchy(INamedTypeSymbol type) - { - if (type.TypeKind == TypeKind.Interface) - { - // Approximate: number of interfaces implemented by this interface - return type.AllInterfaces.Length; - } - - var result = 0; - var current = type.BaseType; - while (current is not null) - { - result++; - current = current.BaseType; - } - - return result; - } + MessageHierarchyBuilder.GetTypeHierarchy(type, markers); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs new file mode 100644 index 00000000000..4e2e3b7125b --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Emitter.cs @@ -0,0 +1,124 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using System; +using System.Linq; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer; +using Utility; + +public sealed partial class AddMessageTypeInterceptor +{ + internal class Emitter(SourceProductionContext sourceProductionContext) + { + public void Emit(InterceptableMessageTypeSpecs messageTypeSpecs) => Emit(sourceProductionContext, messageTypeSpecs); + + static void Emit(SourceProductionContext context, InterceptableMessageTypeSpecs messageTypeSpecs) + { + var interceptableMessageTypeSpecs = messageTypeSpecs.MessageTypes; + if (interceptableMessageTypeSpecs.Count == 0) + { + return; + } + + var sourceWriter = new SourceWriter() + .ForInterceptor() + .WithGeneratedCodeAttribute(); + + sourceWriter.WriteLine(""" + static file class InterceptionsOfAddMessageTypeMethod + { + """); + + sourceWriter.Indentation++; + + sourceWriter.WriteLine(""" + extension (NServiceBus.EndpointConfiguration endpointConfiguration) + { + """); + sourceWriter.Indentation++; + + var groups = interceptableMessageTypeSpecs.Select(m => (MethodName: AddMethodName(m.MessageTypeSpec.Name, m.MessageTypeSpec.FullyQualifiedName), InterceptableMessageType: m)) + .GroupBy(i => i.MethodName) + .OrderBy(g => g.Key, StringComparer.Ordinal) + .ToArray(); + for (int index = 0; index < groups.Length; index++) + { + IGrouping group = groups[index]; + (string MethodName, InterceptableMessageTypeSpec InterceptableMessageType)? first = null; + foreach (var location in group) + { + first ??= location; + + var (_, messageType) = location; + sourceWriter.WriteLine($"{messageType.LocationSpec.Attribute} // {messageType.LocationSpec.DisplayLocation}"); + } + + if (!first.HasValue) + { + continue; + } + + (string methodName, InterceptableMessageTypeSpec interceptableMessageType) = first.Value; + sourceWriter.WriteLine($$""" + public void {{methodName}}() + { + """); + sourceWriter.Indentation++; + + sourceWriter.WriteLine("System.ArgumentNullException.ThrowIfNull(endpointConfiguration);"); + + EmitMessageTypeRegistration(sourceWriter, interceptableMessageType.MessageTypeSpec); + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + if (index < groups.Length - 1) + { + sourceWriter.WriteLine(); + } + } + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + sourceWriter.Indentation--; + sourceWriter.WriteLine("}"); + + sourceWriter.CloseCurlies(); + + context.AddSource("InterceptionsOfAddMessageTypeMethod.g.cs", sourceWriter.ToSourceText()); + } + + static void EmitMessageTypeRegistration(SourceWriter sourceWriter, MessageTypeSpec messageTypeSpec) + { + sourceWriter.WriteLine(""" + var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(endpointConfiguration); + var messageMetadataRegistry = settings.GetOrCreate(); + """); + + if (messageTypeSpec.HierarchyTypeNames.Count == 0) + { + sourceWriter.WriteLine($"messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof({messageTypeSpec.FullyQualifiedName}), []);"); + return; + } + + sourceWriter.WriteLine($"messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof({messageTypeSpec.FullyQualifiedName}),"); + sourceWriter.WriteLine("["); + sourceWriter.Indentation++; + foreach (var hierarchyTypeName in messageTypeSpec.HierarchyTypeNames) + { + sourceWriter.WriteLine($"typeof({hierarchyTypeName}),"); + } + sourceWriter.Indentation--; + sourceWriter.WriteLine("]);"); + } + + static string AddMethodName(string name, string messageType) + { + const string NamePrefix = "AddMessageType_"; + return InterceptorMethodNameBuilder.Build(NamePrefix, name, messageType); + } + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs new file mode 100644 index 00000000000..36d645d9670 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Parser.cs @@ -0,0 +1,101 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Handlers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Operations; +using NServiceBus.Core.Analyzer; +using Utility; + +public sealed partial class AddMessageTypeInterceptor +{ + internal readonly record struct MessageTypeSpec(string Name, string FullyQualifiedName, ImmutableEquatableArray HierarchyTypeNames) + { + public static MessageTypeSpec From(INamedTypeSymbol messageType, Compilation compilation) => + new( + string.Join("__", messageType.ToDisplayParts(SymbolDisplayFormat.FullyQualifiedFormat).Where(x => x.Kind == SymbolDisplayPartKind.ClassName)), + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat), + GetHierarchyTypeNames(messageType, compilation)); + + static ImmutableEquatableArray GetHierarchyTypeNames(INamedTypeSymbol messageType, Compilation compilation) + { + // Shared with handler/saga generation so the emitted hierarchy has the same ordering regardless of which + // registration path wins (duplicate registration is first-wins in the message metadata registry). + return MessageHierarchyBuilder.GetTypeHierarchy(messageType, new MarkerTypes(compilation)) + .Select(t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)) + .ToImmutableEquatableArray(); + } + } + + internal readonly record struct InterceptableMessageTypeSpec(InterceptLocationSpec LocationSpec, MessageTypeSpec MessageTypeSpec); + + internal readonly record struct InterceptableMessageTypeSpecs(ImmutableEquatableArray MessageTypes); + + internal static class Parser + { + public static bool SyntaxLooksLikeAddMessageTypeMethod(SyntaxNode node) => node is InvocationExpressionSyntax + { + Expression: MemberAccessExpressionSyntax + { + Name: GenericNameSyntax + { + Identifier.ValueText: AddMessageTypeMethodName, + TypeArgumentList.Arguments.Count: 1 + } + }, + ArgumentList.Arguments.Count: 0 + }; + + internal static bool IsAddMessageTypeMethod(IMethodSymbol method) => method is + { + Name: AddMessageTypeMethodName, + IsGenericMethod: true, + TypeArguments.Length: 1, + ContainingType: + { + Name: AddMessageTypeClassName, + ContainingNamespace: + { + Name: "NServiceBus", + ContainingNamespace.IsGlobalNamespace: true + } + } + }; + + public static InterceptableMessageTypeSpec? Parse(InvocationExpressionSyntax invocation, SemanticModel semanticModel, CancellationToken cancellationToken = default) + { + if (semanticModel.GetOperation(invocation, cancellationToken) is not IInvocationOperation operation) + { + return null; + } + + // Make sure the method we're looking at is ours and not some (extremely unlikely) copycat + if (!IsAddMessageTypeMethod(operation.TargetMethod)) + { + return null; + } + + if (operation.TargetMethod.TypeArguments[0] is not INamedTypeSymbol messageType) + { + return null; + } + + if (semanticModel.GetInterceptableLocation(invocation, cancellationToken) is not { } location) + { + return null; + } + + return new InterceptableMessageTypeSpec(InterceptLocationSpec.From(location), MessageTypeSpec.From(messageType, semanticModel.Compilation)); + } + + const string AddMessageTypeMethodName = "AddMessageType"; + const string AddMessageTypeClassName = "MessageTypeRegistrationExtensions"; + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs new file mode 100644 index 00000000000..1b32ad84fad --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.Suppressor.cs @@ -0,0 +1,61 @@ +namespace NServiceBus.Core.Analyzer.Messages; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class AddMessageTypeInterceptorSuppressor : DiagnosticSuppressor +{ + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( + SupressionIds.AddMessageTypeInterceptorSuppression, + suppressedDiagnosticId: "IL2026", + justification: "The AddMessageType method has been intercepted by a statically generated variant."); + + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (var diagnostic in context.ReportedDiagnostics) + { + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + { + continue; + } + + var location = diagnostic.AdditionalLocations.Count > 0 ? diagnostic.AdditionalLocations[0] : diagnostic.Location; + if (location.SourceTree is not { } sourceTree) + { + continue; + } + + // The trim analyzer warns on the MemberAccessExpression since https://github.com/dotnet/runtime/pull/110086, + // so the location can be the invocation or its child. + var node = sourceTree.GetRoot().FindNode(location.SourceSpan) switch + { + InvocationExpressionSyntax s => s, + { Parent: InvocationExpressionSyntax s } => s, + _ => null, + }; + + if (node is null || !AddMessageTypeInterceptor.Parser.SyntaxLooksLikeAddMessageTypeMethod(node)) + { + continue; + } + + var semanticModel = context.GetSemanticModel(sourceTree); + + // Only suppress when an interceptor can actually be emitted for this call site. Calls with a generic + // type parameter cannot be intercepted because the hierarchy cannot be computed statically, so they keep + // the RequiresUnreferencedCode fallback warning. + if (AddMessageTypeInterceptor.Parser.Parse(node, semanticModel, context.CancellationToken) is null) + { + continue; + } + + context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + } + } + + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs new file mode 100644 index 00000000000..35d86f13abb --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.TrackingNames.cs @@ -0,0 +1,16 @@ +namespace NServiceBus.Core.Analyzer.Messages; + +public sealed partial class AddMessageTypeInterceptor +{ + internal static class TrackingNames + { + public const string MessageTypeSpec = nameof(MessageTypeSpec); + public const string MessageTypeSpecs = nameof(MessageTypeSpecs); + + public static readonly string[] All = + [ + MessageTypeSpec, + MessageTypeSpecs, + ]; + } +} diff --git a/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs new file mode 100644 index 00000000000..4b594e2493a --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Messages/AddMessageTypeInterceptor.cs @@ -0,0 +1,39 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Messages; + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using NServiceBus.Core.Analyzer; + +[Generator(LanguageNames.CSharp)] +public sealed partial class AddMessageTypeInterceptor : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var addMessageTypes = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (node, _) => Parser.SyntaxLooksLikeAddMessageTypeMethod(node), + transform: static (ctx, _) => (invocation: (InvocationExpressionSyntax)ctx.Node, semanticModel: ctx.SemanticModel)) + .Select(static (pair, cancellationToken) => + { + var (invocation, semanticModel) = pair; + return Parser.Parse(invocation!, semanticModel!, cancellationToken); + }) + .Where(static spec => spec.HasValue) + .Select(static (spec, _) => spec!.Value) + .WithTrackingName(TrackingNames.MessageTypeSpec); + + var collected = addMessageTypes.Collect() + .Select((specs, _) => new InterceptableMessageTypeSpecs(specs.ToImmutableEquatableArray())) + .WithTrackingName(TrackingNames.MessageTypeSpecs); + + context.RegisterSourceOutput(collected, + static (productionContext, spec) => + { + var emitter = new Emitter(productionContext); + emitter.Emit(spec); + }); + } +} diff --git a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs new file mode 100644 index 00000000000..e58f09dcfb3 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs @@ -0,0 +1,899 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer; + +using System; +using System.Collections.Concurrent; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class MessagingMigrationAnalyzer : DiagnosticAnalyzer +{ + const string HelpLink = "https://docs.particular.net/nservicebus/messaging/messages-events-commands"; + const string MessageTypeProperty = "MessageType"; + const string PublishTrimmedProperty = "build_property.PublishTrimmed"; + const string PublishAotProperty = "build_property.PublishAot"; + const string IsAotCompatibleProperty = "build_property.IsAotCompatible"; + const string IsTrimmableProperty = "build_property.IsTrimmable"; + const string EnableTrimAnalyzerProperty = "build_property.EnableTrimAnalyzer"; + + static readonly DiagnosticDescriptor UseGenericTypeRule = new( + DiagnosticIds.UseGenericMessageType, + "Use the strongly typed message overload", + "Use the strongly typed overload with message type '{0}' to make this operation trimming-safe", + "NServiceBus.Code", + DiagnosticSeverity.Info, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + static readonly DiagnosticDescriptor RuntimeTypeMayDifferRule = new( + DiagnosticIds.RuntimeTypeMayDiffer, + "Message routing uses the runtime type", + "This operation routes using the runtime message type; the strongly typed overload would route using '{0}'", + "NServiceBus.Code", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + static readonly DiagnosticDescriptor GenericTypeIsObjectRule = new( + DiagnosticIds.GenericMessageTypeIsObject, + "The message type must not be System.Object", + "The strongly typed overload would route this message as System.Object; specify the actual message type", + "NServiceBus.Code", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + helpLinkUri: HelpLink); + + public override ImmutableArray SupportedDiagnostics { get; } = + [UseGenericTypeRule, RuntimeTypeMayDifferRule, GenericTypeIsObjectRule]; + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(static startContext => + { + if (!KnownTypes.TryCreate(startContext.Compilation, out var knownTypes)) + { + return; + } + + var severityConfiguration = new MigrationDiagnosticConfiguration( + AreMigrationDiagnosticsAutomaticallyEnabled( + startContext.Options.AnalyzerConfigOptionsProvider.GlobalOptions), + startContext.Compilation); + startContext.RegisterOperationAction( + operationContext => AnalyzeInvocation(operationContext, knownTypes, severityConfiguration), + OperationKind.Invocation); + startContext.RegisterOperationAction( + operationContext => AnalyzeDelegateCreation(operationContext, knownTypes, severityConfiguration), + OperationKind.DelegateCreation); + startContext.RegisterOperationAction( + operationContext => AnalyzeSimpleAssignment(operationContext, knownTypes, severityConfiguration), + OperationKind.SimpleAssignment); + }); + } + + static void AnalyzeSimpleAssignment( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var assignment = (ISimpleAssignmentOperation)context.Operation; + if (assignment.Target is not IPropertyReferenceOperation + { + Instance: not null, + Property: { } property + }) + { + return; + } + + var replacementMethodName = ResolveMutatorReplacementMethod(property, knownTypes); + if (replacementMethodName is null) + { + return; + } + + var messageValue = UnwrapImplicitConversions(assignment.Value); + var messageType = messageValue.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic || + messageValue.ConstantValue is { HasValue: true, Value: null }) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // An object-typed assignment would be fixed to the generic overload with T = System.Object, + // which immediately violates NSB0041. Never offer a fixable NSB0039 for the object type. + if (messageType.SpecialType == SpecialType.System_Object) + { + return; + } + + // Mutator contexts preserve the previous logical type when the same instance is assigned + // again, mirroring UpdateMessage. Only direct creation and value types are provably safe. + if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage: true)) + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + assignment.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + assignment.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static string? ResolveMutatorReplacementMethod(IPropertySymbol property, KnownTypes knownTypes) + { + if (property.Name == "Message" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateIncomingMessageContext)) + { + return "UpdateMessageInstance"; + } + + if (property.Name == "OutgoingMessage" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateOutgoingMessageContext)) + { + return "UpdateMessage"; + } + + return null; + } + + static void AnalyzeDelegateCreation( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var delegateCreation = (IDelegateCreationOperation)context.Operation; + if (delegateCreation.Target is not IMethodReferenceOperation methodReference) + { + return; + } + + var invokedMethod = methodReference.Method; + var declaration = (invokedMethod.ReducedFrom ?? invokedMethod).OriginalDefinition; + + if (!IsTargetMethod(declaration, knownTypes, out var contractMember) || + !TryGetMessageParameter(declaration, contractMember, out var messageParameter)) + { + return; + } + + // Method groups bind to object-only overloads unless explicitly generic. + if (invokedMethod.IsGenericMethod) + { + if (invokedMethod.TypeArguments.Length > 0 && + invokedMethod.TypeArguments[0].SpecialType == SpecialType.System_Object) + { + context.ReportDiagnostic(Diagnostic.Create( + GenericTypeIsObjectRule, + methodReference.Syntax.GetLocation())); + } + + return; + } + + // Classify by the delegate parameter type; bound extension method references are unreduced. + if (delegateCreation.Type is not INamedTypeSymbol { DelegateInvokeMethod: { } invokeMethod } || + !TryMapMessageParameter(invokeMethod, declaration, messageParameter, methodReference, out var delegateMessageParameter)) + { + return; + } + + var messageType = delegateMessageParameter.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // UpdateMessage reference types remain ambiguous because same-instance replacement can + // preserve the previous logical type. Value-type method groups cannot bind here (CS0123). + var isRoutingEquivalent = declaration.Name is "UpdateMessage" or "UpdateMessageInstance" + ? messageType.IsValueType + : IsRoutingEquivalentMessageType(messageType); + if (isRoutingEquivalent) + { + if (!severityConfiguration.IsEnabled(context, methodReference.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + methodReference.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, methodReference.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + methodReference.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static void AnalyzeInvocation( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var invocation = (IInvocationOperation)context.Operation; + var invokedMethod = invocation.TargetMethod; + var declaration = (invokedMethod.ReducedFrom ?? invokedMethod).OriginalDefinition; + + if (!IsTargetMethod(declaration, knownTypes, out var contractMember) || + !TryGetMessageParameter(declaration, contractMember, out var messageParameter)) + { + return; + } + + if (IsGenericMessageInstanceOverload(declaration, messageParameter)) + { + // In 10.x, T=object is only reachable through an explicit generic call. + if (invokedMethod.TypeArguments.Length > 0 && + invokedMethod.TypeArguments[0].SpecialType == SpecialType.System_Object) + { + context.ReportDiagnostic(Diagnostic.Create(GenericTypeIsObjectRule, invocation.Syntax.GetLocation())); + } + + return; + } + + if (!IsObjectOverload(declaration, messageParameter)) + { + return; + } + + // Interface implementations may rename parameters. Reduced extensions require the name fallback. + var messageArgument = invocation.Arguments.FirstOrDefault(argument => + argument.Parameter is not null && + (SymbolEqualityComparer.Default.Equals(argument.Parameter.OriginalDefinition, messageParameter) || + argument.Parameter.Name == messageParameter.Name)); + if (messageArgument is null) + { + return; + } + + var messageValue = UnwrapImplicitConversions(messageArgument.Value); + var messageType = messageValue.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic || + messageValue.ConstantValue is { HasValue: true, Value: null }) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // An object-typed creation would be fixed to the generic overload with T = System.Object, which + // immediately violates NSB0041. Never offer a fixable NSB0039 for the object type. + if (messageType.SpecialType == SpecialType.System_Object) + { + return; + } + + var isUpdateMessage = declaration.Name is "UpdateMessage" or "UpdateMessageInstance"; + var isStableVarObjectCreation = !isUpdateMessage && + IsStableVarObjectCreation(messageValue, messageArgument, invocation, invocation.SemanticModel!); + if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage) || + isStableVarObjectCreation) + { + if (!severityConfiguration.IsEnabled(context, invocation.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + invocation.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, invocation.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + invocation.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static bool AreMigrationDiagnosticsAutomaticallyEnabled(AnalyzerConfigOptions globalOptions) => + IsTrue(globalOptions, PublishTrimmedProperty) || + IsTrue(globalOptions, PublishAotProperty) || + IsTrue(globalOptions, IsAotCompatibleProperty) || + IsTrue(globalOptions, IsTrimmableProperty) || + IsTrue(globalOptions, EnableTrimAnalyzerProperty); + + // Mirrors Roslyn's effective severity resolution (CSharpDiagnosticFilter.GetDiagnosticReport plus + // bulk configuration): command line, tree-level editorconfig, global, bulk, then the descriptor + // default. The migration diagnostics are opt-in, so the descriptor default only applies through + // automatic activation (trimming/AOT build properties). Command line, tree-level and global + // configuration block the bulk fallback even when their value is Default, mirroring Roslyn + // (AnalyzerDriver.GetEffectiveSeverities and + // AnalyzerOptionsExtensions.TryGetSeverityFromBulkConfiguration). + sealed class MigrationDiagnosticConfiguration(bool automaticallyEnabled, Compilation compilation) + { + readonly CompilationOptions compilationOptions = compilation.Options; + readonly SyntaxTreeOptionsProvider? syntaxTreeOptionsProvider = compilation.Options.SyntaxTreeOptionsProvider; +#pragma warning disable PS0025 // Dictionary keys should implement IEquatable - trees are per-compilation and use reference equality + readonly ConcurrentDictionary treeCaches = new(); +#pragma warning restore PS0025 + + public bool IsEnabled(OperationAnalysisContext context, SyntaxTree syntaxTree, DiagnosticDescriptor descriptor) + { + var configuredSeverity = ResolveConfiguredSeverity(context, syntaxTree, descriptor); + + if (configuredSeverity != ReportDiagnostic.Default) + { + return configuredSeverity != ReportDiagnostic.Suppress; + } + + return automaticallyEnabled; + } + + ReportDiagnostic ResolveConfiguredSeverity( + OperationAnalysisContext context, + SyntaxTree syntaxTree, + DiagnosticDescriptor descriptor) + { + var cancellationToken = context.CancellationToken; + + if (compilationOptions.SpecificDiagnosticOptions.TryGetValue(descriptor.Id, out var severity) || (syntaxTreeOptionsProvider is not null && + (syntaxTreeOptionsProvider.TryGetDiagnosticValue(syntaxTree, descriptor.Id, cancellationToken, out severity) || + syntaxTreeOptionsProvider.TryGetGlobalDiagnosticValue(descriptor.Id, cancellationToken, out severity)))) + { + return severity; + } + + // Bulk-configuration helper is internal to Roslyn, so mirror it: category-level first, then + // all-analyzer level. Memoized per tree since the keys don't depend on the descriptor id. + if (descriptor.IsEnabledByDefault) + { + var treeCache = treeCaches.GetOrAdd(syntaxTree, static tree => new TreeSeverityCache(tree)); + return treeCache.ResolveBulkSeverity(context.Options.AnalyzerConfigOptionsProvider, descriptor); + } + + return ReportDiagnostic.Default; + } + + sealed class TreeSeverityCache(SyntaxTree tree) + { + readonly ConcurrentDictionary categorySeverities = new(); + volatile bool allResolved; + bool hasAllSeverity; + ReportDiagnostic allSeverity; + + // Category-level bulk config varies by descriptor category; all-level is category-independent. + public ReportDiagnostic ResolveBulkSeverity( + AnalyzerConfigOptionsProvider optionsProvider, + DiagnosticDescriptor descriptor) + { + if (!categorySeverities.TryGetValue(descriptor.Category, out var categorySeverity)) + { + var treeOptions = optionsProvider.GetOptions(tree); + var found = TryGetBulkSeverity( + treeOptions, + $"dotnet_analyzer_diagnostic.category-{descriptor.Category}.severity", + out var severity); + categorySeverity = (found, severity); + categorySeverities[descriptor.Category] = categorySeverity; + } + + if (!allResolved) + { + var treeOptions = optionsProvider.GetOptions(tree); + hasAllSeverity = TryGetBulkSeverity(treeOptions, "dotnet_analyzer_diagnostic.severity", out allSeverity); + allResolved = true; + } + + if (categorySeverity.Found) + { + return categorySeverity.Severity; + } + + return hasAllSeverity ? allSeverity : ReportDiagnostic.Default; + } + + static bool TryGetBulkSeverity(AnalyzerConfigOptions options, string key, out ReportDiagnostic severity) + { + if (options.TryGetValue(key, out var value) && TryParseSeverity(value, out severity)) + { + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } + } + } + + // Mirrors AnalyzerConfigSet.TryParseSeverity. + static bool TryParseSeverity(string value, out ReportDiagnostic severity) + { + if (string.Equals(value, "default", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Default; + return true; + } + + if (string.Equals(value, "error", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Error; + return true; + } + + if (string.Equals(value, "warning", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Warn; + return true; + } + + if (string.Equals(value, "suggestion", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Info; + return true; + } + + if (string.Equals(value, "silent", StringComparison.OrdinalIgnoreCase) || + string.Equals(value, "refactoring", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Hidden; + return true; + } + + if (string.Equals(value, "none", StringComparison.OrdinalIgnoreCase)) + { + severity = ReportDiagnostic.Suppress; + return true; + } + + severity = ReportDiagnostic.Default; + return false; + } + + static bool IsTrue(AnalyzerConfigOptions options, string propertyName) => + options.TryGetValue(propertyName, out var value) && + bool.TryParse(value, out var enabled) && + enabled; + + static IOperation UnwrapImplicitConversions(IOperation operation) + { + while (operation is IConversionOperation { IsImplicit: true } conversion) + { + operation = conversion.Operand; + } + + return operation; + } + + static bool IsStableVarObjectCreation( + IOperation operation, + IArgumentOperation messageArgument, + IInvocationOperation invocation, + SemanticModel semanticModel) + { + if (!IsCompleteStatementExpression(invocation) || + operation is not ILocalReferenceOperation localReference || + localReference.Local.DeclaringSyntaxReferences is not [{ } declarationReference] || + declarationReference.GetSyntax() is not VariableDeclaratorSyntax + { + Initializer: { Value: ObjectCreationExpressionSyntax }, + Parent: VariableDeclarationSyntax + } variableDeclarator || + variableDeclarator.Parent?.Parent is not LocalDeclarationStatementSyntax declarationStatement || + declarationStatement.Declaration.Type is not IdentifierNameSyntax { Identifier.ValueText: "var" } || + semanticModel.GetOperation(variableDeclarator) is not IVariableDeclaratorOperation + { + Initializer.Value: IObjectCreationOperation + }) + { + return false; + } + + // This is intentionally a structural proof rather than a control-flow proof. The local + // declaration must be immediately followed by the invocation in the same block, with no + // executable statement between them. + if (declarationStatement.Parent is not BlockSyntax block || + invocation.Syntax.FirstAncestorOrSelf() is not { Parent: BlockSyntax invocationBlock } invocationStatement || + block.Span != invocationBlock.Span) + { + return false; + } + + var declarationIndex = block.Statements.IndexOf(declarationStatement); + if (declarationIndex < 0 || declarationIndex + 1 >= block.Statements.Count || + block.Statements[declarationIndex + 1].Span != invocationStatement.Span) + { + return false; + } + + // Only a direct local/parameter identifier receiver is accepted. In particular, this + // rejects a local function, delegate, property, member access, or any other computed + // receiver that can run code before the message argument is read. Reduced extension + // invocations expose the receiver as an implicit argument rather than Instance. + var receiver = invocation.Instance ?? + invocation.Arguments.FirstOrDefault(argument => argument.IsImplicit)?.Value; + if (receiver is not (ILocalReferenceOperation or IParameterReferenceOperation) || + receiver.Syntax is not IdentifierNameSyntax) + { + return false; + } + + // The message must be the first explicit argument. This excludes destinations, options, and + // any other preceding argument whose evaluation could mutate the captured local. + var firstExplicitArgument = invocation.Arguments.FirstOrDefault(argument => !argument.IsImplicit); + return firstExplicitArgument is not null && + firstExplicitArgument.Syntax.Span == messageArgument.Syntax.Span; + } + + static bool IsCompleteStatementExpression(IInvocationOperation invocation) + { + if (invocation.Syntax.Parent is ExpressionStatementSyntax { Expression: var expression } && + ReferenceEquals(expression, invocation.Syntax)) + { + return true; + } + + return invocation.Syntax.Parent is AwaitExpressionSyntax { Parent: ExpressionStatementSyntax { Expression: var awaitStatementExpression } } awaitExpression && + ReferenceEquals(awaitStatementExpression, awaitExpression); + } + + static bool IsRoutingEquivalent( + IOperation operation, + ITypeSymbol messageType, + INamedTypeSymbol messageCreator, + bool isUpdateMessage) + { + if (operation is IObjectCreationOperation) + { + return true; + } + + if (messageType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + return false; + } + + if (isUpdateMessage) + { + return messageType.IsValueType; + } + + if (operation is IInvocationOperation { TargetMethod: { Name: "CreateInstance", IsGenericMethod: true } creatorMethod } && + IsOrImplements(creatorMethod.ContainingType, messageCreator)) + { + return true; + } + + return messageType.IsSealed || messageType.IsValueType; + } + + static bool IsRoutingEquivalentMessageType(ITypeSymbol messageType) => + messageType.OriginalDefinition.SpecialType != SpecialType.System_Nullable_T && + (messageType.IsSealed || messageType.IsValueType); + + static bool TryMapMessageParameter( + IMethodSymbol invokeMethod, + IMethodSymbol declaredMethod, + IParameterSymbol messageParameter, + IMethodReferenceOperation methodReference, + out IParameterSymbol delegateParameter) + { + var messageIndex = declaredMethod.Parameters.IndexOf(messageParameter); + if (messageIndex < 0) + { + delegateParameter = null!; + return false; + } + + // Bound extension method references are unreduced, so exclude the receiver. + if (methodReference.Instance is not null && declaredMethod.IsExtensionMethod) + { + messageIndex--; + } + + if (messageIndex < 0 || messageIndex >= invokeMethod.Parameters.Length) + { + delegateParameter = null!; + return false; + } + + delegateParameter = invokeMethod.Parameters[messageIndex]; + return true; + } + + static bool IsOrImplements(INamedTypeSymbol type, INamedTypeSymbol contract) => + SymbolEqualityComparer.Default.Equals(type, contract) || + type.AllInterfaces.Any(candidate => SymbolEqualityComparer.Default.Equals(candidate, contract)); + + static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) + { + contractMember = null; + var containingType = method.ContainingType; + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IMessageSession) || SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IPipelineContext)) + { + return method.Name is "Send" or "Publish"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IMessageProcessingContext)) + { + return method.Name == "Reply"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.MessageSessionExtensions) || + SymbolEqualityComparer.Default.Equals(containingType, knownTypes.PipelineContextExtensions)) + { + return method.Name is "Send" or "SendLocal" or "Publish"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.MessageProcessingContextExtensions)) + { + return method.Name == "Reply"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.Saga)) + { + return method.Name == "ReplyToOriginator"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IOutgoingLogicalMessageContext)) + { + return method.Name == "UpdateMessage"; + } + + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IIncomingLogicalMessageContext)) + { + return method.Name == "UpdateMessageInstance"; + } + + return ImplementsKnownContractMember(method, knownTypes, out contractMember); + } + + // Return the contract member so renamed implementation parameters can be mapped by ordinal. + static bool ImplementsKnownContractMember(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) + { + contractMember = null; + if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage" or "UpdateMessageInstance")) + { + return false; + } + + // Avoid building interface maps for unrelated types; memoized per type. + return knownTypes.ImplementsAnyContractInterface(method.ContainingType) && knownTypes.TryResolveContractMember(method, out contractMember); + } + + static bool TryGetMessageParameter(IMethodSymbol method, IMethodSymbol? contractMember, out IParameterSymbol parameter) + { + // Implementations may rename interface parameters. + if (contractMember is not null) + { + var contractParameter = contractMember.Parameters.FirstOrDefault(candidate => + candidate.Name is "message" or "newInstance"); + if (contractParameter is not null) + { + var contractOrdinal = contractMember.Parameters.IndexOf(contractParameter); + if (contractOrdinal >= 0 && contractOrdinal < method.Parameters.Length) + { + parameter = method.Parameters[contractOrdinal]; + return true; + } + } + } + + parameter = method.Parameters.FirstOrDefault(candidate => candidate.Name is "message" or "newInstance")!; + return parameter is not null; + } + + static bool IsObjectOverload(IMethodSymbol method, IParameterSymbol messageParameter) => + !method.IsGenericMethod && messageParameter.Type.SpecialType == SpecialType.System_Object && !HasExplicitMessageTypeParameter(method); + + static bool HasExplicitMessageTypeParameter(IMethodSymbol method) => + method.Parameters.Any(candidate => candidate is { Name: "messageType", Type: INamedTypeSymbol { SpecialType: SpecialType.None, Name: "Type", ContainingNamespace.Name: "System" } }); + + static bool IsGenericMessageInstanceOverload(IMethodSymbol method, IParameterSymbol messageParameter) => + method is { IsGenericMethod: true, TypeParameters: [var messageTypeParameter, ..] } && + SymbolEqualityComparer.Default.Equals(messageParameter.Type, messageTypeParameter); + + sealed class KnownTypes + { + KnownTypes( + INamedTypeSymbol messageSession, + INamedTypeSymbol pipelineContext, + INamedTypeSymbol messageProcessingContext, + INamedTypeSymbol messageSessionExtensions, + INamedTypeSymbol pipelineContextExtensions, + INamedTypeSymbol messageProcessingContextExtensions, + INamedTypeSymbol saga, + INamedTypeSymbol outgoingLogicalMessageContext, + INamedTypeSymbol incomingLogicalMessageContext, + INamedTypeSymbol messageCreator, + INamedTypeSymbol mutateIncomingMessageContext, + INamedTypeSymbol mutateOutgoingMessageContext) + { + IMessageSession = messageSession; + IPipelineContext = pipelineContext; + IMessageProcessingContext = messageProcessingContext; + MessageSessionExtensions = messageSessionExtensions; + PipelineContextExtensions = pipelineContextExtensions; + MessageProcessingContextExtensions = messageProcessingContextExtensions; + Saga = saga; + IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; + IIncomingLogicalMessageContext = incomingLogicalMessageContext; + IMessageCreator = messageCreator; + MutateIncomingMessageContext = mutateIncomingMessageContext; + MutateOutgoingMessageContext = mutateOutgoingMessageContext; + ContractInterfaces = + [ + messageSession, + pipelineContext, + messageProcessingContext, + outgoingLogicalMessageContext, + incomingLogicalMessageContext + ]; + } + + public INamedTypeSymbol IMessageSession { get; } + public INamedTypeSymbol IPipelineContext { get; } + public INamedTypeSymbol IMessageProcessingContext { get; } + public INamedTypeSymbol MessageSessionExtensions { get; } + public INamedTypeSymbol PipelineContextExtensions { get; } + public INamedTypeSymbol MessageProcessingContextExtensions { get; } + public INamedTypeSymbol Saga { get; } + public INamedTypeSymbol IOutgoingLogicalMessageContext { get; } + public INamedTypeSymbol IIncomingLogicalMessageContext { get; } + public INamedTypeSymbol IMessageCreator { get; } + public INamedTypeSymbol MutateIncomingMessageContext { get; } + public INamedTypeSymbol MutateOutgoingMessageContext { get; } + public ImmutableArray ContractInterfaces { get; } + + readonly ConcurrentDictionary implementsContractCache = new(SymbolEqualityComparer.Default); + readonly ConcurrentDictionary contractMemberCache = new(SymbolEqualityComparer.Default); + + // Memoized per type so unrelated Send/Publish/Reply methods never build interface maps. + public bool ImplementsAnyContractInterface(INamedTypeSymbol type) + { + if (implementsContractCache.TryGetValue(type, out var result)) + { + return result; + } + + result = ContractInterfaces.Any(contract => + type.AllInterfaces.Any(implemented => + SymbolEqualityComparer.Default.Equals(implemented, contract))); + implementsContractCache[type] = result; + return result; + } + + // Memoized per method so interface-walking runs once per declaration across call sites. + public bool TryResolveContractMember(IMethodSymbol method, out IMethodSymbol? contractMember) + { + if (contractMemberCache.TryGetValue(method, out contractMember)) + { + return contractMember is not null; + } + + contractMember = method.ExplicitInterfaceImplementations.FirstOrDefault(implementedMember => + ContractInterfaces.Any(contract => + SymbolEqualityComparer.Default.Equals(implementedMember.ContainingType, contract))); + + if (contractMember is null) + { + foreach (var contract in ContractInterfaces) + { + foreach (var candidateMember in contract.GetMembers(method.Name).OfType()) + { + var implementation = method.ContainingType.FindImplementationForInterfaceMember(candidateMember); + if (implementation is not null && + SymbolEqualityComparer.Default.Equals(implementation.OriginalDefinition, method.OriginalDefinition)) + { + contractMember = candidateMember; + break; + } + } + + if (contractMember is not null) + { + break; + } + } + } + + contractMemberCache[method] = contractMember; + return contractMember is not null; + } + + public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) + { + var messageSession = compilation.GetTypeByMetadataName("NServiceBus.IMessageSession"); + var pipelineContext = compilation.GetTypeByMetadataName("NServiceBus.IPipelineContext"); + var messageProcessingContext = compilation.GetTypeByMetadataName("NServiceBus.IMessageProcessingContext"); + var messageSessionExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageSessionExtensions"); + var pipelineContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.PipelineContextExtensions"); + var messageProcessingContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageProcessingContextExtensions"); + var saga = compilation.GetTypeByMetadataName("NServiceBus.Saga"); + var outgoingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IOutgoingLogicalMessageContext"); + var incomingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IIncomingLogicalMessageContext"); + var messageCreator = compilation.GetTypeByMetadataName("NServiceBus.IMessageCreator"); + var mutateIncomingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateIncomingMessageContext"); + var mutateOutgoingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateOutgoingMessageContext"); + + if (messageSession is null || pipelineContext is null || messageProcessingContext is null || + messageSessionExtensions is null || pipelineContextExtensions is null || + messageProcessingContextExtensions is null || saga is null || + outgoingLogicalMessageContext is null || messageCreator is null || + incomingLogicalMessageContext is null || + mutateIncomingMessageContext is null || mutateOutgoingMessageContext is null) + { + knownTypes = null!; + return false; + } + + knownTypes = new KnownTypes( + messageSession, + pipelineContext, + messageProcessingContext, + messageSessionExtensions, + pipelineContextExtensions, + messageProcessingContextExtensions, + saga, + outgoingLogicalMessageContext, + incomingLogicalMessageContext, + messageCreator, + mutateIncomingMessageContext, + mutateOutgoingMessageContext); + return true; + } + } +} diff --git a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs index 796b0b6ba37..7b809300b7a 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/AddSagaInterceptor.Suppressor.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Core.Analyzer.Sagas; using System.Collections.Immutable; +using Handlers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Diagnostics; @@ -9,16 +10,23 @@ [DiagnosticAnalyzer(LanguageNames.CSharp)] public sealed class AddSagaInterceptorSuppressor : DiagnosticSuppressor { + const string Justification = "The AddSaga method has been intercepted by a statically generated variant."; + static readonly SuppressionDescriptor SuppressRUCDiagnostic = new( SupressionIds.AddSagaInterceptorSuppression, suppressedDiagnosticId: "IL2026", - justification: "The AddSaga method has been intercepted by a statically generated variant."); + justification: Justification); + + static readonly SuppressionDescriptor SuppressRDCDiagnostic = new( + SupressionIds.AddSagaInterceptorAotSuppression, + suppressedDiagnosticId: "IL3050", + justification: Justification); public override void ReportSuppressions(SuppressionAnalysisContext context) { foreach (var diagnostic in context.ReportedDiagnostics) { - if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId) + if (diagnostic.Id != SuppressRUCDiagnostic.SuppressedDiagnosticId && diagnostic.Id != SuppressRDCDiagnostic.SuppressedDiagnosticId) { continue; } @@ -45,12 +53,25 @@ public override void ReportSuppressions(SuppressionAnalysisContext context) var semanticModel = context.GetSemanticModel(sourceTree); var operation = semanticModel.GetOperation(node, context.CancellationToken); - if (operation is IInvocationOperation { TargetMethod: { } methodSymbol } && AddSagaInterceptor.Parser.IsAddSagaMethod(methodSymbol)) + if (operation is not IInvocationOperation { TargetMethod: { } methodSymbol } || !AddSagaInterceptor.Parser.IsAddSagaMethod(methodSymbol)) + { + continue; + } + + // Only suppress when an interceptor can actually be emitted for this call site. A saga that cannot be + // parsed (no Saga base, abstract, or otherwise unsupported) keeps the RequiresUnreferencedCode + // and RequiresDynamicCode fallback warnings. + if (methodSymbol.TypeArguments[0] is not INamedTypeSymbol sagaType || + !HandlerKnownTypes.TryGet(context.Compilation, out var knownTypes) || + Sagas.Parser.Parse(semanticModel, sagaType, knownTypes, context.CancellationToken) is null) { - context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic)); + continue; } + + var targetSuppression = diagnostic.Id == SuppressRUCDiagnostic.SuppressedDiagnosticId ? SuppressRUCDiagnostic : SuppressRDCDiagnostic; + context.ReportSuppression(Suppression.Create(targetSuppression, diagnostic)); } } - public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic]; + public override ImmutableArray SupportedSuppressions => [SuppressRUCDiagnostic, SuppressRDCDiagnostic]; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs index 85b61bbf32a..1d3033b9885 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs @@ -48,11 +48,14 @@ static void EmitSagaMetadataAdd(SourceWriter sourceWriter, SagaSpec details) sourceWriter.WriteLine($"{propertyAccessorClassName}.Instance,"); } - var correlationPropertyAccessorClassName = CorrelationPropertyAccessorName(details.CorrelationPropertyMapping); - var correlationPropertyAccessor = $"{correlationPropertyAccessorClassName}.Instance"; - sourceWriter.Indentation--; sourceWriter.WriteLine("];"); + + // Finder-only sagas have no correlation property and therefore no generated correlation accessor. + var correlationPropertyAccessor = details.CorrelationPropertyMapping is { } correlationProperty + ? $"{CorrelationPropertyAccessorName(details.SagaDataFullyQualifiedName, correlationProperty)}.Instance" + : "null"; + sourceWriter.WriteLine($"var metadata = NServiceBus.Sagas.SagaMetadata.Create<{details.FullyQualifiedName}, {details.SagaDataFullyQualifiedName}>(associatedMessages, {correlationPropertyAccessor}, propertyAccessors);"); sourceWriter.WriteLine("sagaMetadataCollection.Add(metadata);"); } @@ -131,14 +134,21 @@ static string MessagePropertyAccessorName(PropertyMappingSpec mapping) static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, ImmutableEquatableArray sagas) { - var uniqueMappings = new Dictionary<(string PropertyType, string PropertyName), CorrelationPropertyMappingSpec>(); + // Accessors are keyed by the concrete saga-data type plus property identity: two saga-data classes with + // the same correlation property name and type must not share an accessor, because the UnsafeAccessor + // receiver is the concrete saga-data type. + var uniqueMappings = new Dictionary<(string SagaDataType, string PropertyType, string PropertyName), (CorrelationPropertyMappingSpec Mapping, string SagaDataType)>(); foreach (var saga in sagas) { - var mapping = saga.CorrelationPropertyMapping; - var key = (mapping.PropertyType, mapping.PropertyName); + if (saga.CorrelationPropertyMapping is not { } mapping) + { + continue; + } + + var key = (saga.SagaDataFullyQualifiedName, mapping.PropertyType, mapping.PropertyName); if (!uniqueMappings.ContainsKey(key)) { - uniqueMappings.Add(key, mapping); + uniqueMappings.Add(key, (mapping, saga.SagaDataFullyQualifiedName)); } } @@ -147,19 +157,25 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl return; } - var allPropertyMappings = new List(uniqueMappings.Values); + var allPropertyMappings = new List<(CorrelationPropertyMappingSpec Mapping, string SagaDataType)>(uniqueMappings.Values); allPropertyMappings.Sort(static (a, b) => { - var typeComparison = string.CompareOrdinal(a.PropertyType, b.PropertyType); - return typeComparison != 0 ? typeComparison : string.CompareOrdinal(a.PropertyName, b.PropertyName); + var sagaTypeComparison = string.CompareOrdinal(a.SagaDataType, b.SagaDataType); + if (sagaTypeComparison != 0) + { + return sagaTypeComparison; + } + + var typeComparison = string.CompareOrdinal(a.Mapping.PropertyType, b.Mapping.PropertyType); + return typeComparison != 0 ? typeComparison : string.CompareOrdinal(a.Mapping.PropertyName, b.Mapping.PropertyName); }); sourceWriter.WriteLine(); for (var index = 0; index < allPropertyMappings.Count; index++) { - var mapping = allPropertyMappings[index]; - var accessorClassName = CorrelationPropertyAccessorName(mapping); + var (mapping, sagaDataType) = allPropertyMappings[index]; + var accessorClassName = CorrelationPropertyAccessorName(sagaDataType, mapping); _ = sourceWriter.WithCompilerGeneratedAttribute() .WithGeneratedCodeAttribute(); sourceWriter.WriteLine($"file sealed class {accessorClassName} : NServiceBus.Sagas.CorrelationPropertyAccessor"); @@ -169,15 +185,15 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl sourceWriter.WriteLine($$"""{{accessorClassName}}() { }"""); sourceWriter.WriteLine(); - sourceWriter.WriteLine("public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(sagaData);"); + sourceWriter.WriteLine($"public override object? AccessFrom(NServiceBus.IContainSagaData sagaData) => AccessFrom_Property(({sagaDataType})sagaData);"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"get_{mapping.PropertyName}\")]"); - sourceWriter.WriteLine($"static extern {mapping.PropertyType} AccessFrom_Property(NServiceBus.IContainSagaData sagaData);"); + sourceWriter.WriteLine($"static extern {mapping.PropertyType} AccessFrom_Property({sagaDataType} sagaData);"); sourceWriter.WriteLine(); - sourceWriter.WriteLine($"public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(sagaData, (({mapping.PropertyType})value));"); + sourceWriter.WriteLine($"public override void WriteTo(NServiceBus.IContainSagaData sagaData, object value) => WriteTo_Property(({sagaDataType})sagaData, (({mapping.PropertyType})value));"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = \"set_{mapping.PropertyName}\")]"); - sourceWriter.WriteLine($"static extern {mapping.PropertyType} WriteTo_Property(NServiceBus.IContainSagaData sagaData, {mapping.PropertyType} value);"); + sourceWriter.WriteLine($"static extern void WriteTo_Property({sagaDataType} sagaData, {mapping.PropertyType} value);"); sourceWriter.WriteLine(); sourceWriter.WriteLine($"public static readonly NServiceBus.Sagas.CorrelationPropertyAccessor Instance = new {accessorClassName}();"); sourceWriter.Indentation--; @@ -190,9 +206,9 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl } } - static string CorrelationPropertyAccessorName(CorrelationPropertyMappingSpec mapping) + static string CorrelationPropertyAccessorName(string sagaDataType, CorrelationPropertyMappingSpec mapping) { - var hash = NonCryptographicHash.GetHash(mapping.PropertyType, "_", mapping.PropertyName); + var hash = NonCryptographicHash.GetHash(sagaDataType, "_", mapping.PropertyType, "_", mapping.PropertyName); return $"{mapping.PropertyName}As{mapping.PropertyTypeMetadataName}Accessor_{hash:x16}"; } } diff --git a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs index 991f26a0172..b959dc3babb 100644 --- a/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs +++ b/src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs @@ -18,7 +18,7 @@ public static partial class Sagas public record SagaSpec : AddHandlerAndSagasRegistrationGenerator.Parser.BaseSpec { - public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec correlationProperty, ImmutableEquatableArray propertyMappings) + public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec? correlationProperty, ImmutableEquatableArray propertyMappings) : base(handler) { SagaDataFullyQualifiedName = sagaDataFullyQualifiedName; @@ -29,7 +29,7 @@ public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, Correlat public string SagaDataFullyQualifiedName { get; } - public CorrelationPropertyMappingSpec CorrelationPropertyMapping { get; } + public CorrelationPropertyMappingSpec? CorrelationPropertyMapping { get; } public ImmutableEquatableArray PropertyMappings { get; } public HandlerSpec Handler { get; } } @@ -60,10 +60,11 @@ public static class Parser var sagaBaseSpec = Handlers.Parser.Parse(sagaType, BaseParser.SpecKind.Saga, knownTypes, cancellationToken); var sagaDataFullyQualifiedName = sagaDataType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - // Analyze ConfigureHowToFindSaga to extract mappings + // Analyze ConfigureHowToFindSaga to extract mappings. Finder-only sagas have no correlation property + // and no property mappings but are still valid sagas that must be registered. var (correlationProperty, propertyMappings) = ExtractPropertyMappings(sagaType, sagaSemanticModel, cancellationToken); - return correlationProperty is null ? null : new SagaSpec(sagaBaseSpec, sagaDataFullyQualifiedName, correlationProperty.Value, propertyMappings); + return new SagaSpec(sagaBaseSpec, sagaDataFullyQualifiedName, correlationProperty, propertyMappings); } static INamedTypeSymbol? GetSagaDataType(INamedTypeSymbol sagaType) diff --git a/src/NServiceBus.Core.Analyzer/SupressionIds.cs b/src/NServiceBus.Core.Analyzer/SupressionIds.cs index e0a22a10a11..e0667c09575 100644 --- a/src/NServiceBus.Core.Analyzer/SupressionIds.cs +++ b/src/NServiceBus.Core.Analyzer/SupressionIds.cs @@ -4,4 +4,7 @@ public static class SupressionIds { public const string AddHandlerInterceptorSuppression = "NSBS0001"; public const string AddSagaInterceptorSuppression = "NSBS0002"; + public const string AddMessageTypeInterceptorSuppression = "NSBS0003"; + public const string AddHandlerInterceptorAotSuppression = "NSBS0004"; + public const string AddSagaInterceptorAotSuppression = "NSBS0005"; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs b/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs new file mode 100644 index 00000000000..d7a69b36b66 --- /dev/null +++ b/src/NServiceBus.Core.Analyzer/Utility/MessageHierarchyBuilder.cs @@ -0,0 +1,58 @@ +#nullable enable + +namespace NServiceBus.Core.Analyzer.Utility; + +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using NServiceBus.Core.Analyzer.Handlers; + +// Shared by handler/saga and AddMessageType generation so the emitted hierarchy has the same ordering regardless of +// which registration path wins. Mirrors the reflection-based runtime inference: interfaces rank by how many interfaces +// they inherit, classes by base-type chain depth, ordered descending with a stable sort. +static class MessageHierarchyBuilder +{ + public static IEnumerable GetTypeHierarchy(INamedTypeSymbol type, MarkerTypes markers) => + GetParentTypes(type) + .Where(t => !markers.IsMarkerInterface(t)) + .Select(t => new { Type = t, Rank = PlaceInMessageHierarchy(t) }) + .OrderByDescending(item => item.Rank) + .Select(item => item.Type); + + static IEnumerable GetParentTypes(INamedTypeSymbol type) + { + foreach (var iface in type.AllInterfaces) + { + yield return iface; + } + + var currentBase = type.BaseType; + while (currentBase is { SpecialType: not SpecialType.System_Object }) + { + if (currentBase is { } named) + { + yield return named; + } + + currentBase = currentBase.BaseType; + } + } + + static int PlaceInMessageHierarchy(INamedTypeSymbol type) + { + if (type.TypeKind == TypeKind.Interface) + { + return type.AllInterfaces.Length; + } + + var result = 0; + var current = type.BaseType; + while (current is not null) + { + result++; + current = current.BaseType; + } + + return result; + } +} diff --git a/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs b/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs index 648015c28cf..1b218c6c6ad 100644 --- a/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs +++ b/src/NServiceBus.Core.Analyzer/Utility/NonCryptographicHash.cs @@ -1,8 +1,6 @@ namespace NServiceBus.Core.Analyzer.Utility; using System; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; /// /// 64-bit FNV-1a over chars, https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function @@ -21,34 +19,28 @@ public static ulong GetHash(params ReadOnlySpan parts) { string part = parts[index]; ReadOnlySpan span = part.AsSpan(); - ref char first = ref MemoryMarshal.GetReference(span); int length = span.Length; int i = 0; // Process 4 chars at a time for (; i + 3 < length; i += 4) { - char c0 = Unsafe.Add(ref first, i); - hash ^= c0; + hash ^= span[i]; hash *= prime; - char c1 = Unsafe.Add(ref first, i + 1); - hash ^= c1; + hash ^= span[i + 1]; hash *= prime; - char c2 = Unsafe.Add(ref first, i + 2); - hash ^= c2; + hash ^= span[i + 2]; hash *= prime; - char c3 = Unsafe.Add(ref first, i + 3); - hash ^= c3; + hash ^= span[i + 3]; hash *= prime; } // Handle remainder (0–3 chars) for (; i < length; i++) { - char c = Unsafe.Add(ref first, i); - hash ^= c; + hash ^= span[i]; hash *= prime; } } diff --git a/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs b/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs index 678d331ac8e..b0f28138b3c 100644 --- a/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs +++ b/src/NServiceBus.Core.Analyzer/Utility/SourceWriter.cs @@ -110,12 +110,16 @@ static ReadOnlySpan GetNextLine(ref ReadOnlySpan remainingText, out return next; } - static unsafe void AppendSpan(StringBuilder builder, ReadOnlySpan span) + static void AppendSpan(StringBuilder builder, ReadOnlySpan span) { // There is no StringBuilder.Append(ReadOnlySpan) overload in the NS2.0 - fixed (char* ptr = span) + // SAFETY: fixed pins the span for the synchronous Append call; length is explicit, no null terminator assumed. + unsafe { - builder.Append(ptr, span.Length); + fixed (char* ptr = span) + { + builder.Append(ptr, span.Length); + } } } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs b/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs new file mode 100644 index 00000000000..6622cfcaa2a --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/LogManagerUsage.cs @@ -0,0 +1,73 @@ +namespace NServiceBus.Core.Tests.API; + +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Text; +using NServiceBus.Core.Tests.API.Infra; +using NServiceBus.Logging; +using NUnit.Framework; +using Particular.Approvals; + +// The legacy logging API (NServiceBus.Logging.LogManager.GetLogger and the +// NServiceBus.Logging.ILog logger fields it produces) is still used throughout +// NServiceBus.Core. The goal is to migrate this code to the high performance, +// source-generated loggers provided by Microsoft.Extensions.Logging using the +// [LoggerMessage] source generator. +// +// This test captures the set of types that still hold an NServiceBus.Logging.ILog +// logger field so that any new usage is immediately visible. When a type is migrated +// to a source-generated logger its ILog field is removed and it drops off this list, +// so the list naturally shrinks over time. It is acceptable for the list to grow +// only when the type affects user-facing code or when DI (dependency injection) is +// not available to obtain an ILogger. +// +// See https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator +// for more details about the Microsoft.Extensions.Logging source generator. +[TestFixture] +public class LogManagerUsage +{ + [Test] + public void ApproveLogManagerUsage() + { + var b = new StringBuilder() + .AppendLine("The following types hold an NServiceBus.Logging.ILog logger field obtained via LogManager.GetLogger.") + .AppendLine("For new code where DI is available, use the high performance source generated loggers from") + .AppendLine("Microsoft.Extensions.Logging instead (see https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator).") + .AppendLine("Changes that make this list longer should only be approved when the type affects user-facing") + .AppendLine("code or when DI is not available. The list should otherwise shrink over time.") + .AppendLine("-----"); + + foreach (var type in NServiceBusAssembly.Types.OrderBy(t => t.FullName, StringComparer.Ordinal)) + { + if (HasILogField(type)) + { + b.AppendLine(type.FullName); + } + } + + Console.WriteLine(b.ToString()); + Approver.Verify(b.ToString()); + } + + static bool HasILogField(Type type) + { + // The NServiceBus.Logging namespace contains adapter types that intentionally + // implement the legacy API surface and are not candidates for migration. + if (type.Namespace == "NServiceBus.Logging") + { + return false; + } + + // Compiler-generated state machines and closures are not meaningful migration + // units; the ILog usage they contain belongs to their containing type. + if (Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)) + { + return false; + } + + return type.GetFields(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) + .Any(field => field.FieldType == typeof(ILog)); + } +} diff --git a/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs new file mode 100644 index 00000000000..96103ab8773 --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/NullabilityWarnings.cs @@ -0,0 +1,140 @@ +namespace NServiceBus.Core.Tests.API; + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Particular.Approvals; + +// As part of the nullable reference type migration effort, individual folders are annotated with +// #nullable enable one at a time (tracked in NullableEnable.CompletedFolders.approved.txt and +// NullableEnable.IncompleteFolders.approved.txt). This test previews what would happen if nullable +// reference types were force-enabled for the whole project: files that already opt in via +// #nullable enable are unaffected, but any file without an explicit directive picks up the +// project-wide default instead of staying oblivious. This test captures the current set of +// resulting warnings so that any new regressions are immediately visible. The goal is to keep the +// list shrinking over time. Once all warnings are resolved +// and the approved file empty, this test can be deleted and enable can be set +// directly in the NServiceBus.Core.csproj. +// +// Only warnings matching the hardcoded set of nullable reference type diagnostic IDs below are captured +// (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings), +// +// See https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references for more details. +[TestFixture] +public partial class NullabilityWarnings +{ + [Test] + [CancelAfter(30_000)] + public async Task ApproveNullabilityWarnings(CancellationToken cancellationToken = default) + { + var projectPath = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "NServiceBus.Core", + "NServiceBus.Core.csproj")); + + var warnings = await BuildWithNullableEnabled(projectPath, cancellationToken); + + Approver.Verify(warnings); + } + + static async Task BuildWithNullableEnabled(string projectPath, CancellationToken cancellationToken = default) + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + startInfo.ArgumentList.Add("build"); + startInfo.ArgumentList.Add(projectPath); + startInfo.ArgumentList.Add("-c:Release"); + // Limit the build scope to NServiceBus.Core so warnings from referenced analyzer projects are excluded. + startInfo.ArgumentList.Add("--no-dependencies"); + startInfo.ArgumentList.Add("--no-incremental"); + startInfo.ArgumentList.Add("-p:Nullable=enable"); + startInfo.ArgumentList.Add("-p:TreatWarningsAsErrors=false"); + startInfo.ArgumentList.Add("-p:IsPackable=false"); + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + + var output = await outputTask; + var error = await errorTask; + + Assert.That(process.ExitCode, Is.Zero, $"Build failed:{Environment.NewLine}{error}{Environment.NewLine}{output}"); + + var warnings = NullableWarningRegex().Matches(output) + .Select(m => ScrubLine(m.Value.Trim())) + .Distinct() + .OrderBy(w => w, StringComparer.Ordinal) + .ToList(); + + var grouped = warnings + .GroupBy(w => FileRegex().Match(w).Groups["file"].Value) + .OrderBy(g => g.Key, StringComparer.Ordinal); + + var result = new StringBuilder() + .AppendLine("The following nullable warnings are present in NServiceBus.Core.") + .AppendLine("Changes that make this list longer should not be approved.") + .AppendLine("-----"); + + foreach (var group in grouped) + { + _ = result.AppendLine().AppendLine(group.Key); + foreach (var warning in group) + { + _ = result.AppendLine($" {MessageRegex().Match(warning).Groups["msg"].Value}"); + } + } + + return result.ToString(); + } + + static string ScrubLine(string line) + { + line = PathPrefixRegex().Replace(line, "", 1); + line = line.Replace('\\', '/'); + line = LineNumbersRegex().Replace(line, ""); + line = ProjectPathSuffixRegex().Replace(line, ""); + return line; + } + + [GeneratedRegex(@"^.+?(?=src[\\/])", RegexOptions.IgnoreCase)] + private static partial Regex PathPrefixRegex(); + + [GeneratedRegex(@"\(\d+,\d+\)")] + private static partial Regex LineNumbersRegex(); + + [GeneratedRegex(@"\s*\[[^\]]+[/\\][^\]]+\]$")] + private static partial Regex ProjectPathSuffixRegex(); + + [GeneratedRegex($@".+: warning CS({NullableWarningCodes}):.+")] + private static partial Regex NullableWarningRegex(); + + [GeneratedRegex(@"^(?src/[^\s:]+)")] + private static partial Regex FileRegex(); + + [GeneratedRegex($@": warning (?CS({NullableWarningCodes}):.+)$")] + private static partial Regex MessageRegex(); + + // Sourced from https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-messages/nullable-warnings + const string NullableWarningCodes = + "8597|8598|8600|8601|8602|8603|8604|8605|8607|8608|8609|8610|8611|8612|8613|8614|" + + "8615|8616|8617|8618|8619|8620|8621|8622|8623|8624|8625|8628|8629|8631|8632|8633|" + + "8634|8636|8637|8639|8643|8644|8645|8650|8651|8655|8667|8668|8669|8670|8714|8762|" + + "8763|8764|8765|8766|8767|8768|8769|8770|8774|8775|8776|8777|8819|8824|8825|8847"; +} diff --git a/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs b/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs new file mode 100644 index 00000000000..943746294c5 --- /dev/null +++ b/src/NServiceBus.Core.Tests/API/NullableEnabledDirectories.cs @@ -0,0 +1,169 @@ +namespace NServiceBus.Core.Tests.API; + +using System; +using System.IO; +using System.Linq; +using System.Text; +using NUnit.Framework; + +[TestFixture] +public class NullableEnabledDirectories +{ + [Test] + public void EnsureFilesInCompletedDirectoriesAreAnnotated() + { + var sourceRoot = FindSourceRoot(); + var completedDirectories = ReadCompletedDirectories(sourceRoot); + + var violations = new StringBuilder(); + + foreach (var relativeDirectory in completedDirectories) + { + var directory = Path.Combine(sourceRoot, relativeDirectory); + + if (!Directory.Exists(directory)) + { + violations.AppendLine($"{relativeDirectory} (directory listed in {ApprovedFileName} no longer exists)"); + continue; + } + + foreach (var file in Directory.EnumerateFiles(directory, "*.cs", SearchOption.TopDirectoryOnly)) + { + if (!IsAnnotated(file)) + { + violations.AppendLine(Path.GetRelativePath(sourceRoot, file).Replace('\\', '/')); + } + } + } + + if (violations.Length > 0) + { + Assert.Fail( + $"The following files are missing the '#nullable enable' annotation, followed by a blank line, " + + $"even though they live in a directory listed as fully migrated in {ApprovedFileName}. Either fix " + + $"the file or, if the directory is no longer fully migrated, remove it from {ApprovedFileName}:{Environment.NewLine}{violations}"); + } + } + + [Test] + public void EnsureFullyAnnotatedDirectoriesAreListedAsCompleted() + { + var sourceRoot = FindSourceRoot(); + var completedDirectories = ReadCompletedDirectories(sourceRoot).ToHashSet(StringComparer.Ordinal); + + var missingDirectories = Directory.EnumerateFiles(sourceRoot, "*.cs", SearchOption.AllDirectories) + .Where(file => !IsBinOrObj(file)) + .GroupBy(file => Path.GetDirectoryName(file)!) + .Select(group => new + { + Directory = Path.GetRelativePath(sourceRoot, group.Key).Replace('\\', '/'), + Total = group.Count(), + Annotated = group.Count(IsAnnotated) + }) + .Where(entry => entry.Annotated == entry.Total) + .Where(entry => !completedDirectories.Contains(entry.Directory)) + .OrderBy(entry => entry.Directory, StringComparer.Ordinal) + .Select(entry => entry.Directory) + .ToArray(); + + if (missingDirectories.Length > 0) + { + Assert.Fail( + $"The following directories have every '.cs' file annotated with '#nullable enable' but are not " + + $"listed in {ApprovedFileName}. Add them to the approval file to lock in the migration:" + + $"{Environment.NewLine}{string.Join(Environment.NewLine, missingDirectories)}"); + } + } + + [Test] + [Explicit("Run this test to generate a report of directories that are not fully annotated with '#nullable enable'.")] + public void GenerateIncompleteDirectoriesReport() + { + var sourceRoot = FindSourceRoot(); + var approvalFilesDirectory = Path.Combine(sourceRoot, "NServiceBus.Core.Tests", "ApprovalFiles"); + var incompleteReportFilePath = Path.Combine(approvalFilesDirectory, IncompleteReceivedFileName); + + var incompleteReport = BuildIncompleteDirectoriesReport(sourceRoot); + + if (Path.Exists(incompleteReport)) + { + File.Delete(incompleteReportFilePath); + } + + File.WriteAllText(incompleteReportFilePath, incompleteReport); + } + + static string BuildIncompleteDirectoriesReport(string sourceRoot) + { + var builder = new StringBuilder() + .AppendLine("The following directories have at least one .cs file that is NOT annotated with #nullable enable.") + .AppendLine("Format: directoryannotated/total files.") + .AppendLine("-----") + .AppendLine("This file is for analysis purposes only and should not be committed."); + + var directories = Directory.EnumerateFiles(sourceRoot, "*.cs", SearchOption.AllDirectories) + .Where(file => !IsBinOrObj(file)) + .GroupBy(file => Path.GetDirectoryName(file)!) + .Select(group => new + { + Directory = Path.GetRelativePath(sourceRoot, group.Key).Replace('\\', '/'), + Total = group.Count(), + Annotated = group.Count(IsAnnotated) + }) + .Where(entry => entry.Annotated < entry.Total) + .OrderBy(entry => entry.Directory, StringComparer.Ordinal); + + foreach (var entry in directories) + { + builder.AppendLine($"{entry.Directory}\t{entry.Annotated}/{entry.Total}"); + } + + return builder.ToString(); + } + + static bool IsAnnotated(string file) + { + var firstTwoLines = File.ReadLines(file).Take(2).ToArray(); + var firstLine = firstTwoLines.ElementAtOrDefault(0); + var secondLine = firstTwoLines.ElementAtOrDefault(1); + + return firstLine == "#nullable enable" && secondLine == ""; + } + + static bool IsBinOrObj(string path) + { + var normalized = path.Replace('\\', '/'); + return normalized.Contains("/bin/") || normalized.Contains("/obj/"); + } + + static string[] ReadCompletedDirectories(string sourceRoot) + { + var approvedFilePath = Path.Combine(sourceRoot, "NServiceBus.Core.Tests", "ApprovalFiles", ApprovedFileName); + + return File.ReadAllLines(approvedFilePath) + .SkipWhile(line => line != "-----") + .Skip(1) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .ToArray(); + } + + static string FindSourceRoot() + { + var directory = TestContext.CurrentContext.TestDirectory; + + while (directory != null) + { + if (Directory.GetFiles(directory, "*.csproj").Length == 1) + { + return Directory.GetParent(directory)!.FullName; + } + + directory = Directory.GetParent(directory)?.FullName; + } + + throw new InvalidOperationException("Could not find the src directory."); + } + + const string ApprovedFileName = "NullableEnable.CompletedFolders.approved.txt"; + const string IncompleteReceivedFileName = "NullableEnable.IncompleteFolders.txt"; +} diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 04b4bf1c532..5a9feef8a4a 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1,6 +1,5 @@ [assembly: System.CLSCompliant(false)] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.AcceptanceTesting, PublicKey=0024000004800000940000000602000000240000525341310004000001000100dde965e6172e019ac82c2639ffe494dd2e7dd16347c34762a05732b492e110f2e4e2e1b5ef2d85c848ccfb671ee20a47c8d1376276708dc30a90ff1121b647ba3b7259a6bc383b2034938ef0e275b58b920375ac605076178123693c6c4f1331661a62eba28c249386855637780e3ff5f23a6d854700eaa6803ef48907513b92")] -[assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.ContainerTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.Core.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo(@"NServiceBus.PersistenceTests, PublicKey=00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5")] [assembly: System.Runtime.InteropServices.ComVisible(false)] @@ -249,6 +248,8 @@ namespace NServiceBus public static class DiagnosticSettingsExtensions { public static void AddStartupDiagnosticsSection(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, object section) { } + public static void AddStartupDiagnosticsSection(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, T section, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } + public static void AddStartupDiagnosticsSectionFactory(this NServiceBus.Settings.IReadOnlySettings settings, string sectionName, System.Func sectionFactory, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } public static void CustomDiagnosticsWriter(this NServiceBus.EndpointConfiguration config, System.Func customDiagnosticsWriter) { } public static void SetDiagnosticsPath(this NServiceBus.EndpointConfiguration config, string path) { } public static void WriteDiagnosticsToLog(this NServiceBus.EndpointConfiguration config) { } @@ -318,7 +319,7 @@ namespace NServiceBus { extension(NServiceBus.Features.FeatureConfigurationContext context) { - public void AddEnvelopeHandler() + public void AddEnvelopeHandler<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : class, NServiceBus.IEnvelopeHandler { } } } @@ -528,9 +529,9 @@ namespace NServiceBus } public interface IMessageCreator { - object CreateInstance(System.Type messageType); - T CreateInstance(); - T CreateInstance(System.Action action); + object CreateInstance([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(); + T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action); } public interface IMessageHandlerContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext { @@ -543,15 +544,42 @@ namespace NServiceBus string MessageId { get; } string ReplyToAddress { get; } System.Threading.Tasks.Task ForwardCurrentMessageTo(string destination); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Reply(object message, NServiceBus.ReplyOptions options); - System.Threading.Tasks.Task Reply(System.Action messageConstructor, NServiceBus.ReplyOptions options); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Reply(object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.ReplyOptions options); + System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.ReplyOptions options); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.ReplyOptions options); } public interface IMessageSession { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Publish(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Publish(object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.PublishOptions publishOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Send(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Send(object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.SendOptions sendOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Subscribe(System.Type eventType, NServiceBus.SubscribeOptions subscribeOptions, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Unsubscribe(System.Type eventType, NServiceBus.UnsubscribeOptions unsubscribeOptions, System.Threading.CancellationToken cancellationToken = default); } @@ -565,10 +593,28 @@ namespace NServiceBus } public interface IPipelineContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Publish(object message, NServiceBus.PublishOptions options); - System.Threading.Tasks.Task Publish(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Publish(object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.PublishOptions options); + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.PublishOptions publishOptions); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.PublishOptions options); + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] System.Threading.Tasks.Task Send(object message, NServiceBus.SendOptions options); - System.Threading.Tasks.Task Send(System.Action messageConstructor, NServiceBus.SendOptions options); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + System.Threading.Tasks.Task Send(object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.SendOptions options); + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action messageConstructor, NServiceBus.SendOptions options); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T message, NServiceBus.SendOptions options); } public interface ISagaNotFoundHandler { @@ -667,6 +713,7 @@ namespace NServiceBus public class LearningPersistence : NServiceBus.Persistence.PersistenceDefinition, NServiceBus.Persistence.IPersistenceDefinitionFactory { } public static class LearningSagaPersisterConfigurationExtensions { + public static void SagaSerializerOptions(this NServiceBus.PersistenceExtensions persistenceExtensions, System.Text.Json.JsonSerializerOptions options) { } public static void SagaStorageDirectory(this NServiceBus.PersistenceExtensions persistenceExtensions, string path) { } } public class LearningTransport : NServiceBus.Transport.TransportDefinition @@ -708,10 +755,16 @@ namespace NServiceBus { public static void DisablePublishing(this NServiceBus.RoutingSettings routingSettings) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Reflection.Assembly assembly, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Type eventType, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public static void RegisterPublisher(this NServiceBus.RoutingSettings routingSettings, System.Reflection.Assembly assembly, string @namespace, string publisherEndpoint) where T : NServiceBus.Transport.TransportDefinition, NServiceBus.Routing.IMessageDrivenSubscriptionTransport { } public static void SubscriptionAuthorizer(this NServiceBus.RoutingSettings routingSettings, System.Func authorizer) @@ -719,6 +772,8 @@ namespace NServiceBus } public static class MessageHandlerRegistrationExtensions { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public static void AddHandler<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] THandler>(this NServiceBus.EndpointConfiguration config) { } @@ -741,8 +796,14 @@ namespace NServiceBus } public static class MessageProcessingContextExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, object message) { } - public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Reply(this NServiceBus.IMessageProcessingContext context, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageProcessingContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Reply<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageProcessingContext context, T message) { } } public static class MessageProcessingOptimizationExtensions { @@ -753,20 +814,49 @@ namespace NServiceBus } public static class MessageSessionExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish(this NServiceBus.IMessageSession session, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IMessageSession session, string destination, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, string destination, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, string destination, T message, System.Threading.CancellationToken cancellationToken = default) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, object message, System.Threading.CancellationToken cancellationToken = default) { } - public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IMessageSession session, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, System.Action messageConstructor, System.Threading.CancellationToken cancellationToken = default) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IMessageSession session, T message, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Subscribe(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Type messageType, System.Threading.CancellationToken cancellationToken = default) { } public static System.Threading.Tasks.Task Unsubscribe(this NServiceBus.IMessageSession session, System.Threading.CancellationToken cancellationToken = default) { } } + public static class MessageTypeRegistrationExtensions + { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode(@"AddMessageType relies on an NServiceBus source-generated interceptor to register the statically known message hierarchy without reflection when trimming is enabled. If this warning is reported, the interceptor was not used for this call; enable or restore the NServiceBus analyzer/source-generator tooling and use a statically known message type.")] + public static void AddMessageType<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TMessage>(this NServiceBus.EndpointConfiguration config) { } + } public class MetersOptions { public MetersOptions() { } @@ -855,15 +945,39 @@ namespace NServiceBus } public static class PipelineContextExtensions { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context) { } - public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Publish(this NServiceBus.IPipelineContext context, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context) { } + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Publish<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, object message) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, object message) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } - public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task Send(this NServiceBus.IPipelineContext context, string destination, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, string destination, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task Send<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, string destination, T message) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, object message) { } - public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + public static System.Threading.Tasks.Task SendLocal(this NServiceBus.IPipelineContext context, object message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, System.Action messageConstructor) { } + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + public static System.Threading.Tasks.Task SendLocal<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(this NServiceBus.IPipelineContext context, T message) { } } public class PublishOptions : NServiceBus.Extensibility.ExtendableOptions { @@ -1005,8 +1119,12 @@ namespace NServiceBus { public RoutingSettings(NServiceBus.Settings.SettingsHolder settings) { } public void DoNotEnforceBestPractices() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Routing messages by assembly or namespace requires assembly scanning and is not s" + + "upported in trimming scenarios. Register routes by message type instead.")] public void RouteToEndpoint(System.Reflection.Assembly assembly, string destination) { } public void RouteToEndpoint(System.Type messageType, string destination) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Routing messages by assembly or namespace requires assembly scanning and is not s" + + "upported in trimming scenarios. Register routes by message type instead.")] public void RouteToEndpoint(System.Reflection.Assembly assembly, string @namespace, string destination) { } } public class RoutingSettings : NServiceBus.RoutingSettings @@ -1021,13 +1139,20 @@ namespace NServiceBus public NServiceBus.IContainSagaData Entity { get; set; } protected abstract void ConfigureHowToFindSaga(NServiceBus.IConfigureHowToFindSagaWithMessage sagaMessageFindingConfiguration); protected void MarkAsComplete() { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [System.Obsolete("Use \'ReplyToOriginator\' instead. Will be treated as an error from version 11.0" + + ".0. Will be removed in version 12.0.0.", false)] protected System.Threading.Tasks.Task ReplyToOriginator(NServiceBus.IMessageHandlerContext context, object message, System.Collections.Generic.IReadOnlyDictionary? outgoingHeaders = null) { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at) + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + protected System.Threading.Tasks.Task ReplyToOriginator<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(NServiceBus.IMessageHandlerContext context, T message, System.Collections.Generic.IReadOnlyDictionary? outgoingHeaders = null) { } + protected System.Threading.Tasks.Task RequestTimeout<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at) where TTimeoutMessageType : new() { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.TimeSpan within) + protected System.Threading.Tasks.Task RequestTimeout<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.TimeSpan within) where TTimeoutMessageType : new() { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at, TTimeoutMessageType timeoutMessage) { } - protected System.Threading.Tasks.Task RequestTimeout(NServiceBus.IMessageHandlerContext context, System.TimeSpan within, TTimeoutMessageType timeoutMessage) { } + protected System.Threading.Tasks.Task RequestTimeout<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.DateTimeOffset at, TTimeoutMessageType timeoutMessage) { } + protected System.Threading.Tasks.Task RequestTimeout<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TTimeoutMessageType>(NServiceBus.IMessageHandlerContext context, System.TimeSpan within, TTimeoutMessageType timeoutMessage) { } } [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple=false, Inherited=false)] public sealed class SagaAttribute : System.Attribute @@ -1052,7 +1177,9 @@ namespace NServiceBus } public static class SagaRegistrationExtensions { - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static void AddSaga<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>(this NServiceBus.EndpointConfiguration config) where TSaga : NServiceBus.Saga, NServiceBus.IHandleMessages { } } @@ -1113,6 +1240,8 @@ namespace NServiceBus { public StartupDiagnosticEntries() { } public void Add(string sectionName, object section) { } + public void Add(string sectionName, T section, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } + public void AddFactory(string sectionName, System.Func sectionFactory, System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo) { } public class StartupDiagnosticEntry { public StartupDiagnosticEntry() { } @@ -1160,8 +1289,14 @@ namespace NServiceBus public class SubscriptionMigrationModeSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { public SubscriptionMigrationModeSettings(NServiceBus.Settings.SettingsHolder settings) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public void RegisterPublisher(System.Reflection.Assembly assembly, string publisherEndpoint) { } public void RegisterPublisher(System.Type eventType, string publisherEndpoint) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering publishers by assembly or namespace requires assembly scanning and is" + + " not supported in trimming scenarios. Register publishers by message type instea" + + "d.")] public void RegisterPublisher(System.Reflection.Assembly assembly, string @namespace, string publisherEndpoint) { } public void SubscriptionAuthorizer(System.Func authorizer) { } } @@ -1274,7 +1409,10 @@ namespace NServiceBus public static NServiceBus.Serialization.SerializationExtensions Namespace(this NServiceBus.Serialization.SerializationExtensions config, string namespaceToUse) { } public static NServiceBus.Serialization.SerializationExtensions SanitizeInput(this NServiceBus.Serialization.SerializationExtensions config) { } } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("XmlSerializer is not supported in trimming scenarios.")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("XmlSerializer relies on dynamic code generation which is not available with Ahead" + + " of Time compilation")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("XmlSerializer is not supported in trimming scenarios and members from deserialize" + + "d types may be trimmed if not referenced directly.")] public class XmlSerializer : NServiceBus.Serialization.SerializationDefinition { public XmlSerializer() { } @@ -1749,12 +1887,28 @@ namespace NServiceBus.MessageInterfaces } namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection { + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("The message mapper generates concrete proxies for interface-based messages using " + + "System.Reflection.Emit, which is not supported when dynamic code generation is u" + + "navailable (e.g. trimming or NativeAOT).")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("The message mapper reflects over all fields, properties, constructors and custom " + + "attributes of the provided message types to generate concrete proxies, which can" + + "not be statically analyzed by the trimmer.")] public class MessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper { public MessageMapper() { } - public object CreateInstance(System.Type t) { } - public T CreateInstance() { } - public T CreateInstance(System.Action action) { } + public object CreateInstance([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type t) { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>() { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action) { } + public System.Type? GetMappedTypeFor(System.Type t) { } + public System.Type? GetMappedTypeFor(string typeName) { } + public void Initialize(System.Collections.Generic.IEnumerable? types) { } + } + public sealed class TrimmingSafeMessageMapper : NServiceBus.IMessageCreator, NServiceBus.MessageInterfaces.IMessageMapper + { + public TrimmingSafeMessageMapper() { } + public object CreateInstance([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type t) { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>() { } + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Action action) { } public System.Type? GetMappedTypeFor(System.Type t) { } public System.Type? GetMappedTypeFor(string typeName) { } public void Initialize(System.Collections.Generic.IEnumerable? types) { } @@ -1783,7 +1937,15 @@ namespace NServiceBus.MessageMutator public MutateIncomingMessageContext(object message, System.Collections.Generic.Dictionary headers, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary Headers { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [set: System.Obsolete("Use \'UpdateMessageInstance(T)\' or \'UpdateMessageInstance(object, Type)\' instea" + + "d. Will be treated as an error from version 11.0.0. Will be removed in version 1" + + "2.0.0.", false)] public object Message { get; set; } + public void UpdateMessageInstance(object newMessage, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessageInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateIncomingTransportMessageContext : NServiceBus.ICancellableContext { @@ -1797,9 +1959,16 @@ namespace NServiceBus.MessageMutator public MutateOutgoingMessageContext(object outgoingMessage, System.Collections.Generic.Dictionary outgoingHeaders, object? incomingMessage, System.Collections.Generic.IReadOnlyDictionary? incomingHeaders, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary OutgoingHeaders { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] + [set: System.Obsolete("Use \'UpdateMessage(T)\' or \'UpdateMessage(object, Type)\' instead. Will be treat" + + "ed as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public object OutgoingMessage { get; set; } public bool TryGetIncomingHeaders([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Collections.Generic.IReadOnlyDictionary? incomingHeaders) { } public bool TryGetIncomingMessage([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? incomingMessage) { } + public void UpdateMessage(object newMessage, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessage<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateOutgoingTransportMessageContext : NServiceBus.ICancellableContext { @@ -1821,7 +1990,7 @@ namespace NServiceBus.Outbox public interface IOutboxStorage { System.Threading.Tasks.Task BeginTransaction(NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); - System.Threading.Tasks.Task Get(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); + System.Threading.Tasks.Task Get(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task SetAsDispatched(string messageId, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); System.Threading.Tasks.Task Store(NServiceBus.Outbox.OutboxMessage message, NServiceBus.Outbox.IOutboxTransaction transaction, NServiceBus.Extensibility.ContextBag context, System.Threading.CancellationToken cancellationToken = default); } @@ -1838,11 +2007,11 @@ namespace NServiceBus.Outbox public class OutboxSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { } public class TransportOperation { - public TransportOperation(string messageId, NServiceBus.Transport.DispatchProperties properties, System.ReadOnlyMemory body, System.Collections.Generic.Dictionary headers) { } + public TransportOperation(string messageId, NServiceBus.Transport.DispatchProperties? properties, System.ReadOnlyMemory body, System.Collections.Generic.Dictionary? headers) { } public System.ReadOnlyMemory Body { get; } - public System.Collections.Generic.Dictionary Headers { get; } + public System.Collections.Generic.Dictionary? Headers { get; } public string MessageId { get; } - public NServiceBus.Transport.DispatchProperties Options { get; } + public NServiceBus.Transport.DispatchProperties? Options { get; } } } namespace NServiceBus.Performance.TimeToBeReceived @@ -1947,7 +2116,16 @@ namespace NServiceBus.Pipeline System.Collections.Generic.Dictionary Headers { get; } NServiceBus.Pipeline.LogicalMessage Message { get; } bool MessageHandled { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] void UpdateMessageInstance(object newInstance); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + void UpdateMessageInstance(object newInstance, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newInstance); } public interface IIncomingPhysicalMessageContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext, NServiceBus.Pipeline.IBehaviorContext, NServiceBus.Pipeline.IIncomingContext { @@ -1971,7 +2149,16 @@ namespace NServiceBus.Pipeline { NServiceBus.Pipeline.OutgoingLogicalMessage Message { get; } System.Collections.Generic.IReadOnlyCollection RoutingStrategies { get; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] void UpdateMessage(object newInstance); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + void UpdateMessage(object newInstance, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + void UpdateMessage<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newInstance); } public interface IOutgoingPhysicalMessageContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IPipelineContext, NServiceBus.Pipeline.IBehaviorContext, NServiceBus.Pipeline.IOutgoingContext { @@ -2056,6 +2243,7 @@ namespace NServiceBus.Pipeline { public LogicalMessageFactory(NServiceBus.Unicast.Messages.MessageMetadataRegistry messageMetadataRegistry, NServiceBus.MessageInterfaces.IMessageMapper messageMapper) { } public NServiceBus.Pipeline.LogicalMessage Create(object message) { } + public NServiceBus.Pipeline.LogicalMessage Create(NServiceBus.Unicast.Messages.MessageMetadata metadata, object message) { } public NServiceBus.Pipeline.LogicalMessage Create(System.Type messageType, object message) { } } public class MessageHandler @@ -2069,35 +2257,36 @@ namespace NServiceBus.Pipeline } public class OutgoingLogicalMessage { - public OutgoingLogicalMessage(System.Type messageType, object message) { } + public OutgoingLogicalMessage([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, object message) { } public object Instance { get; } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } } public class PipelineSettings : NServiceBus.Configuration.AdvancedExtensibility.ExposeSettings { public void Register(NServiceBus.Pipeline.RegisterStep registration) { } - public void Register(System.Type behavior, string description) { } - public void Register(string stepId, System.Type behavior, string description) { } + public void Register([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string description) { } + public void Register(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string description) { } public void Register() where TRegisterStep : NServiceBus.Pipeline.RegisterStep, new () { } - public void Register(string description) { } - public void Register(System.Func factoryMethod, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TBehavior>(string description) { } + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(System.Func factoryMethod, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(T behavior, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T behavior, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(string stepId, System.Func factoryMethod, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void Register(string stepId, T behavior, string description) + public void Register<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string description) where T : NServiceBus.Pipeline.IBehavior { } - public void RegisterOrReplace(string stepId, System.Type behavior, string? description = null) { } - public void RegisterOrReplace(string stepId, System.Func factoryMethod, string? description = null) + public void RegisterOrReplace(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string? description = null) { } + public void RegisterOrReplace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void RegisterOrReplace(string stepId, T behavior, string? description = null) + public void RegisterOrReplace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void Replace(string stepId, System.Type newBehavior, string? description = null) { } - public void Replace(string stepId, System.Func factoryMethod, string? description = null) + public void Replace(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type newBehavior, string? description = null) { } + public void Replace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, System.Func factoryMethod, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } - public void Replace(string stepId, T newBehavior, string? description = null) + public void Replace<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T newBehavior, string? description = null) where T : NServiceBus.Pipeline.IBehavior { } } public abstract class PipelineTerminator : NServiceBus.Pipeline.StageConnector.ITerminatingContext> @@ -2111,7 +2300,8 @@ namespace NServiceBus.Pipeline [System.Diagnostics.DebuggerDisplay("{StepId}({BehaviorType.FullName}) - {Description}")] public abstract class RegisterStep { - protected RegisterStep(string stepId, System.Type behavior, string? description, System.Func? factoryMethod = null) { } + protected RegisterStep(string stepId, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type behavior, string? description, System.Func? factoryMethod = null) { } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type BehaviorType { get; } public string Description { get; } public string StepId { get; } @@ -2195,12 +2385,13 @@ namespace NServiceBus.Routing public interface IMessageDrivenSubscriptionTransport { } public class MulticastAddressTag : NServiceBus.Routing.AddressTag { - public MulticastAddressTag(System.Type messageType) { } + public MulticastAddressTag([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } } public class MulticastRoutingStrategy : NServiceBus.Routing.RoutingStrategy { - public MulticastRoutingStrategy(System.Type messageType) { } + public MulticastRoutingStrategy([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } public override NServiceBus.Routing.AddressTag Apply(System.Collections.Generic.Dictionary headers) { } } public class RouteTableEntry @@ -2375,13 +2566,17 @@ namespace NServiceBus.Sagas [System.Obsolete("Use the overload without available types and conventions. Will be removed in vers" + "ion 11.0.0.", true)] public static NServiceBus.Sagas.SagaMetadata Create(System.Type sagaType, System.Collections.Generic.IEnumerable availableTypes, NServiceBus.Conventions conventions) { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga>() where TSaga : NServiceBus.Saga { } public static NServiceBus.Sagas.SagaMetadata Create<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] TSaga, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties)] TSagaData>(System.Collections.Generic.IReadOnlyCollection associatedMessages, NServiceBus.Sagas.CorrelationPropertyAccessor? correlationPropertyAccessor = null, System.Collections.Generic.IReadOnlyCollection? propertyAccessors = null) where TSaga : NServiceBus.Saga where TSagaData : class, NServiceBus.IContainSagaData, new () { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation whic" + + "h is not available with Ahead of Time compilation.")] + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code.")] public static System.Collections.Generic.IEnumerable CreateMany(System.Collections.Generic.IEnumerable sagaTypes) { } public class CorrelationPropertyMetadata { @@ -2415,7 +2610,7 @@ namespace NServiceBus.Serialization public interface IMessageSerializer { string ContentType { get; } - object[] Deserialize(System.ReadOnlyMemory body, System.Collections.Generic.IList messageTypes = null); + object[] Deserialize(System.ReadOnlyMemory body, System.Collections.Generic.IList? messageTypes = null); void Serialize(object message, System.IO.Stream stream); } public abstract class SerializationDefinition @@ -2484,13 +2679,26 @@ namespace NServiceBus.Transport Default = 1, Isolated = 2, } - public class DispatchProperties : System.Collections.Generic.Dictionary + public class DispatchProperties : System.Collections.Generic.ICollection>, System.Collections.Generic.IDictionary, System.Collections.Generic.IEnumerable>, System.Collections.Generic.IReadOnlyCollection>, System.Collections.Generic.IReadOnlyDictionary, System.Collections.IEnumerable { public DispatchProperties() { } public DispatchProperties(System.Collections.Generic.Dictionary properties) { } - public NServiceBus.DelayedDelivery.DelayDeliveryWith DelayDeliveryWith { get; set; } - public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore { get; set; } - public NServiceBus.DelayedDelivery.DoNotDeliverBefore DoNotDeliverBefore { get; set; } + public DispatchProperties(System.Collections.Generic.IDictionary properties) { } + public int Count { get; } + public NServiceBus.DelayedDelivery.DelayDeliveryWith? DelayDeliveryWith { get; set; } + public NServiceBus.Performance.TimeToBeReceived.DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { get; set; } + public NServiceBus.DelayedDelivery.DoNotDeliverBefore? DoNotDeliverBefore { get; set; } + public bool IsReadOnly { get; } + public string this[string key] { get; set; } + public System.Collections.Generic.ICollection Keys { get; } + public System.Collections.Generic.ICollection Values { get; } + public void Add(string key, string value) { } + public void Clear() { } + public bool ContainsKey(string key) { } + public System.Collections.Generic.IEnumerator> GetEnumerator() { } + public bool Remove(string key) { } + public bool TryAdd(string key, string value) { } + public bool TryGetValue(string key, [System.Diagnostics.CodeAnalysis.MaybeNullWhen(false)] out string value) { } } public class ErrorContext { @@ -2592,8 +2800,9 @@ namespace NServiceBus.Transport } public class MulticastTransportOperation : NServiceBus.Transport.IOutgoingTransportOperation { - public MulticastTransportOperation(NServiceBus.Transport.OutgoingMessage message, System.Type messageType, NServiceBus.Transport.DispatchProperties properties, NServiceBus.Transport.DispatchConsistency requiredDispatchConsistency = 1) { } + public MulticastTransportOperation(NServiceBus.Transport.OutgoingMessage message, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType, NServiceBus.Transport.DispatchProperties properties, NServiceBus.Transport.DispatchConsistency requiredDispatchConsistency = 1) { } public NServiceBus.Transport.OutgoingMessage Message { get; } + [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] public System.Type MessageType { get; } public NServiceBus.Transport.DispatchProperties Properties { get; } public NServiceBus.Transport.DispatchConsistency RequiredDispatchConsistency { get; } @@ -2711,6 +2920,8 @@ namespace NServiceBus.Unicast public class MessageHandlerRegistry { public MessageHandlerRegistry() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public void AddHandler() { } @@ -2718,6 +2929,8 @@ namespace NServiceBus.Unicast where THandler : class, NServiceBus.IHandleMessages { } public void AddMessageHandlerForMessage<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicMethods | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] THandlerAdapter, TMessage, THandler>() where THandlerAdapter : class, NServiceBus.IHandleMessages { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] public void AddScannedHandlers(System.Collections.Generic.IEnumerable orderedTypes) { } @@ -2726,6 +2939,8 @@ namespace NServiceBus.Unicast public void Clear() { } public System.Collections.Generic.List GetHandlersFor(System.Type messageType) { } public System.Collections.Generic.IEnumerable GetMessageTypes() { } + [System.Diagnostics.CodeAnalysis.RequiresDynamicCode("Registering handlers using assembly scanning relies on dynamic code generation wh" + + "ich is not available with Ahead of Time compilation.")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Registering handlers using assembly scanning is not supported in trimming scenari" + "os.")] [System.Obsolete("Use \'AddHandler()\' instead. Will be treated as an error from version 11" + diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt index 8288aa603b5..c8850eae524 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldSupportWritingToLogAndWriter.approved.txt @@ -1,3 +1,2 @@ {"Endpoint":{"EndpointName":"MyEndpointOne"}} - INFO TestingLoggerFactory Logging to testing logger with level Debug INFO NServiceBus.HostStartupDiagnosticsWriter Startup diagnostics: {"Endpoint":{"EndpointName":"MyEndpointOne"}}. diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt new file mode 100644 index 00000000000..72902b4d1f7 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteAlphabeticalSectionOrder.approved.txt @@ -0,0 +1 @@ +{"Alpha":{"Value":"a"},"Mike":{"Value":"m"},"Zulu":{"Value":"z"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt new file mode 100644 index 00000000000..0f2236a5255 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteDuplicateEntriesWithSynthesizedNameCollision.approved.txt @@ -0,0 +1 @@ +{"Section":{"Value":"First"},"Section-2":{"Value":"AlreadySynthesized"},"Section-3":{"Value":"Third"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt new file mode 100644 index 00000000000..3d9befbeea8 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteEscapedPropertyNames.approved.txt @@ -0,0 +1 @@ +{"Special":{"Property With Spaces":true,"Normal":42}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt new file mode 100644 index 00000000000..bdeaec570f8 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteMixedTypedAndLegacySections.approved.txt @@ -0,0 +1 @@ +{"Legacy":{"Value":"old"},"Typed":{"Name":"MyEndpoint","SendOnly":false,"NServiceBusVersion":"1.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt new file mode 100644 index 00000000000..99ec4686f2e --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteNullAndNestedValues.approved.txt @@ -0,0 +1 @@ +{"Nested":{"Inner":{"Deep":"value"}},"NullValue":{"Value":null}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt new file mode 100644 index 00000000000..5dc714ebc94 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedDuplicateEntries.approved.txt @@ -0,0 +1 @@ +{"Section":{"Name":"First","SendOnly":false,"NServiceBusVersion":"1.0.0"},"Section-2":{"Name":"Second","SendOnly":false,"NServiceBusVersion":"2.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt new file mode 100644 index 00000000000..278c2c516ce --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSection.approved.txt @@ -0,0 +1 @@ +{"Typed":{"Name":"MyEndpoint","SendOnly":false,"NServiceBusVersion":"1.0.0"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt new file mode 100644 index 00000000000..2138da8a4ec --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithNestedCollections.approved.txt @@ -0,0 +1 @@ +{"NestedCollections":{"LocalQueueAddress":{"BaseAddress":"myqueue","Discriminator":null,"Properties":{},"Qualifier":null},"InstanceSpecificQueueAddress":null,"PurgeOnStartup":false,"TransactionMode":"TransactionScope","MaxConcurrency":10,"Satellites":[{"Name":"Sat1","ReceiveAddress":{"BaseAddress":"satqueue","Discriminator":null,"Properties":{},"Qualifier":null},"MaxConcurrency":5}],"MessageHandlers":{"MsgType1":["Handler1","Handler2"]}}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt new file mode 100644 index 00000000000..cfe55f962c4 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithProjectedTypeName.approved.txt @@ -0,0 +1 @@ +{"WithType":{"Type":"NServiceBus.Core.Tests.OpenTelemetry.DiagnosticsWriterTests"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt new file mode 100644 index 00000000000..8f910cd1ac3 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/DiagnosticsWriterTests.ShouldWriteTypedSectionWithSystemType.approved.txt @@ -0,0 +1 @@ +{"WithType":{"TypeValue":"NServiceBus.Core.Tests.OpenTelemetry.DiagnosticsWriterTests"}} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt new file mode 100644 index 00000000000..fb3f7c59c76 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/LogManagerUsage.ApproveLogManagerUsage.approved.txt @@ -0,0 +1,49 @@ +The following types hold an NServiceBus.Logging.ILog logger field obtained via LogManager.GetLogger. +For new code where DI is available, use the high performance source generated loggers from +Microsoft.Extensions.Logging instead (see https://learn.microsoft.com/dotnet/core/extensions/logger-message-generator). +Changes that make this list longer should only be approved when the type affects user-facing +code or when DI is not available. The list should otherwise shrink over time. +----- +NServiceBus.AsyncFile +NServiceBus.AuditConfigReader +NServiceBus.AuditInvalidLicenseBehavior +NServiceBus.ConsecutiveFailuresCircuitBreaker +NServiceBus.Conventions +NServiceBus.DelayedMessagePoller +NServiceBus.DeserializeMessageConnector +NServiceBus.DirectoryBasedTransaction +NServiceBus.EnvelopeUnwrapper +NServiceBus.ErrorQueueSettings +NServiceBus.Features.Audit +NServiceBus.Features.AutoSubscribe+ApplySubscriptions +NServiceBus.Features.FeatureStartupTaskController`1 +NServiceBus.Features.LicenseReminder +NServiceBus.HostStartupDiagnosticsWriter +NServiceBus.HostStartupDiagnosticsWriterFactory +NServiceBus.HostingComponent +NServiceBus.LearningTransportMessagePump +NServiceBus.LicenseManager +NServiceBus.LoadHandlersConnector +NServiceBus.LogErrorOnInvalidLicenseBehavior +NServiceBus.MessageDrivenSubscribeTerminator +NServiceBus.MessageDrivenUnsubscribeTerminator +NServiceBus.MigrationSubscribeTerminator +NServiceBus.MigrationUnsubscribeTerminator +NServiceBus.PersistenceComponent +NServiceBus.PipelineModelBuilder +NServiceBus.Pipeline`1 +NServiceBus.ReceiveComponent +NServiceBus.RecoverabilityComponent +NServiceBus.RoutingToDispatchConnector +NServiceBus.RunningEndpointInstance +NServiceBus.SagaPersistenceBehavior +NServiceBus.SerializationFeature +NServiceBus.SerializationSettingsExtensions +NServiceBus.SerializeMessageConnector +NServiceBus.SubscriptionReceiverBehavior +NServiceBus.Unicast.Messages.MessageMetadataRegistry +NServiceBus.UnicastPublishRouter +NServiceBus.Validations +NServiceBus.WrappedMessageReceiver +NServiceBus.XmlDeserialization +NServiceBus.XmlSerializerCache diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt new file mode 100644 index 00000000000..ea20cafe71c --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullabilityWarnings.ApproveNullabilityWarnings.approved.txt @@ -0,0 +1,277 @@ +The following nullable warnings are present in NServiceBus.Core. +Changes that make this list longer should not be approved. +----- + +src/NServiceBus.Core/EndpointConfiguration.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'obj' in 'void Action.Invoke(TInitializationExtension obj)'. + +src/NServiceBus.Core/EndpointCreator.cs + CS8618: Non-nullable field 'envelopeComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'featureComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'hostingComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'pipelineComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'receiveComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'recoverabilityComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'sendComponent' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportSeam' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'MessageSession' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8619: Nullability of reference types in value of type 'string?[]' doesn't match target type 'string[]'. + +src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.Set(string key, object value)'. + +src/NServiceBus.Core/Licensing/LicenseReminder.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Pipeline/Incoming/LogicalMessageFactory.cs + CS8604: Possible null reference argument for parameter 'messageType' in 'MessageMetadata MessageMetadataRegistry.GetMessageMetadata(Type messageType)'. + +src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs + CS8604: Possible null reference argument for parameter 'headers' in 'OutgoingMessage.OutgoingMessage(string messageId, Dictionary headers, ReadOnlyMemory body)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'MulticastAddressTag.MulticastAddressTag(Type messageType)'. + CS8604: Possible null reference argument for parameter 'properties' in 'TransportOperation.TransportOperation(OutgoingMessage message, AddressTag addressTag, DispatchProperties properties = null, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default)'. + +src/NServiceBus.Core/Pipeline/Outgoing/SerializeMessageConnector.cs + CS8604: Possible null reference argument for parameter 'messageType' in 'MessageMetadata MessageMetadataRegistry.GetMessageMetadata(Type messageType)'. + +src/NServiceBus.Core/Routing/AutomaticSubscriptions/AutoSubscribe.cs + CS8602: Dereference of a possibly null reference. + +src/NServiceBus.Core/Routing/EndpointInstance.cs + CS8625: Cannot convert null literal to non-nullable reference type. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscribeTerminator.cs + CS8597: Thrown value may be null. + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageDrivenSubscribeTerminator.SendSubscribeMessageWithRetries(string destination, OutgoingMessage subscriptionMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespacePublisherSource.NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenUnsubscribeTerminator.cs + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageDrivenUnsubscribeTerminator.SendUnsubscribeMessageWithRetries(string destination, OutgoingMessage unsubscribeMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageType.cs + CS8601: Possible null reference assignment. + CS8618: Non-nullable property 'TypeName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Version' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/PublisherAddress.cs + CS8602: Dereference of a possibly null reference. + CS8618: Non-nullable field 'addresses' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'endpoint' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'instances' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8765: Nullability of type of parameter 'obj' doesn't match overridden member (possibly because of nullability attributes). + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/SubscriptionReceiverBehavior.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'endpoint' in 'Subscriber.Subscriber(string transportAddress, string endpoint)'. + +src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/SubscriptionRouter.cs + CS8600: Converting null literal or possible null value to non-nullable type. + +src/NServiceBus.Core/Routing/RoutingComponent.cs + CS8604: Possible null reference argument for parameter 'instanceSpecificQueue' in 'UnicastSendRouter.UnicastSendRouter(bool isSendOnly, string receiveQueueName, QueueAddress instanceSpecificQueue, IDistributionPolicy defaultDistributionPolicy, UnicastRoutingTable unicastRoutingTable, EndpointInstances endpointInstances, ITransportAddressResolver transportAddressResolver)'. + +src/NServiceBus.Core/Routing/RoutingSettings.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespaceRouteSource.NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, UnicastRoute route)'. + +src/NServiceBus.Core/Routing/SingleInstanceRoundRobinDistributionStrategy.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationSubscribeTerminator.cs + CS8597: Thrown value may be null. + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MigrationSubscribeTerminator.SendSubscribeMessageWithRetries(string destination, OutgoingMessage subscriptionMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationUnsubscribeTerminator.cs + CS8601: Possible null reference assignment. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MigrationUnsubscribeTerminator.SendUnsubscribeMessageWithRetries(string destination, OutgoingMessage unsubscribeMessage, string messageType, ContextBag context, int retriesCount, CancellationToken cancellationToken)'. + +src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8604: Possible null reference argument for parameter 'messageNamespace' in 'NamespacePublisherSource.NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address)'. + +src/NServiceBus.Core/Routing/UnicastPublishRouter.cs + CS8600: Converting null literal or possible null value to non-nullable type. + +src/NServiceBus.Core/Routing/UnicastRoute.cs + CS8618: Non-nullable property 'Endpoint' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Instance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'PhysicalAddress' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Routing/UnicastRoutingTable.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Routing/UnicastSendRouter.cs + CS8618: Non-nullable field 'instanceSpecificQueue' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'ExplicitDestination' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SpecificInstance' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Sagas/ActiveSagaInstance.cs + CS8604: Possible null reference argument for parameter 'InitialValue' in 'CorrelationPropertyInfo.CorrelationPropertyInfo(string Name, Type Type, object InitialValue, bool HasInitialValue)'. + CS8604: Possible null reference argument for parameter 'currentCorrelationPropertyValue' in 'void ActiveSagaInstance.ValidateCorrelationPropertyHaveValue(object currentCorrelationPropertyValue)'. + CS8604: Possible null reference argument for parameter 'currentCorrelationPropertyValue' in 'void ActiveSagaInstance.ValidateCorrelationPropertyNotModified(object currentCorrelationPropertyValue)'. + CS8618: Non-nullable field 'correlationProperty' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'SagaId' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Sagas/CustomFinderAdapter.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Sagas/SagaCorrelationProperty.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Sagas/SagaFinderDefinition.cs + CS8618: Non-nullable property 'MessageTypeName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Properties' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Sagas/SagaLookupValues.cs + CS8601: Possible null reference assignment. + +src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'messageType' in 'bool SagaMetadata.TryGetFinder(string messageType, out SagaFinderDefinition? finderDefinition)'. + CS8604: Possible null reference argument for parameter 'value' in 'SagaCorrelationProperty.SagaCorrelationProperty(string name, object value)'. + CS8619: Nullability of reference types in value of type 'Task' doesn't match target type 'Task'. + +src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs + CS8600: Converting null literal or possible null value to non-nullable type. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'fields' in 'FieldInfo XmlDeserialization.GetField(FieldInfo[] fields, string name)'. + CS8604: Possible null reference argument for parameter 'node' in 'object XmlDeserialization.Process(XmlNode node, object parent, Type nodeType = null)'. + CS8604: Possible null reference argument for parameter 'properties' in 'PropertyInfo XmlDeserialization.GetProperty(PropertyInfo[] properties, string name)'. + CS8618: Non-nullable field 'defaultNamespace' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'messageType' in 'XmlSerialization.XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = "http://tempuri.net")'. + CS8625: Cannot convert null literal to non-nullable reference type. + CS8767: Nullability of reference types in type of parameter 'messageTypesToDeserialize' of 'object[] XmlMessageSerializer.Deserialize(ReadOnlyMemory body, IList messageTypesToDeserialize = null)' doesn't match implicitly implemented member 'object[] IMessageSerializer.Deserialize(ReadOnlyMemory body, IList? messageTypes = null)' (possibly because of nullability attributes). + +src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs + CS8604: Possible null reference argument for parameter 'elem' in 'void XmlSerialization.WriteObject(XElement elem, string name, Type type, object value, bool useNS = false)'. + CS8604: Possible null reference argument for parameter 'item' in 'bool List.Contains(string item)'. + CS8604: Possible null reference argument for parameter 'value' in 'void XmlSerialization.WriteObject(XElement elem, string name, Type type, object value, bool useNS = false)'. + +src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs + CS8604: Possible null reference argument for parameter 'typeArguments' in 'Type Type.MakeGenericType(params Type[] typeArguments)'. + +src/NServiceBus.Core/Settings/SettingsHolder.cs + CS8601: Possible null reference assignment. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'key' in 'T SettingsHolder.GetOrDefault(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.HasExplicitValue(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.HasSetting(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'bool SettingsHolder.TryGet(string key, out T val)'. + CS8604: Possible null reference argument for parameter 'key' in 'object SettingsHolder.Get(string key)'. + CS8604: Possible null reference argument for parameter 'key' in 'void SettingsHolder.Set(string key, object value)'. + CS8604: Possible null reference argument for parameter 'key' in 'void SettingsHolder.SetDefault(string key, object value)'. + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.Set(string key, object value)'. + CS8604: Possible null reference argument for parameter 'value' in 'void SettingsHolder.SetDefault(string key, object value)'. + +src/NServiceBus.Core/StartableEndpoint.cs + CS8618: Non-nullable field 'stoppingTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportInfrastructure' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Transports/IncomingMessageExtensions.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Transports/Learning/DelayedMessagePoller.cs + CS8618: Non-nullable field 'polling' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Transports/Learning/DirectoryBasedTransaction.cs + CS8618: Non-nullable property 'FileToProcess' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/Learning/HeaderSerializer.cs + CS8603: Possible null reference return. + +src/NServiceBus.Core/Transports/Learning/LearningTransport.cs + CS8618: Non-nullable property 'StorageDirectory' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/Learning/LearningTransportInfrastructure.cs + CS8604: Possible null reference argument for parameter 'subscriptionManager' in 'LearningTransportMessagePump.LearningTransportMessagePump(string id, string receiveAddress, string basePath, Action criticalErrorAction, ISubscriptionManager subscriptionManager, ReceiveSettings receiveSettings, TransportTransactionMode transactionMode)'. + +src/NServiceBus.Core/Transports/Learning/LearningTransportMessagePump.cs + CS8618: Non-nullable field 'bodyDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'committedTransactionDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'concurrencyLimiter' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'delayedDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'delayedMessagePoller' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messageProcessingCancellationTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpBasePath' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpCancellationTokenSource' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'messagePumpTask' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'onError' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'onMessage' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'pendingTransactionDir' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/Learning/LearningTransportSubscriptionManager.cs + CS8604: Possible null reference argument for parameter 'path2' in 'string Path.Combine(string path1, string path2)'. + +src/NServiceBus.Core/Transports/Learning/NoTransaction.cs + CS8618: Non-nullable property 'FileToProcess' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/QueueAddress.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/TransportInfrastructure.cs + CS8618: Non-nullable property 'Dispatcher' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Receivers' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/Transports/TransportOperation.cs + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Transports/TransportSeam.cs + CS8618: Non-nullable field 'receiverSettings' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'transportInfrastructure' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Unicast/MessageOperations.cs + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.PublishMessage(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. + CS8604: Possible null reference argument for parameter 'message' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.PublishMessage(IBehaviorContext context, Type messageType, object message, PublishOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options)'. + CS8604: Possible null reference argument for parameter 'messageType' in 'Task MessageOperations.SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options)'. + +src/NServiceBus.Core/Unicast/Messages/MessageMetadata.cs + CS8604: Possible null reference argument for parameter 'item' in 'bool ICollection.Contains(string item)'. + CS8625: Cannot convert null literal to non-nullable reference type. + +src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs + CS8601: Possible null reference assignment. + CS8603: Possible null reference return. + CS8604: Possible null reference argument for parameter 'key' in 'bool ConcurrentDictionary.TryAdd(string key, Type value)'. + CS8618: Non-nullable field 'isMessageType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + +src/NServiceBus.Core/Unicast/Queuing/QueueNotFoundException.cs + CS8618: Non-nullable property 'Queue' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/VersionInformation.cs + CS8601: Possible null reference assignment. + CS8618: Non-nullable property 'MajorMinorPatch' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + +src/NServiceBus.Core/obsoletes-v10.cs + CS8618: Non-nullable field 'isMessageType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable field 'sagaFinders' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable. + CS8618: Non-nullable property 'AssociatedMessages' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'EntityName' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Loader' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'NotFoundHandler' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaDataFactory' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaEntityType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. + CS8618: Non-nullable property 'SagaType' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable. diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt index 9116a220ddd..fec9fb40c88 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableAnnotations.ApproveNullableTypes.approved.txt @@ -9,7 +9,6 @@ NServiceBus.ContentTypes NServiceBus.DateTimeOffsetHelper NServiceBus.DistributionPolicy NServiceBus.EndpointConfiguration -NServiceBus.Faults.FailedMessage NServiceBus.Headers NServiceBus.IDistributionPolicy NServiceBus.IMessageHandlerContext @@ -22,12 +21,6 @@ NServiceBus.MessageDrivenSubscriptionsConfigExtensions NServiceBus.MessageProcessingContextExtensions NServiceBus.MessageSessionExtensions NServiceBus.OnSatelliteMessage -NServiceBus.Outbox.IOutboxStorage -NServiceBus.Outbox.OutboxMessage -NServiceBus.Outbox.TransportOperation -NServiceBus.OutboxConfigExtensions -NServiceBus.Persistence.CompletableSynchronizedStorageSessionExtensions -NServiceBus.Persistence.ICompletableSynchronizedStorageSession NServiceBus.Pipeline.ISubscribeContext NServiceBus.Pipeline.IUnsubscribeContext NServiceBus.ReceiveSettingsExtensions @@ -56,18 +49,9 @@ NServiceBus.Sagas.SagaCorrelationProperty NServiceBus.Sagas.SagaFinderDefinition NServiceBus.Sagas.SagaIdGeneratorContext NServiceBus.SagasConfigExtensions -NServiceBus.Serialization.IMessageSerializer -NServiceBus.Serialization.SerializationDefinition -NServiceBus.Serialization.SerializationExtensions`1 -NServiceBus.SerializationConfigExtensions -NServiceBus.SerializationContextExtensions -NServiceBus.SerializationExtensionsExtensions NServiceBus.Settings.IReadOnlySettings NServiceBus.Settings.SettingsHolder -NServiceBus.StaticHeadersConfigExtensions NServiceBus.SubscriptionMigrationModeSettings -NServiceBus.Support.RuntimeEnvironment -NServiceBus.Transport.DispatchProperties NServiceBus.Transport.ErrorContext NServiceBus.Transport.IMessageDispatcher NServiceBus.Transport.IMessageReceiver @@ -99,6 +83,5 @@ NServiceBus.Unicast.Queuing.QueueNotFoundException NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions.ISubscriptionStorage NServiceBus.Unicast.Subscriptions.MessageDrivenSubscriptions.Subscriber NServiceBus.Unicast.Subscriptions.MessageType -NServiceBus.UnitOfWorkSettingsExtensions NServiceBus.XmlSerializationExtensions NServiceBus.XmlSerializer diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt new file mode 100644 index 00000000000..a01570c7de4 --- /dev/null +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/NullableEnable.CompletedFolders.approved.txt @@ -0,0 +1,69 @@ +The following directories have every .cs file annotated with #nullable enable. +Changes that remove a directory from this list, or add a directory whose files are not +all annotated, should not be approved. New files added to a listed directory must also +be annotated with #nullable enable. +----- +NServiceBus.AcceptanceTests/Core/LoggingIntegration +NServiceBus.Core.Analyzer/Features +NServiceBus.Core.Tests/Receiving +NServiceBus.Core.Tests/Utils +NServiceBus.Core/Audit +NServiceBus.Core/Causation +NServiceBus.Core/CircuitBreakers +NServiceBus.Core/Conventions +NServiceBus.Core/Correlation +NServiceBus.Core/CriticalError +NServiceBus.Core/DelayedDelivery +NServiceBus.Core/Envelopes +NServiceBus.Core/Extensibility +NServiceBus.Core/Features +NServiceBus.Core/Hosting +NServiceBus.Core/Hosting/Helpers +NServiceBus.Core/Hosting/KeyedServices +NServiceBus.Core/Hosting/StartupDiagnostics +NServiceBus.Core/IdGeneration +NServiceBus.Core/Installation +NServiceBus.Core/Licensing +NServiceBus.Core/Logging +NServiceBus.Core/MessageInterfaces +NServiceBus.Core/MessageInterfaces/MessageMapper +NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection +NServiceBus.Core/MessageMutators +NServiceBus.Core/MessageMutators/MutateInstanceMessage +NServiceBus.Core/MessageMutators/MutateTransportMessage +NServiceBus.Core/Notifications +NServiceBus.Core/OpenTelemetry +NServiceBus.Core/OpenTelemetry/Metrics +NServiceBus.Core/OpenTelemetry/Tracing +NServiceBus.Core/Performance/MessageProcessingOptimizations +NServiceBus.Core/Performance/Statistics +NServiceBus.Core/Performance/TimeToBeReceived +NServiceBus.Core/Persistence +NServiceBus.Core/Persistence/Learning +NServiceBus.Core/Persistence/Learning/SagaPersister +NServiceBus.Core/Pipeline +NServiceBus.Core/Pipeline/Incoming +NServiceBus.Core/Pipeline/Outgoing +NServiceBus.Core/Receiving +NServiceBus.Core/Recoverability +NServiceBus.Core/Recoverability/DelayedRetries +NServiceBus.Core/Recoverability/ImmediateRetries +NServiceBus.Core/Recoverability/Settings +NServiceBus.Core/Serializers/SystemJson +NServiceBus.Core/Support +NServiceBus.Core/Unicast/Config +NServiceBus.AcceptanceTests/Core/UnitOfWork/TransactionScope +NServiceBus.Core.Tests/Reliability/Outbox +NServiceBus.Core.Tests/ServicePlatform/Retries +NServiceBus.Core.Tests/Utils/Reflection +NServiceBus.Core/Recoverability/Faults +NServiceBus.Core/Reliability/Outbox +NServiceBus.Core/Reliability/SynchronizedStorage +NServiceBus.Core/Serialization +NServiceBus.Core/ServicePlatform/Retries +NServiceBus.Core/StaticHeaders +NServiceBus.Core/UnitOfWork +NServiceBus.Core/UnitOfWork/TransactionScopes +NServiceBus.Core/Utils +NServiceBus.Core/Utils/Reflection +NServiceBus.Core/Utils/Reflection \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt index 631d90e5f42..5d4598a9783 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/StructConventionsTests.ApproveStructsWhichDontFollowStructGuidelines.approved.txt @@ -1,4 +1,4 @@ --------------------------------------------------- REMEMBER -------------------------------------------------- +-------------------------------------------------- REMEMBER -------------------------------------------------- CONSIDER defining a struct instead of a class if instances of the type are small and commonly short-lived or are commonly embedded in other objects. AVOID defining a struct unless the type has all of the following characteristics: @@ -10,3 +10,30 @@ AVOID defining a struct unless the type has all of the following characteristics In all other cases, you should define your types as classes. -------------------------------------------------- REMEMBER -------------------------------------------------- +NServiceBus.Extensibility.ContextBag+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.Object is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.Object is a reference type. + - The size cannot be determined because there are fields that are reference types. + +NServiceBus.Transport.DispatchProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + +NServiceBus.Transport.ReceiveProperties+Slot violates the following rules: + - The following fields are public, so the type is not immutable: + - Field Key of type System.String is public. + - Field Value of type System.String is public. + - The following fields are reference types, which are potentially mutable: + - Field Key of type System.String is a reference type. + - Field Value of type System.String is a reference type. + - The size cannot be determined because there are fields that are reference types. + diff --git a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs index 56ea6f238ab..6c408de4871 100644 --- a/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs +++ b/src/NServiceBus.Core.Tests/AssemblyScanner/AssemblyScanningComponentTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.AssemblyScanner; +#nullable enable + +namespace NServiceBus.Core.Tests.AssemblyScanner; using System; using System.IO; @@ -57,6 +59,88 @@ public void Should_allow_assembly_scanning_to_be_disabled() Assert.That(component.AvailableTypes, Is.Empty); } + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_dynamic_code_not_supported() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + DynamicCodeSupported = false, + AssemblyScannerConfiguration = { Disable = true } + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_feature_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + StrictRegisteredOnlyMessageMetadataEnabled = true, + AssemblyScannerConfiguration = { Disable = true } + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + + [Test] + public void Should_enable_strict_registered_only_mode_when_scanning_disabled_and_runtime_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + using (AppContextSwitchHelper.Enable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) { AssemblyScannerConfiguration = { Disable = true } }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.True); + } + } + + [Test] + public void Should_not_enable_strict_registered_only_mode_when_scanning_disabled_in_normal_jit() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) { AssemblyScannerConfiguration = { Disable = true } }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + Assert.That(component.IsStrictRegisteredOnlyMode, Is.False); + } + + [Test] + public void Should_not_enable_strict_registered_only_mode_when_scanning_enabled_and_feature_switch_enabled() + { + var settingsHolder = new SettingsHolder(); + settingsHolder.Set(new HostingComponent.Settings(settingsHolder)); + + var configuration = new AssemblyScanningComponent.Configuration(settingsHolder) + { + StrictRegisteredOnlyMessageMetadataEnabled = true, + DynamicCodeSupported = true + }; + + var component = AssemblyScanningComponent.Initialize(configuration, settingsHolder); + + // Strict mode is only ever enabled when assembly scanning is disabled. With scanning enabled the + // component must not enable it even when the feature switch is enabled. + Assert.That(component.IsStrictRegisteredOnlyMode, Is.False); + } + [Test] public void Should_throw_enabled_and_dynamic_code_not_supported() { @@ -69,4 +153,24 @@ public void Should_throw_enabled_and_dynamic_code_not_supported() Assert.That(exception?.Message, Does.Contain("Assembly scanning requires to access unreferenced and dynamic code")); } + + sealed class AppContextSwitchHelper : IDisposable + { + readonly string switchName; + + public static AppContextSwitchHelper Enable(string switchName) => new(switchName, true); + + AppContextSwitchHelper(string switchName, bool value) + { + this.switchName = switchName; + AppContext.SetSwitch(switchName, value); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + + public void Dispose() + { + AppContext.SetSwitch(switchName, false); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/ContextBagTests.cs b/src/NServiceBus.Core.Tests/ContextBagTests.cs index d41b7b67413..73072b3b201 100644 --- a/src/NServiceBus.Core.Tests/ContextBagTests.cs +++ b/src/NServiceBus.Core.Tests/ContextBagTests.cs @@ -1,9 +1,10 @@ -#nullable enable +#nullable enable namespace NServiceBus.Core.Tests; using Extensibility; using NUnit.Framework; +using System.Collections.Generic; [TestFixture] public class ContextBagTests @@ -62,4 +63,504 @@ public void SetOnRoot_should_set_value_on_root_context() Assert.That(fork.Get(key), Is.EqualTo(42), "stored value should be visible to a forked context"); } } + + [Test] + public void Should_set_and_get_a_single_inline_entry() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + } + + [Test] + public void Should_set_and_get_four_inline_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 4; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 4; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_set_and_get_eight_inline_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_set_and_get_more_than_eight_entries() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 12; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 12; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_update_an_existing_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "original"); + bag.Set("key1", "updated"); + + Assert.That(bag.Get("key1"), Is.EqualTo("updated")); + } + + [Test] + public void Should_update_an_existing_stash_key() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Set("key9", "updated"); + + Assert.That(bag.Get("key9"), Is.EqualTo("updated")); + } + + [Test] + public void Should_remove_the_first_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key1"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_remove_a_middle_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key2"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key2"), Throws.TypeOf()); + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_remove_the_last_inline_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key3"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key3"), Throws.TypeOf()); + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + } + } + + [Test] + public void Should_remove_a_stash_key() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Remove("key9"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key9"), Throws.TypeOf()); + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_keep_remaining_inline_keys_after_swap_removal() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + bag.Set("key4", "value4"); + + bag.Remove("key1"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(bag.Get("key2"), Is.EqualTo("value2")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + + bag.Remove("key2"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(() => bag.Get("key1"), Throws.TypeOf()); + Assert.That(() => bag.Get("key2"), Throws.TypeOf()); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + } + + [Test] + public void Should_clear_the_bag() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 12; i++) + { + bag.Set($"key{i}", $"value{i}"); + } + + bag.Clear(); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 12; i++) + { + Assert.That(() => bag.Get($"key{i}"), Throws.TypeOf()); + } + } + } + + [Test] + public void Should_throw_when_getting_a_missing_key() + { + var bag = new ContextBag(); + + Assert.That(() => bag.Get("nonexistent"), Throws.TypeOf()); + } + + [Test] + public void Should_return_false_when_try_getting_a_missing_key() + { + var bag = new ContextBag(); + + var found = bag.TryGet("nonexistent", out var result); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.False); + Assert.That(result, Is.Null); + } + } + + [Test] + public void Should_find_a_key_in_the_parent_bag() + { + var root = new ContextBag(); + var child = new ContextBag(root); + + root.Set("parentKey", "parentValue"); + + Assert.That(child.Get("parentKey"), Is.EqualTo("parentValue")); + } + + [Test] + public void Should_fallback_to_parent_after_removing_local_override() + { + var root = new ContextBag(); + var child = new ContextBag(root); + + root.Set("sharedKey", "rootValue"); + child.Set("sharedKey", "childValue"); + + child.Remove("sharedKey"); + + Assert.That(child.Get("sharedKey"), Is.EqualTo("rootValue")); + } + + [Test] + public void Should_allow_removing_a_nonexistent_key() + { + var bag = new ContextBag(); + + Assert.That(() => bag.Remove("nonexistent"), Throws.Nothing); + } + + [Test] + public void Should_keep_inline_entries_reachable_after_overflowing_to_stash() + { + var bag = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + bag.Set($"key{i}", $"inline{i}"); + } + + bag.Set("key9", "stashValue"); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(bag.Get($"key{i}"), Is.EqualTo($"inline{i}")); + } + + Assert.That(bag.Get("key9"), Is.EqualTo("stashValue")); + } + } + + [Test] + public void Should_reuse_inline_slots_after_removing_a_key() + { + var bag = new ContextBag(); + + bag.Set("key1", "value1"); + bag.Set("key2", "value2"); + bag.Set("key3", "value3"); + + bag.Remove("key2"); + + bag.Set("key4", "value4"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(bag.Get("key1"), Is.EqualTo("value1")); + Assert.That(bag.Get("key3"), Is.EqualTo("value3")); + Assert.That(bag.Get("key4"), Is.EqualTo("value4")); + } + } + + [Test] + public void Should_merge_inline_entries_into_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + source.Set("key1", "value1"); + source.Set("key2", "value2"); + source.Set("key3", "value3"); + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + Assert.That(target.Get("key2"), Is.EqualTo("value2")); + Assert.That(target.Get("key3"), Is.EqualTo("value3")); + } + } + + [Test] + public void Should_merge_inline_entries_into_non_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("existing", "existingValue"); + source.Set("key1", "value1"); + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("existing"), Is.EqualTo("existingValue")); + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + } + } + + [Test] + public void Should_merge_overwrites_existing_key() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("sharedKey", "originalValue"); + source.Set("sharedKey", "newValue"); + + target.Merge(source); + + Assert.That(target.Get("sharedKey"), Is.EqualTo("newValue")); + } + + [Test] + public void Should_merge_stash_entries_into_empty_target() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_stash_entries_into_target_with_full_inline() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 8; i++) + { + target.Set($"existing{i}", $"existingValue{i}"); + } + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 8; i++) + { + Assert.That(target.Get($"existing{i}"), Is.EqualTo($"existingValue{i}")); + } + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_stash_entries_fill_remaining_inline_slots() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("existing", "existingValue"); + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + Assert.That(target.Get("existing"), Is.EqualTo("existingValue")); + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_when_both_have_stash_entries() + { + var target = new ContextBag(); + var source = new ContextBag(); + + for (var i = 1; i <= 9; i++) + { + target.Set($"existing{i}", $"existingValue{i}"); + } + + for (var i = 1; i <= 9; i++) + { + source.Set($"key{i}", $"value{i}"); + } + + target.Merge(source); + + using (Assert.EnterMultipleScope()) + { + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"existing{i}"), Is.EqualTo($"existingValue{i}")); + } + + for (var i = 1; i <= 9; i++) + { + Assert.That(target.Get($"key{i}"), Is.EqualTo($"value{i}")); + } + } + } + + [Test] + public void Should_merge_from_empty_source() + { + var target = new ContextBag(); + var source = new ContextBag(); + + target.Set("key1", "value1"); + + target.Merge(source); + + Assert.That(target.Get("key1"), Is.EqualTo("value1")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs b/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs new file mode 100644 index 00000000000..7aeabfdf592 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Hosting/AppContextSwitchesTests.cs @@ -0,0 +1,56 @@ +namespace NServiceBus.Core.Tests.Host; + +using System; +using NUnit.Framework; + +[TestFixture] +public class AppContextSwitchesTests +{ + [Test] + public void Strict_registered_only_message_metadata_defaults_to_false_when_switch_unset() + { + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.False); + } + + [Test] + public void Strict_registered_only_message_metadata_is_enabled_when_switch_is_set() + { + using (AppContextSwitchHelper.Enable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.True); + } + } + + [Test] + public void Strict_registered_only_message_metadata_is_disabled_when_switch_is_set_to_false() + { + using (AppContextSwitchHelper.Disable(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName)) + { + Assert.That(AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled, Is.False); + } + } + + sealed class AppContextSwitchHelper : IDisposable + { + readonly string switchName; + + public static AppContextSwitchHelper Enable(string switchName) => new(switchName, true); + + public static AppContextSwitchHelper Disable(string switchName) => new(switchName, false); + + AppContextSwitchHelper(string switchName, bool value) + { + this.switchName = switchName; + AppContext.SetSwitch(switchName, value); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + + public void Dispose() + { + AppContext.SetSwitch(switchName, false); + AppContextSwitches.ResetStrictRegisteredOnlyMessageMetadata(); + } + } +} diff --git a/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs b/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs index 213df348fa5..9100f2c84b3 100644 --- a/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/HostInfoSettingsTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Host; +#nullable enable + +namespace NServiceBus.Core.Tests.Host; using System; using NServiceBus.Support; diff --git a/src/NServiceBus.Core.Tests/Hosting/HostTests.cs b/src/NServiceBus.Core.Tests/Hosting/HostTests.cs index 05e591cd6f4..a607aa52ccb 100644 --- a/src/NServiceBus.Core.Tests/Hosting/HostTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/HostTests.cs @@ -8,7 +8,7 @@ public class HostTests { [Test] - public void When_not_running_ASP_NET_should_choose_BaseDirectory_as_output_directory() + public void Should_choose_BaseDirectory_as_output_directory() { var directory = Host.GetOutputDirectory(); diff --git a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs index 6d5f7952488..19c63815ba4 100644 --- a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceCollectionAdapterTests.cs @@ -9,8 +9,6 @@ namespace NServiceBus.Core.Tests.Host; using Microsoft.Extensions.DependencyInjection.Extensions; using NUnit.Framework; -#pragma warning disable IDE0028 // Collection initialization can be simplified — test clarity requires explicit Add calls - [TestFixture] public class KeyedServiceCollectionAdapterTests { @@ -413,20 +411,6 @@ public void Remove_should_throw_on_null() Assert.Throws(() => adapter.Remove(null!)); } - [Test] - public void Remove_should_decrement_service_type_count() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var descriptor = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton); - adapter.Add(descriptor); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - adapter.Remove(descriptor); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void RemoveAt_should_remove_descriptor_at_index() { @@ -449,28 +433,15 @@ public void RemoveAt_should_remove_descriptor_at_index() Assert.That(inner[0].ServiceType, Is.EqualTo(typeof(IBar))); } - [Test] - public void RemoveAt_should_decrement_service_type_count() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var d1 = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient); - adapter.Add(d1); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - adapter.RemoveAt(0); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void Clear_should_remove_all_from_adapter_and_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped)); + var adapter = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient), + new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped) + }; adapter.Clear(); @@ -481,18 +452,6 @@ public void Clear_should_remove_all_from_adapter_and_inner() } } - [Test] - public void Clear_should_reset_service_type_counts() - { - var adapter = CreateAdapter("ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Clear(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - } - [Test] public void CopyTo_should_copy_original_descriptors() { @@ -515,78 +474,79 @@ public void CopyTo_should_copy_original_descriptors() } [Test] - public void ContainsService_should_return_true_for_registered_type() + public void ContainsLocalService_should_distinguish_keyed_and_non_keyed_registrations() { var adapter = CreateAdapter(); adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo2), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.ContainsLocalService(typeof(IFoo), null), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "other"), Is.False); + } } [Test] - public void ContainsService_should_return_false_for_unregistered_type() + public void ContainsLocalService_should_match_closed_generic_request_for_open_generic_registration() { var adapter = CreateAdapter(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); + adapter.Add(new ServiceDescriptor(typeof(IGeneric<>), "sub", typeof(GenericFoo<>), ServiceLifetime.Transient)); + + Assert.That(adapter.ContainsLocalService(typeof(IGeneric), "sub"), Is.True); } [Test] - public void ContainsService_should_return_true_for_open_generic_when_closed_registered() + public void ContainsLocalService_should_return_false_after_remove() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IGeneric<>), typeof(GenericFoo<>), ServiceLifetime.Transient)); + var descriptor = new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Transient); + adapter.Add(descriptor); + + adapter.Remove(descriptor); - Assert.That(adapter.ContainsService(typeof(IGeneric)), Is.True); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); } [Test] - public void ContainsService_should_return_false_for_closed_when_only_other_closed_registered() + public void ContainsLocalService_should_return_false_after_remove_at() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IGeneric), typeof(GenericFoo), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Transient)); - Assert.That(adapter.ContainsService(typeof(IGeneric)), Is.False); - } + adapter.RemoveAt(0); - [Test] - public void ContainsService_should_throw_on_null() - { - var adapter = CreateAdapter(); - Assert.Throws(() => adapter.ContainsService(null!)); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); } [Test] - public void Multiple_same_type_should_increment_count() + public void ContainsLocalService_should_return_false_after_clear() { var adapter = CreateAdapter(); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient)); + adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo2), ServiceLifetime.Transient)); + + adapter.Clear(); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.ContainsLocalService(typeof(IFoo), null), Is.False); + Assert.That(adapter.ContainsLocalService(typeof(IFoo), "sub"), Is.False); + } } [Test] - public void Removing_all_of_same_type_should_remove_service_type() + public void GetLocalServiceKey_should_project_to_endpoint_composite_key() { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - var d1 = new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient); - var d2 = new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient); - adapter.Add(d1); - adapter.Add(d2); - - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + var adapter = CreateAdapter("ep"); - adapter.Remove(d1); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); + var key = adapter.GetLocalServiceKey("sub"); - adapter.Remove(d2); using (Assert.EnterMultipleScope()) { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - Assert.That(inner, Is.Empty); + Assert.That(key.BaseKey, Is.EqualTo("ep")); + Assert.That(key.ServiceKey, Is.EqualTo("sub")); } } @@ -594,9 +554,11 @@ public void Removing_all_of_same_type_should_remove_service_type() public void Add_should_augment_non_keyed_descriptor_to_keyed_in_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton)); + _ = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton) + }; Assert.That(inner[0].ServiceKey, Is.InstanceOf()); var key = (KeyedServiceKey)inner[0].ServiceKey!; @@ -611,9 +573,11 @@ public void Add_should_augment_non_keyed_descriptor_to_keyed_in_inner() public void Add_should_augment_keyed_descriptor_with_composite_key_in_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - adapter.Add(new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Singleton)); + _ = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), "sub", typeof(Foo), ServiceLifetime.Singleton) + }; Assert.That(inner[0].ServiceKey, Is.InstanceOf()); var key = (KeyedServiceKey)inner[0].ServiceKey!; @@ -667,10 +631,11 @@ public void Remove_should_sync_with_inner_collection() public void Clear_should_remove_all_from_inner() { var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton)); - adapter.Add(new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped)); + var adapter = new KeyedServiceCollectionAdapter(inner, "ep") + { + new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton), + new ServiceDescriptor(typeof(IBar), typeof(Bar), ServiceLifetime.Scoped) + }; adapter.Clear(); @@ -823,36 +788,6 @@ public void Add_instance_descriptor_should_keep_instance_in_original() Assert.That(adapter[0].ImplementationInstance, Is.SameAs(instance)); } - [Test] - public void Service_type_counts_should_track_multiple_registrations() - { - var inner = new ServiceCollection(); - var adapter = new KeyedServiceCollectionAdapter(inner, "ep"); - - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo2), ServiceLifetime.Transient)); - adapter.Add(new ServiceDescriptor(typeof(IFoo), typeof(Foo3), ServiceLifetime.Transient)); - - using (Assert.EnterMultipleScope()) - { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - Assert.That(inner, Has.Count.EqualTo(3)); - } - - adapter.Remove(adapter[0]); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Remove(adapter[0]); - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.True); - - adapter.Remove(adapter[0]); - using (Assert.EnterMultipleScope()) - { - Assert.That(adapter.ContainsService(typeof(IFoo)), Is.False); - Assert.That(inner, Is.Empty); - } - } - interface IFoo; interface IBar; @@ -865,8 +800,6 @@ class Foo : IFoo; class Foo2 : IFoo; - class Foo3 : IFoo; - class Bar : IBar; class Baz : IBaz; diff --git a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs index dbde1062a24..db850530c36 100644 --- a/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs +++ b/src/NServiceBus.Core.Tests/Hosting/KeyedServiceProviderAdapterTests.cs @@ -2,6 +2,9 @@ namespace NServiceBus.Core.Tests.Host; +using System; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -36,4 +39,369 @@ public async Task Async_dispose_should_not_dispose_underlying_service_provider() var scopeFactory = adapter.GetRequiredService(); Assert.DoesNotThrow(() => scopeFactory.CreateScope().Dispose()); } + + [Test] + public void GetRequiredService_should_resolve_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredService(typeof(IComponent)); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetService_should_resolve_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetService(typeof(IComponent)); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void IsKeyedService_should_return_true_for_local_keyed_service() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_return_true_for_root_keyed_service() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_use_root_keyed_service_probe() + { + var providerServices = new ServiceCollection(); + providerServices.AddKeyedSingleton("component"); + var adapterServices = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(adapterServices, "endpoint"); + using var rootProvider = providerServices.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.True); + } + + [Test] + public void IsKeyedService_should_return_false_when_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), "component"); + + Assert.That(result, Is.False); + } + + [Test] + public void IsKeyedService_should_return_false_for_local_service_with_different_endpoint_key() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsKeyedService(typeof(IComponent), new KeyedServiceKey("other-endpoint", "component")); + + Assert.That(result, Is.False); + } + + [Test] + public void IsService_should_return_true_for_local_endpoint_service() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_true_for_root_service_registered_with_endpoint_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_true_for_root_unkeyed_service() + { + var services = new ServiceCollection(); + services.AddSingleton(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_use_root_service_probe() + { + var providerServices = new ServiceCollection(); + providerServices.AddSingleton(); + var adapterServices = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(adapterServices, "endpoint"); + using var rootProvider = providerServices.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.True); + } + + [Test] + public void IsService_should_return_false_when_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var result = adapter.IsService(typeof(IComponent)); + + Assert.That(result, Is.False); + } + + [Test] + public void IsService_should_return_true_for_adapter_services() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.IsService(typeof(IServiceProvider)), Is.True); + Assert.That(adapter.IsService(typeof(ISupportRequiredService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceProviderIsService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceProviderIsKeyedService)), Is.True); + Assert.That(adapter.IsService(typeof(IServiceScopeFactory)), Is.True); + Assert.That(adapter.IsService(typeof(IEnumerable)), Is.True); + } + } + + [Test] + public void GetService_and_GetRequiredService_should_return_adapter_services() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + using (Assert.EnterMultipleScope()) + { + Assert.That(adapter.GetService(typeof(IServiceProvider)), Is.SameAs(adapter)); + Assert.That(adapter.GetRequiredService(typeof(ISupportRequiredService)), Is.SameAs(adapter)); + Assert.That(adapter.GetService(typeof(IServiceProviderIsService)), Is.SameAs(adapter)); + Assert.That(adapter.GetRequiredService(typeof(IServiceProviderIsKeyedService)), Is.SameAs(adapter)); + Assert.That(adapter.GetService(typeof(IServiceScopeFactory)), Is.Not.Null); + Assert.That(adapter.GetRequiredService(typeof(IServiceScopeFactory)), Is.Not.Null); + } + } + + [Test] + public void GetRequiredKeyedService_should_prefer_local_keyed_service_for_matching_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetKeyedService_should_prefer_local_keyed_service_for_matching_key() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetRequiredKeyedService_should_fallback_to_root_keyed_service_when_local_key_does_not_match() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("root-component"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("local-component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetRequiredKeyedService(typeof(IComponent), "root-component"); + + Assert.That(component, Is.InstanceOf()); + } + + [Test] + public void GetKeyedService_should_return_null_when_keyed_service_is_not_available() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var component = adapter.GetKeyedService(typeof(IComponent), "component"); + + Assert.That(component, Is.Null); + } + + [Test] + public void GetService_and_GetRequiredService_should_resolve_local_endpoint_enumerables() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + keyedServices.AddSingleton(); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetService(typeof(IEnumerable))!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredService(typeof(IEnumerable))).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetService_and_GetRequiredService_should_resolve_root_endpoint_keyed_enumerables() + { + var services = new ServiceCollection(); + services.AddKeyedSingleton("endpoint"); + services.AddKeyedSingleton("endpoint"); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetService(typeof(IEnumerable))!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredService(typeof(IEnumerable))).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetKeyedService_and_GetRequiredKeyedService_should_resolve_local_keyed_enumerables() + { + var services = new ServiceCollection(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddKeyedSingleton("component"); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetKeyedService(typeof(IEnumerable), "component")!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredKeyedService(typeof(IEnumerable), "component")).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Has.Count.EqualTo(2)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + [Test] + public void GetKeyedService_and_GetRequiredKeyedService_should_resolve_all_services_for_any_key() + { + var services = new ServiceCollection(); + services.AddSingleton(); + var keyedServices = new KeyedServiceCollectionAdapter(services, "endpoint"); + keyedServices.AddSingleton(); + keyedServices.AddKeyedSingleton("component"); + using var rootProvider = services.BuildServiceProvider(); + var adapter = new KeyedServiceProviderAdapter(rootProvider, "endpoint", keyedServices); + + var components = ((IEnumerable)adapter.GetKeyedService(typeof(IEnumerable), KeyedServiceKey.Any)!).ToList(); + var requiredComponents = ((IEnumerable)adapter.GetRequiredKeyedService(typeof(IEnumerable), KeyedServiceKey.Any)).ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(components, Has.Count.EqualTo(3)); + Assert.That(requiredComponents, Has.Count.EqualTo(3)); + Assert.That(requiredComponents, Is.EquivalentTo(components)); + } + } + + interface IComponent; + + class LocalComponent : IComponent; + + class LocalKeyedComponent : IComponent; + + class SecondLocalKeyedComponent : IComponent; + + class RootEndpointComponent : IComponent; + + class SecondRootEndpointComponent : IComponent; + + class RootComponent : IComponent; + + class RootKeyedComponent : IComponent; } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs b/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs index b71eaedfc92..0dcb3a4ce78 100644 --- a/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs +++ b/src/NServiceBus.Core.Tests/Licensing/LicenseManagerTests.cs @@ -35,17 +35,17 @@ public void WhenSubscriptionLicenseExpired() } [Test] - public void WhenUpgradeProtectionExpiredForThisRelease() + public void WhenUpgradeProtectionNoLongerBeingSupported() { var logger = new TestableLogger(); - LicenseManager.LogLicenseStatus(LicenseStatus.InvalidDueToExpiredUpgradeProtection, logger, new License(), "fake-url"); + LicenseManager.LogLicenseStatus(LicenseStatus.InvalidDueToUpgradeProtectionNoLongerBeingSupported, logger, new License(), "fake-url"); Assert.That(logger.Logs, Has.Count.EqualTo(1)); using (Assert.EnterMultipleScope()) { Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Error)); - Assert.That(logger.Logs[0].message, Is.EqualTo("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); + Assert.That(logger.Logs[0].message, Is.EqualTo("Licenses with ugprade protection are no longer supported. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); } } @@ -116,51 +116,6 @@ public void WhenSubscriptionAboutToExpire(int daysRemaining, string expectedMess } } - [TestCase(3, "Upgrade protection expiring in 3 days. Contact us to renew your license: contact@particular.net")] - [TestCase(1, "Upgrade protection expiring in 1 day. Contact us to renew your license: contact@particular.net")] - [TestCase(0, "Upgrade protection expiring today. Contact us to renew your license: contact@particular.net")] - public void WhenUpgradeProtectionAboutToExpire(int daysRemaining, string expectedMessage) - { - var logger = new TestableLogger(); - var today = new DateTime(2012, 12, 12); - var license = new License - { - utcDateTimeProvider = () => today, - UpgradeProtectionExpiration = today.AddDays(daysRemaining) - }; - - LicenseManager.LogLicenseStatus(LicenseStatus.ValidWithExpiringUpgradeProtection, logger, license, "fake-url"); - - Assert.That(logger.Logs, Has.Count.EqualTo(1)); - using (Assert.EnterMultipleScope()) - { - Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Warn)); - Assert.That(logger.Logs[0].message, Is.EqualTo(expectedMessage)); - } - } - - [Test] - public void WhenUpgradeProtectionExpiredForFutureVersions() - { - var logger = new TestableLogger(); - var today = new DateTime(2012, 12, 12); - var license = new License - { - utcDateTimeProvider = () => today, - releaseDateProvider = () => today.AddDays(-20), - UpgradeProtectionExpiration = today.AddDays(-10) - }; - - LicenseManager.LogLicenseStatus(LicenseStatus.ValidWithExpiredUpgradeProtection, logger, license, "fake-url"); - - Assert.That(logger.Logs, Has.Count.EqualTo(1)); - using (Assert.EnterMultipleScope()) - { - Assert.That(logger.Logs[0].level, Is.EqualTo(LogLevel.Warn)); - Assert.That(logger.Logs[0].message, Is.EqualTo("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net")); - } - } - class TestableLogger : ILog { public bool IsDebugEnabled => true; diff --git a/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs b/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs index 11b1d04f188..bc411c18680 100644 --- a/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs +++ b/src/NServiceBus.Core.Tests/Logging/EndpointLoggingScopeTests.cs @@ -33,6 +33,26 @@ public void Should_include_endpoint_name_and_identifier_for_multi_hosted_endpoin new KeyValuePair("EndpointIdentifier", "blue")); } + [Test] + public void Should_preserve_structured_state_when_logging_in_slot_scope() + { + var loggerFactory = new FakeLoggerLoggerFactory(); + var slot = new EndpointLogSlot("Sales", "blue"); + LogManager.RegisterSlotFactory(slot, new MicrosoftLoggerFactoryAdapter(loggerFactory)); + + var logger = LogManager.GetLogger($"{nameof(EndpointLoggingScopeTests)}-{Guid.NewGuid():N}"); + + using (LogManager.BeginSlotScope(slot)) + { + logger.InfoFormat("message {0}", "value"); + } + + Assert.That(loggerFactory.CapturedStructuredState, Has.Some.Matches>?>(state => + state is not null && + state.Any(kv => kv is { Key: "{OriginalFormat}", Value: "message {0}" }) && + state.Any(kv => kv is { Key: "0", Value: "value" }))); + } + [Test] public void Should_include_only_endpoint_name_when_identifier_is_not_provided() { @@ -314,6 +334,8 @@ sealed class FakeLoggerLoggerFactory : Microsoft.Extensions.Logging.ILoggerFacto public List CapturedMessages => [.. provider.Collector.GetSnapshot().Select(r => r.Message)]; + public List>?> CapturedStructuredState => [.. provider.Collector.GetSnapshot().Select(r => r.StructuredState)]; + public List>> CapturedLogScopes => [ .. provider.Collector.GetSnapshot() diff --git a/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs b/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs index 7f201b6a152..f121efd64c8 100644 --- a/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs +++ b/src/NServiceBus.Core.Tests/Logging/RollingLoggerTests.cs @@ -96,6 +96,108 @@ protected override void AppendLine(string message) } } + [Test] + public void When_file_is_deleted_underneath_immediately_before_size_lookup() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatDeletesBeforeSizeLookup(tempPath.TempDirectory, maxFileSize: 2) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + + // Second write exceeds maxFileSize, so the synchronization enumerates today's file; + // the override removes it between enumeration and the metadata read + logger.WriteLine("Bar"); + + // The name proves the sequence number was reused (vanished file counts as empty) + // instead of rolling to a new sequence + var file = tempPath.GetSingle(); + Assert.That(Path.GetFileName(file), Is.EqualTo("nsb_log_2010-10-01_0.txt")); + } + + class RollingLoggerThatDeletesBeforeSizeLookup(string targetDirectory, long maxFileSize) : + RollingLogger(targetDirectory, maxFileSize: maxFileSize) + { + protected override long GetFileSizeOrZero(string path) + { + File.Delete(path); + return base.GetFileSizeOrZero(path); + } + } + + [Test] + public void When_size_lookup_fails_with_io_error_sync_is_aborted_and_retried() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatFailsSizeLookup(tempPath.TempDirectory, maxFileSize: 10) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + logger.WriteLine("Bar"); + logger.WriteLine("Baz"); + + // The metadata read fails with a transient I/O error, which must abort the synchronization + // instead of treating the file as empty and reusing it + logger.FailSizeLookup = true; + logger.WriteLine("Qux"); + logger.FailSizeLookup = false; + + // The next write retries the synchronization and rolls to a new sequence + logger.WriteLine("Foo2"); + + var files = tempPath.GetFiles(); + Assert.That(files, Has.Count.EqualTo(2)); + Assert.That(Path.GetFileName(files[1]), Is.EqualTo("nsb_log_2010-10-01_1.txt")); + } + + [Test] + public void When_sync_fails_during_filename_calculation_state_is_not_committed_and_next_write_retries() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLoggerThatFailsSizeLookup(tempPath.TempDirectory, maxFileSize: 20) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("LongMessage"); + logger.WriteLine("LongMessage"); + + logger.GetDate = () => new DateTimeOffset(2010, 10, 2, 0, 0, 0, TimeSpan.Zero); + logger.WriteLine("Baz"); + + // The clock moves back to a date that already has an oversized file while the stale current file + // remains below the size limit. The metadata read fails, so the synchronization must abort without + // committing the new date; otherwise the next write would stay on the stale file and not retry. + logger.GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero); + logger.FailSizeLookup = true; + logger.WriteLine("Qux"); + logger.FailSizeLookup = false; + + // The next write retries the synchronization and rolls the oversized day one file + logger.WriteLine("Foo2"); + + var files = tempPath.GetFiles(); + Assert.That(files, Has.Count.EqualTo(3)); + Assert.That(Path.GetFileName(files[1]), Is.EqualTo("nsb_log_2010-10-01_1.txt")); + } + + class RollingLoggerThatFailsSizeLookup(string targetDirectory, long maxFileSize) : + RollingLogger(targetDirectory, maxFileSize: maxFileSize) + { + public bool FailSizeLookup { get; set; } + + protected override long GetFileSizeOrZero(string path) + { + if (FailSizeLookup) + { + // An overlong path component makes the metadata read throw a PathTooLongException + return base.GetFileSizeOrZero(Path.Combine(Path.GetDirectoryName(path)!, new string('x', 300))); + } + return base.GetFileSizeOrZero(path); + } + } + [Test] public void When_file_already_exists_and_is_too_large_a_new_sequence_file_is_written() { @@ -396,4 +498,29 @@ public void When_line_is_write_file_has_correct_name() var singleFile = tempPath.GetSingle(); Assert.That(Path.GetFileName(singleFile), Is.EqualTo("nsb_log_2010-10-01_0.txt")); } + + [Test] + public void When_log_directory_is_deleted_underneath_write_does_not_throw_and_recovers() + { + using var tempPath = new TempPath("RollingLoggerTests"); + var logger = new RollingLogger(tempPath.TempDirectory) + { + GetDate = () => new DateTimeOffset(2010, 10, 1, 0, 0, 0, TimeSpan.Zero) + }; + logger.WriteLine("Foo"); + + Directory.Delete(tempPath.TempDirectory, true); + logger.GetDate = () => new DateTimeOffset(2010, 10, 2, 0, 0, 0, TimeSpan.Zero); + + // Simulates e.g. a deployment or slot swap removing the whole log directory: the synchronization + // and the write must degrade to tracing instead of throwing out of WriteLine + Assert.That(() => logger.WriteLine("Bar"), Throws.Nothing); + + Directory.CreateDirectory(tempPath.TempDirectory); + logger.WriteLine("Baz"); + + // GetSingle also verifies the day one file is not resurrected after the directory came back + var singleFile = tempPath.GetSingle(); + Assert.That(Path.GetFileName(singleFile), Is.EqualTo("nsb_log_2010-10-02_0.txt")); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs b/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs new file mode 100644 index 00000000000..000127a2484 --- /dev/null +++ b/src/NServiceBus.Core.Tests/MessageMapper/When_mapping_with_trimming_safe_mapper.cs @@ -0,0 +1,125 @@ +#nullable enable + +namespace MessageMapperTests; + +using System; +using NServiceBus; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NUnit.Framework; + +[TestFixture] +public class When_mapping_with_trimming_safe_mapper +{ + [Test] + public void Initialize_is_a_noop_and_does_not_throw() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.DoesNotThrow(() => mapper.Initialize(null)); + Assert.DoesNotThrow(() => mapper.Initialize([])); + Assert.DoesNotThrow(() => mapper.Initialize([typeof(IInterfaceWithOnlyProperties)])); + } + + [Test] + public void GetMappedTypeFor_returns_the_same_concrete_type() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.That(mapper.GetMappedTypeFor(typeof(ConcreteMessage)), Is.SameAs(typeof(ConcreteMessage))); + } + + [Test] + public void GetMappedTypeFor_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + // Failing here keeps the deserialization error actionable instead of letting the serializer fail opaquely + // later on an interface it cannot target. + var ex = Assert.Throws(() => mapper.GetMappedTypeFor(typeof(IInterfaceWithOnlyProperties))); + + Assert.That(ex!.Message, Does.Contain("not supported").And.Contain("dynamic code")); + } + + [Test] + public void GetMappedTypeFor_by_name_returns_null() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.That(mapper.GetMappedTypeFor(typeof(ConcreteMessage).FullName!), Is.Null); + } + + [Test] + public void CreateInstance_for_concrete_type_returns_an_instance() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(typeof(ConcreteMessage)); + + Assert.That(instance, Is.InstanceOf()); + } + + [Test] + public void CreateInstance_generic_for_concrete_type_returns_an_instance() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(); + + Assert.That(instance, Is.InstanceOf()); + } + + [Test] + public void CreateInstance_generic_with_action_applies_the_action() + { + var mapper = new TrimmingSafeMessageMapper(); + + var instance = mapper.CreateInstance(m => m.SomeProperty = "hello"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(instance, Is.InstanceOf()); + Assert.That(instance.SomeProperty, Is.EqualTo("hello")); + } + } + + [Test] + public void CreateInstance_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + var ex = Assert.Throws(() => mapper.CreateInstance(typeof(IInterfaceWithOnlyProperties))); + + Assert.That(ex!.Message, Does.Contain("not supported").And.Contain("dynamic code")); + } + + [Test] + public void CreateInstance_generic_for_interface_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.Throws(() => mapper.CreateInstance()); + } + + [Test] + public void CreateInstance_for_abstract_type_throws_not_supported() + { + var mapper = new TrimmingSafeMessageMapper(); + + Assert.Throws(() => mapper.CreateInstance(typeof(AbstractMessage))); + } + + public interface IInterfaceWithOnlyProperties : IMessage + { + string SomeProperty { get; set; } + } + + public class ConcreteMessage : IMessage + { + public string? SomeProperty { get; set; } + } + + public abstract class AbstractMessage + { + public string? SomeProperty { get; set; } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index 0fa1ce103f1..17a60efea43 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -126,15 +127,87 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + + [Test] + public async Task When_typed_replacement_is_followed_by_object_setter_should_use_the_object_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + Assert.That(context.UpdateMessageObjCalled, Is.True); + } + } + class InterceptUpdateMessageIncomingLogicalMessageContext : TestableIncomingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessageInstance(object newInstance) { base.UpdateMessageInstance(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessageInstance(object newInstance, Type messageType) + { + base.UpdateMessageInstance(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -162,7 +235,19 @@ class MutatorWhichMutatesTheBody : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new object(); +#pragma warning restore CS0618 + + return Task.CompletedTask; + } + } + + class MutatorWhichDeclaresAMessageType : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); return Task.CompletedTask; } @@ -178,4 +263,10 @@ public Task MutateIncoming(MutateIncomingMessageContext context) class TestMessage : IMessage { } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs index b16ed6426c1..5466f006912 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -122,15 +123,87 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + + [Test] + public async Task When_typed_replacement_is_followed_by_object_setter_should_use_the_object_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + Assert.That(context.UpdateMessageObjCalled, Is.True); + } + } + class InterceptUpdateMessageOutgoingLogicalMessageContext : TestableOutgoingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessage(object newInstance) { base.UpdateMessage(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessage(object newInstance, Type messageType) + { + base.UpdateMessage(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -166,9 +239,27 @@ class MutatorWhichMutatesTheBody : IMutateOutgoingMessages { public Task MutateOutgoing(MutateOutgoingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new object(); +#pragma warning restore CS0618 return Task.CompletedTask; } } + + class MutatorWhichDeclaresAMessageType : IMutateOutgoingMessages + { + public Task MutateOutgoing(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyMessage()); + + return Task.CompletedTask; + } + } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs new file mode 100644 index 00000000000..47317c70202 --- /dev/null +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs @@ -0,0 +1,94 @@ +namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; + +using System; +using System.Threading.Tasks; +using MessageMutator; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.MessageInterfaces; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Testing; +using Unicast.Messages; + +[TestFixture] +public class When_incoming_mutator_replaces_message_instance +{ + [Test] + public async Task Should_recompute_metadata_for_the_replacement_instance_type() + { + var context = CreateContext(new ReplaceWithReplacementMessageMutator()); + var behavior = new MutateIncomingMessageBehavior([]); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + } + } + + [Test] + public async Task Should_keep_original_metadata_when_instance_is_not_replaced() + { + var context = CreateContext(new DoNothingMutator()); + var behavior = new MutateIncomingMessageBehavior([]); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); + } + } + + static IncomingLogicalMessageContext CreateContext(IMutateIncomingMessages mutator) + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); + + var services = new ServiceCollection(); + services.AddSingleton(registry); + services.AddSingleton(); + services.AddSingleton(new TrimmingSafeMessageMapper()); + services.AddSingleton(mutator); + IServiceProvider provider = services.BuildServiceProvider(); + + var parentContext = new TestableIncomingPhysicalMessageContext(); + parentContext.Extensions.Set(provider); + + var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(typeof(OriginalMessage)), new OriginalMessage()); + + return new IncomingLogicalMessageContext(logicalMessage, parentContext); + } + + class ReplaceWithReplacementMessageMutator : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal + context.Message = new ReplacementMessage(); +#pragma warning restore CS0618 + return Task.CompletedTask; + } + } + + class DoNothingMutator : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) => Task.CompletedTask; + } + + public class OriginalMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class ReplacementMessage : IMessage + { + public string SomeProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index 9d3ef87ddac..a8a2aa6f64b 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -12,13 +12,13 @@ - - - + + + - - + + diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs index a8880919f67..c4825b33064 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/ActivityExtensionsTests.cs @@ -16,7 +16,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_key_not_fou var contextBag = new ContextBag(); using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.False); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.False); Assert.That(activity, Is.Null); } } @@ -31,7 +31,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_value_null( using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.False); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.False); Assert.That(activity, Is.Null); } } @@ -50,7 +50,7 @@ public void TryGetRecordingPipelineActivity_should_return_false_when_not_recordi using (Assert.EnterMultipleScope()) { - Assert.That(contextBag.TryGetRecordingOutgoingPipelineActivity(out var activity), Is.True); + Assert.That(contextBag.TryGetOutgoingPipelineActivity(out var activity), Is.True); Assert.That(activity, Is.EqualTo(recordingActivity)); } } diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs index 217a90555cf..dd4fa9805d8 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/DiagnosticsWriterTests.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Core.Tests.OpenTelemetry; using System; +using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; @@ -32,71 +33,48 @@ public void LoggerSetup() [Test] public async Task ShouldWriteWhenDuplicateEntriesPresent() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(false); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointTwo" }); diagnostics.Add("Version", new { Version = "1.0.0.0" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, false); - await writer.Write(diagnostics.entries); - Approver.Verify(output); + Approver.Verify(output()); } [Test] public async Task ShouldWriteEntriesWithTypesUsingTheFullName() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(false); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("TypeIndicator", new { SomeType = typeof(DiagnosticsWriterTests) }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, false); - await writer.Write(diagnostics.entries); - Approver.Verify(output); + Approver.Verify(output()); } [Test] public async Task ShouldSupportWritingToLogAndWriter() { - var output = string.Empty; - var testWriter = new Func((diagnosticOutput, _) => - { - output = diagnosticOutput; - return Task.CompletedTask; - }); + var (writer, output) = CreateCaptureWriter(true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); - Approver.Verify(output + Environment.NewLine + logStatements, s => TimestampScrubber().Replace(s, "")); + Approver.Verify(output() + Environment.NewLine + logStatements, s => TimestampScrubber().Replace(s, "")); } [Test] public async Task ShouldSupportWritingToLogEvenWhenWriterIsNoOp() { - var testWriter = new Func((_, _) => Task.CompletedTask); + var writer = new HostStartupDiagnosticsWriter(NoOpWriter, true, true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); Approver.Verify(logStatements.ToString(), s => TimestampScrubber().Replace(s, "")); @@ -105,17 +83,292 @@ public async Task ShouldSupportWritingToLogEvenWhenWriterIsNoOp() [Test] public async Task ShouldSupportWritingToLogEvenWhenWriterFails() { - var testWriter = new Func((_, _) => Task.FromException(new InvalidOperationException("Test"))); + var writer = new HostStartupDiagnosticsWriter(FailingWriter, true, true); var diagnostics = new StartupDiagnosticEntries(); diagnostics.Add("Endpoint", new { EndpointName = "MyEndpointOne" }); - var writer = new HostStartupDiagnosticsWriter(testWriter, true, true); - await writer.Write(diagnostics.entries); Approver.Verify(logStatements.ToString(), inputToScrub => StackTraceScrubber.ScrubFileInfoFromStackTrace(TimestampScrubber().Replace(inputToScrub, ""))); } + [Test] + public async Task ShouldWriteAlphabeticalSectionOrder() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Zulu", new { Value = "z" }); + diagnostics.Add("Mike", new { Value = "m" }); + diagnostics.Add("Alpha", new { Value = "a" }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteEscapedPropertyNames() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Special", new Dictionary + { + { "Property With Spaces", true }, + { "Normal", 42 } + }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteNullAndNestedValues() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("NullValue", new { Value = default(object) }); + diagnostics.Add("Nested", new { Inner = new { Deep = "value" } }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldInvokeLazySectionOnce() + { + var invocationCount = 0; + var (writer, _) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Lazy", new Func(() => + { + invocationCount++; + return new { Value = "lazy" }; + })); + + await writer.Write(diagnostics.entries); + + Assert.That(invocationCount, Is.EqualTo(1)); + } + + [Test] + public async Task ShouldCompactAssemblyScanningOnlyInLog() + { + var (writer, output) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("AssemblyScanning", new AssemblyScanningDiagnostics( + [new AssemblyDetails("MyAssembly", "1.0.0.0")], + [], + false, + new AssemblyScannerConfiguration()), + StartupDiagnosticsJsonContext.Default.AssemblyScanningDiagnostics); + + await writer.Write(diagnostics.entries); + + var logOutput = logStatements.ToString(); + Assert.That(logOutput, Does.Contain("\"Assemblies\":[]"), "Log output should have compacted assemblies"); + Assert.That(output(), Does.Contain("MyAssembly"), "Custom writer output should have full assemblies"); + } + + [Test] + public async Task ShouldTruncateLogAtThreshold() + { + var writer = new HostStartupDiagnosticsWriter(NoOpWriter, true, true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Large", new { Data = new string('X', 40000) }); + + await writer.Write(diagnostics.entries); + + var logOutput = logStatements.ToString(); + Assert.That(logOutput, Does.Contain("... (truncated)")); + } + + [Test] + public async Task ShouldWriteTypedSection() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Typed", new EndpointDiagnostics + { + Name = "MyEndpoint", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldInvokeTypedFactoryOnce() + { + var invocationCount = 0; + var (writer, output) = CreateCaptureWriter(true); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.AddFactory("Factory", () => + { + invocationCount++; + return new EndpointDiagnostics + { + Name = "FromFactory", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }; + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Assert.That(invocationCount, Is.EqualTo(1)); + Assert.That(output(), Does.Contain("FromFactory")); + } + + [Test] + public async Task ShouldWriteMixedTypedAndLegacySections() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Legacy", new { Value = "old" }); + diagnostics.Add("Typed", new EndpointDiagnostics + { + Name = "MyEndpoint", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedDuplicateEntries() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Section", new EndpointDiagnostics + { + Name = "First", + SendOnly = false, + NServiceBusVersion = "1.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + diagnostics.Add("Section", new EndpointDiagnostics + { + Name = "Second", + SendOnly = false, + NServiceBusVersion = "2.0.0" + }, StartupDiagnosticsJsonContext.Default.EndpointDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteDuplicateEntriesWithSynthesizedNameCollision() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("Section", new { Value = "First" }); + diagnostics.Add("Section-2", new { Value = "AlreadySynthesized" }); + diagnostics.Add("Section", new { Value = "Third" }); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithProjectedTypeName() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("WithType", new ContainerDiagnostics + { + Type = typeof(DiagnosticsWriterTests).FullName! + }, StartupDiagnosticsJsonContext.Default.ContainerDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithSystemType() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("WithType", new TestTypeDiagnosticsDto + { + TypeValue = typeof(DiagnosticsWriterTests) + }, TestDiagnosticsJsonContext.Default.TestTypeDiagnosticsDto); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + [Test] + public async Task ShouldWriteTypedSectionWithNestedCollections() + { + var (writer, output) = CreateCaptureWriter(false); + var diagnostics = new StartupDiagnosticEntries(); + diagnostics.Add("NestedCollections", new ReceivingDiagnostics + { + LocalQueueAddress = new QueueAddressDiagnostics + { + BaseAddress = "myqueue", + Discriminator = null, + Properties = [], + Qualifier = null + }, + InstanceSpecificQueueAddress = null, + PurgeOnStartup = false, + TransactionMode = "TransactionScope", + MaxConcurrency = 10, + Satellites = + [ + new SatelliteDiagnostics + { + Name = "Sat1", + ReceiveAddress = new QueueAddressDiagnostics + { + BaseAddress = "satqueue", + Discriminator = null, + Properties = [], + Qualifier = null + }, + MaxConcurrency = 5 + } + ], + MessageHandlers = new Dictionary> + { + { "MsgType1", ["Handler1", "Handler2"] } + } + }, StartupDiagnosticsJsonContext.Default.ReceivingDiagnostics); + + await writer.Write(diagnostics.entries); + + Approver.Verify(output()); + } + + static (HostStartupDiagnosticsWriter Writer, Func GetOutput) CreateCaptureWriter(bool writeToLog) + { + var output = string.Empty; + var testWriter = new Func((diagnosticOutput, _) => + { + output = diagnosticOutput; + return Task.CompletedTask; + }); + return (new HostStartupDiagnosticsWriter(testWriter, true, writeToLog), () => output); + } + + static Task NoOpWriter(string _, CancellationToken cancellationToken) => Task.CompletedTask; + + static Task FailingWriter(string _, CancellationToken cancellationToken) => + Task.FromException(new InvalidOperationException("Test")); + [GeneratedRegex(@"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}", RegexOptions.Compiled)] private static partial Regex TimestampScrubber(); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs index cb8e2d5823b..ec5d0c067ea 100644 --- a/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/PopulateRecoverabilityTraceMetadataBehaviorTests.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Core.Tests.OpenTelemetry; using System; diff --git a/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs b/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs new file mode 100644 index 00000000000..4baf0c84aa7 --- /dev/null +++ b/src/NServiceBus.Core.Tests/OpenTelemetry/TestDiagnosticsJsonContext.cs @@ -0,0 +1,25 @@ +namespace NServiceBus.Core.Tests.OpenTelemetry; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +[JsonSerializable(typeof(TestTypeDiagnosticsDto))] +sealed partial class TestDiagnosticsJsonContext : JsonSerializerContext +{ +} + +sealed class TestTypeDiagnosticsDto +{ + [JsonConverter(typeof(TestFullNameTypeConverter))] + public required Type TypeValue { get; init; } +} + +sealed class TestFullNameTypeConverter : JsonConverter +{ + public override Type Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + throw new NotSupportedException(); + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) => + writer.WriteStringValue(value.FullName); +} diff --git a/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs new file mode 100644 index 00000000000..59e968fd5f1 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs @@ -0,0 +1,109 @@ +namespace NServiceBus.Core.Tests.Pipeline.Incoming; + +using System; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Testing; +using Unicast.Messages; + +[TestFixture] +public class IncomingLogicalMessageContextTests +{ + [Test] + public void Updating_the_message_to_a_new_type_should_update_the_MessageType() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + var differentMessage = new MyDifferentMessage(); + context.UpdateMessageInstance(differentMessage); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(differentMessage)); + } + } + + [Test] + public void Updating_the_existing_instance_with_a_different_explicit_type_should_use_that_type() + { + var message = new MySubMessage(); + var context = CreateContext(typeof(MySubMessage), message); + + context.UpdateMessageInstance(message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Updating_the_existing_instance_with_the_same_type_should_preserve_the_metadata() + { + var message = new MyDifferentMessage(); + var context = CreateContext(typeof(MyDifferentMessage), message); + + var metadataBefore = context.Message.Metadata; + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.Metadata, Is.SameAs(metadataBefore)); + } + + [Test] + public void Updating_with_an_explicit_type_that_is_not_assignable_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), typeof(string))); + } + + [Test] + public void Updating_with_a_null_instance_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(MyDifferentMessage))); + } + + [Test] + public void Updating_with_a_null_message_type_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), null!)); + } + + static IncomingLogicalMessageContext CreateContext(Type messageType, object instance = null) + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyDifferentMessage), typeof(MySubMessage)]); + var services = new ServiceCollection(); + services.AddSingleton(registry); + services.AddSingleton(); + services.AddSingleton(new TrimmingSafeMessageMapper()); + IServiceProvider provider = services.BuildServiceProvider(); + + var parentContext = new TestableIncomingPhysicalMessageContext(); + parentContext.Extensions.Set(provider); + + instance ??= new MyDifferentMessage(); + + var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(messageType), instance); + var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); + + return context; + } + + class MyDifferentMessage : IMessage + { } + + class MySubMessage : MyDifferentMessage + { } +} diff --git a/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs b/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs new file mode 100644 index 00000000000..ae2034a7826 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Pipeline/LogicalMessageFactoryTests.cs @@ -0,0 +1,135 @@ +namespace NServiceBus.Core.Tests.Pipeline; + +using System; +using System.Collections.Generic; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class LogicalMessageFactoryTests +{ + MessageMetadataRegistry registry; + MessageMapper mapper; + LogicalMessageFactory factory; + + [SetUp] + public void SetUp() + { + registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + mapper = new MessageMapper(); + factory = new LogicalMessageFactory(registry, mapper); + } + + [Test] + public void Create_with_object_overload_uses_runtime_type_for_metadata() + { + var message = new ConcreteMessage(); + + var logicalMessage = factory.Create(message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + Assert.That(logicalMessage.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Create_with_explicit_concrete_type_uses_that_type_for_metadata() + { + var message = new ConcreteMessage(); + + var logicalMessage = factory.Create(typeof(ConcreteMessage), message); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + } + + [Test] + public void Create_normalizes_proxy_instances_to_the_interface_type() + { + mapper.Initialize([typeof(IMyMessage)]); + var proxy = mapper.CreateInstance(); + + var logicalMessage = factory.Create(proxy.GetType(), proxy); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Create_with_interface_type_resolves_metadata_for_the_generated_proxy_type() + { + mapper.Initialize([typeof(IMyMessage)]); + var proxy = mapper.CreateInstance(); + + var logicalMessage = factory.Create(typeof(IMyMessage), proxy); + + Assert.That(logicalMessage.MessageType, Is.EqualTo(proxy.GetType())); + } + + [Test] + public void Create_throws_not_supported_when_mapper_cannot_map_interface_type() + { + var trimmingSafeFactory = new LogicalMessageFactory(registry, new TrimmingSafeMessageMapper()); + + Assert.Throws(() => trimmingSafeFactory.Create(typeof(IMyMessage), new ConcreteMessage())); + } + + [Test] + public void Create_with_metadata_overload_uses_given_metadata_without_invoking_mapper() + { + var throwingMapper = new ThrowingMessageMapper(); + var throwingFactory = new LogicalMessageFactory(registry, throwingMapper); + var metadata = registry.GetMessageMetadata(typeof(ConcreteMessage)); + var message = new ConcreteMessage(); + + var logicalMessage = throwingFactory.Create(metadata, message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(logicalMessage.MessageType, Is.EqualTo(typeof(ConcreteMessage))); + Assert.That(logicalMessage.Metadata, Is.SameAs(metadata)); + Assert.That(logicalMessage.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Create_with_metadata_overload_throws_when_metadata_is_null() + { + Assert.Throws(() => factory.Create((MessageMetadata)null, new ConcreteMessage())); + } + + [Test] + public void Create_throws_when_type_has_no_metadata_and_is_not_a_message_type() + { + Assert.Throws(() => factory.Create(typeof(string), "not a message")); + } + + public interface IMyMessage : IMessage + { + string SomeProperty { get; set; } + } + + public class ConcreteMessage : IMessage + { + public string SomeProperty { get; set; } + } + + class ThrowingMessageMapper : IMessageMapper + { + public void Initialize(IEnumerable types) => throw new InvalidOperationException("Should not be called"); + + public Type GetMappedTypeFor(Type t) => throw new InvalidOperationException("Should not be called"); + + public Type GetMappedTypeFor(string typeName) => throw new InvalidOperationException("Should not be called"); + + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => throw new InvalidOperationException("Should not be called"); + + public T CreateInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) => throw new InvalidOperationException("Should not be called"); + + public object CreateInstance([System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) => throw new InvalidOperationException("Should not be called"); + } +} diff --git a/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs index 14120d2b2f3..73f2a97c18b 100644 --- a/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs +++ b/src/NServiceBus.Core.Tests/Pipeline/Outgoing/OutgoingLogicalMessageContextTests.cs @@ -53,7 +53,29 @@ public void Updating_the_message_to_a_new_type_should_update_the_MessageType() Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); } - class MyDifferentMessage + [Test] + public void Updating_the_message_with_an_explicit_type_should_use_that_type() + { + var context = new OutgoingLogicalMessageContext("message1234", [], new OutgoingLogicalMessage(typeof(MyDifferentMessage), new MyDifferentMessage()), null, new FakeRootContext()); + var replacement = new MyDifferentMessage(); + + context.UpdateMessage(replacement); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Updating_the_existing_instance_with_an_explicit_type_should_use_that_type() + { + var message = new MyDifferentMessage(); + var context = new OutgoingLogicalMessageContext("message1234", [], new OutgoingLogicalMessage(typeof(MyDifferentMessage), message), null, new FakeRootContext()); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + class MyDifferentMessage : IMyMessage { public Guid Id { get; set; } } diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs index 50da4363934..10f06128c92 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/FakeOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Reliability.Outbox; +#nullable enable + +namespace NServiceBus.Core.Tests.Reliability.Outbox; using System.Threading; using System.Threading.Tasks; @@ -8,16 +10,17 @@ class FakeOutboxStorage : IOutboxStorage { - public OutboxMessage ExistingMessage { get; set; } - public OutboxMessage StoredMessage { get; set; } + public OutboxMessage? ExistingMessage { get; set; } + + public OutboxMessage? StoredMessage { get; set; } public bool WasDispatched { get; set; } - public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) + public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) { - if (ExistingMessage != null && ExistingMessage.MessageId == messageId) + if (ExistingMessage is not null && ExistingMessage.MessageId == messageId) { - return Task.FromResult(ExistingMessage); + return Task.FromResult(ExistingMessage); } return Task.FromResult(default(OutboxMessage)); diff --git a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs index ca48d520767..1f5eae8ffc0 100644 --- a/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs +++ b/src/NServiceBus.Core.Tests/Reliability/Outbox/TransportReceiveToPhysicalMessageConnectorTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.Reliability.Outbox; +#nullable enable + +namespace NServiceBus.Core.Tests.Reliability.Outbox; using System; using System.Collections.Generic; @@ -7,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using AcceptanceTests.Core.OpenTelemetry.Metrics; +using Microsoft.Extensions.Logging.Abstractions; using NServiceBus.Outbox; using NServiceBus.Pipeline; using NServiceBus.Routing; @@ -42,7 +45,7 @@ public async Task Should_honor_stored_delivery_constraints() await Invoke(context); - var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations.First().Properties); + var operationProperties = new DispatchProperties(fakeBatchPipeline.TransportOperations?.First().Properties ?? []); var delayDeliveryWith = operationProperties.DelayDeliveryWith; Assert.That(delayDeliveryWith, Is.Not.Null); Assert.That(delayDeliveryWith.Delay, Is.EqualTo(TimeSpan.FromSeconds(10))); @@ -77,7 +80,7 @@ public async Task Should_honor_stored_direct_routing() await Invoke(context); - var routing = fakeBatchPipeline.TransportOperations.First().AddressTag as UnicastAddressTag; + var routing = fakeBatchPipeline.TransportOperations?.First().AddressTag as UnicastAddressTag; Assert.That(routing, Is.Not.Null); using (Assert.EnterMultipleScope()) { @@ -93,20 +96,18 @@ public async Task Should_honor_stored_pubsub_routing() var messageId = "id"; var properties = new DispatchProperties { - ["EventType"] = typeof(MyEvent).AssemblyQualifiedName + ["EventType"] = typeof(MyEvent).AssemblyQualifiedName! }; - - fakeOutbox.ExistingMessage = new OutboxMessage(messageId, new[] - { + fakeOutbox.ExistingMessage = new OutboxMessage(messageId, [ new NServiceBus.Outbox.TransportOperation("x", properties, Array.Empty(), []) - }); + ]); var context = CreateContext(fakeBatchPipeline, messageId); await Invoke(context); - var routing = fakeBatchPipeline.TransportOperations.First().AddressTag as MulticastAddressTag; + var routing = fakeBatchPipeline.TransportOperations?.First().AddressTag as MulticastAddressTag; Assert.That(routing, Is.Not.Null); using (Assert.EnterMultipleScope()) { @@ -189,6 +190,63 @@ public async Task Should_not_add_batch_dispatch_events_when_no_batched_messages( } } + [Test] + public async Task Should_store_outbox_message_when_outbox_is_enabled() + { + var context = CreateContext(fakeBatchPipeline, "id"); + + await Invoke(context, c => + { + c.Extensions.Get().Add( + new TransportOperation(new OutgoingMessage("out-1", [], Array.Empty()), new UnicastAddressTag("destination"))); + return Task.CompletedTask; + }); + + Assert.That(fakeOutbox.StoredMessage, Is.Not.Null); + var stored = fakeOutbox.StoredMessage!; + using (Assert.EnterMultipleScope()) + { + Assert.That(stored.MessageId, Is.EqualTo("id")); + Assert.That(stored.TransportOperations, Has.Length.EqualTo(1)); + Assert.That(stored.TransportOperations[0].Options!["Destination"], Is.EqualTo("destination")); + } + } + + [Test] + public async Task Should_still_dispatch_when_outbox_is_disabled() + { + var noOpBehavior = new TransportReceiveToPhysicalMessageConnector( + new NoOpOutboxStorage(), new PipelineMetrics(fakeMeterFactory, "queue", "disc", new MetersOptions()), new InstrumentationOptions(), NullLogger.Instance); + + var context = CreateContext(fakeBatchPipeline, "id"); + + await noOpBehavior.Invoke(context, c => + { + c.Extensions.Get().AddRange([ + new TransportOperation(new OutgoingMessage("out-1", [], Array.Empty()), new UnicastAddressTag("destination")), + new TransportOperation(new OutgoingMessage("out-2", [], Array.Empty()), new MulticastAddressTag(typeof(MyEvent))) + ]); + return Task.CompletedTask; + }); + + Assert.That(fakeBatchPipeline.TransportOperations, Is.Not.Null); + var dispatched = fakeBatchPipeline.TransportOperations!.ToArray(); + Assert.That(dispatched, Has.Length.EqualTo(2)); + + var unicast = dispatched.Single(o => o.Message.MessageId == "out-1"); + var multicast = dispatched.Single(o => o.Message.MessageId == "out-2"); + using (Assert.EnterMultipleScope()) + { + Assert.That(((UnicastAddressTag)unicast.AddressTag).Destination, Is.EqualTo("destination")); + Assert.That(((MulticastAddressTag)multicast.AddressTag).MessageType, Is.EqualTo(typeof(MyEvent))); + + // With no outbox there is nothing to round-trip the address tag through, so the operations are + // dispatched without the routing strategy being serialized into their dispatch properties. + Assert.That(unicast.Properties.ContainsKey("Destination"), Is.False); + Assert.That(multicast.Properties.ContainsKey("EventType"), Is.False); + } + } + static TestableTransportReceiveContext CreateContext(FakeBatchPipeline pipeline, string messageId) { var context = new TestableTransportReceiveContext @@ -209,13 +267,13 @@ public void SetUp() fakeBatchPipeline = new FakeBatchPipeline(); fakeMeterFactory = new TestMeterFactory(); - behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new PipelineMetrics(new TestMeterFactory(), "queue", "disc", new MetersOptions()), new InstrumentationOptions()); + behavior = new TransportReceiveToPhysicalMessageConnector(fakeOutbox, new PipelineMetrics(fakeMeterFactory, "queue", "disc", new MetersOptions()), new InstrumentationOptions(), NullLogger.Instance); } [TearDown] public void TearDown() => fakeMeterFactory.Dispose(); - Task Invoke(ITransportReceiveContext context, Func next = null) => behavior.Invoke(context, next ?? (_ => Task.CompletedTask)); + Task Invoke(ITransportReceiveContext context, Func? next = null) => behavior.Invoke(context, next ?? (_ => Task.CompletedTask)); TransportReceiveToPhysicalMessageConnector behavior; @@ -234,7 +292,7 @@ public IPipeline Pipeline() class FakeBatchPipeline : IPipeline { - public IEnumerable TransportOperations { get; set; } + public IEnumerable? TransportOperations { get; set; } public Task Invoke(IBatchDispatchContext context) { diff --git a/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs b/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs index 426dd5b6067..9f18b65c3b3 100644 --- a/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs +++ b/src/NServiceBus.Core.Tests/Sagas/SagaMetadataCreationTests.cs @@ -78,6 +78,52 @@ public void HandleBothUniqueAttributeAndMapping() } } + [Test] + public void When_generated_correlation_accessor_is_supplied_it_is_used() + { + var sagaData = new MySagaWithMappedProperty.SagaData(); + var accessor = new TestCorrelationPropertyAccessor(); + var metadata = SagaMetadata.Create( + [new SagaMessage(typeof(SomeMessage), true, false)], + accessor, + []); + + Assert.That(metadata.TryGetCorrelationProperty(out var correlatedProperty), Is.True); + Assert.That(correlatedProperty.Accessor, Is.SameAs(accessor)); + + // The supplied accessor must actually be used to write and read the correlation value on the saga data. + correlatedProperty.Accessor.WriteTo(sagaData, 42); + Assert.That(sagaData.UniqueProperty, Is.EqualTo(42)); + Assert.That(correlatedProperty.Accessor.AccessFrom(sagaData), Is.EqualTo(42)); + } + + [Test] + public void When_no_correlation_accessor_is_supplied_an_expression_based_accessor_is_created() + { + var sagaData = new MySagaWithMappedProperty.SagaData(); + var metadata = SagaMetadata.Create( + [new SagaMessage(typeof(SomeMessage), true, false)], + null, + []); + + Assert.That(metadata.TryGetCorrelationProperty(out var correlatedProperty), Is.True); + Assert.That(correlatedProperty.Accessor, Is.TypeOf>()); + + // The fallback accessor must round-trip the correlation value on the saga data. + correlatedProperty.Accessor.WriteTo(sagaData, 42); + Assert.That(sagaData.UniqueProperty, Is.EqualTo(42)); + Assert.That(correlatedProperty.Accessor.AccessFrom(sagaData), Is.EqualTo(42)); + } + + class TestCorrelationPropertyAccessor : CorrelationPropertyAccessor + { + public override void WriteTo(IContainSagaData sagaData, object value) => + ((MySagaWithMappedProperty.SagaData)sagaData).UniqueProperty = (int)value; + + public override object AccessFrom(IContainSagaData sagaData) => + ((MySagaWithMappedProperty.SagaData)sagaData).UniqueProperty; + } + [Test] public void AutomaticallyAddUniqueForMappedProperty() { diff --git a/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs b/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs new file mode 100644 index 00000000000..47247bf486c --- /dev/null +++ b/src/NServiceBus.Core.Tests/Serializers/SystemTextJson/JsonMessageSerializerRootTypeTests.cs @@ -0,0 +1,59 @@ +namespace NServiceBus.Core.Tests.SystemTextJson; + +using System; +using System.IO; +using System.Text.Json; +using NServiceBus.MessageInterfaces.MessageMapper.Reflection; +using NServiceBus.Serializers.SystemJson; +using NUnit.Framework; + +[TestFixture] +public class JsonMessageSerializerRootTypeTests +{ + [Test] + public void Should_deserialize_once_when_a_later_message_type_is_a_base_of_the_current_root() + { + var serializer = CreateSerializer(); + var body = Serialize(serializer, new DerivedMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(DerivedMessage), typeof(BaseMessage)]); + + Assert.That(result, Has.Length.EqualTo(1)); + Assert.That(result[0], Is.TypeOf()); + } + + [Test] + public void Should_deserialize_each_message_type_when_neither_type_is_assignable_from_the_current_root() + { + var serializer = CreateSerializer(); + var body = Serialize(serializer, new DerivedMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(BaseMessage), typeof(DerivedMessage)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(result[1], Is.TypeOf()); + } + } + + static JsonMessageSerializer CreateSerializer() => new(new JsonSerializerOptions(), ContentTypes.Json, new TrimmingSafeMessageMapper()); + + static byte[] Serialize(JsonMessageSerializer serializer, object message) + { + using var stream = new MemoryStream(); + serializer.Serialize(message, stream); + return stream.ToArray(); + } + + public class BaseMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class DerivedMessage : BaseMessage + { + public string AdditionalProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs b/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs new file mode 100644 index 00000000000..e9e237832f9 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Serializers/XML/XmlMessageSerializerCharacterizationTests.cs @@ -0,0 +1,89 @@ +namespace NServiceBus.Core.Tests.Serializers.XML; + +using System; +using System.IO; +using System.Text; +using NServiceBus.Serializers.XML.Test; +using NServiceBus.Serializers.XML.Test.A; +using NServiceBus.Serializers.XML.Test.B; +using NUnit.Framework; + +[TestFixture] +public class XmlMessageSerializerCharacterizationTests +{ + [Test] + public void Should_deserialize_once_when_a_later_message_type_is_a_base_of_the_current_root() + { + var serializer = SerializerFactory.Create(typeof(DerivedXmlMessage), typeof(BaseXmlMessage)); + var body = Serialize(serializer, new DerivedXmlMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(DerivedXmlMessage), typeof(BaseXmlMessage)]); + + Assert.That(result, Has.Length.EqualTo(1)); + Assert.That(result[0], Is.TypeOf()); + } + + [Test] + public void Should_deserialize_each_message_type_when_neither_type_is_assignable_from_the_current_root() + { + var serializer = SerializerFactory.Create(typeof(BaseXmlMessage), typeof(DerivedXmlMessage)); + var body = Serialize(serializer, new DerivedXmlMessage { SomeProperty = "value", AdditionalProperty = "additional" }); + + var result = serializer.Deserialize(body, [typeof(BaseXmlMessage), typeof(DerivedXmlMessage)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(result[1], Is.TypeOf()); + } + } + + [Test] + public void Should_deserialize_all_messages_from_a_legacy_multi_message_payload() + { + var serializer = SerializerFactory.Create(typeof(Command1), typeof(Command2)); + var command1Id = Guid.NewGuid(); + var command2Id = Guid.NewGuid(); + + var body = Encoding.UTF8.GetBytes( + "" + + SerializeBody(serializer, new Command1(command1Id)) + + SerializeBody(serializer, new Command2(command2Id)) + + ""); + + var result = serializer.Deserialize(body, [typeof(Command1), typeof(Command2)]); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Has.Length.EqualTo(2)); + Assert.That(result[0], Is.TypeOf()); + Assert.That(((Command1)result[0]).Id, Is.EqualTo(command1Id)); + Assert.That(result[1], Is.TypeOf()); + Assert.That(((Command2)result[1]).Id, Is.EqualTo(command2Id)); + } + } + + static byte[] Serialize(XmlMessageSerializer serializer, object message) + { + using var stream = new MemoryStream(); + serializer.Serialize(message, stream); + return stream.ToArray(); + } + + static string SerializeBody(XmlMessageSerializer serializer, object message) + { + var xml = Encoding.UTF8.GetString(Serialize(serializer, message)); + return xml[(xml.IndexOf('>') + 1)..]; + } + + public class BaseXmlMessage : IMessage + { + public string SomeProperty { get; set; } + } + + public class DerivedXmlMessage : BaseXmlMessage + { + public string AdditionalProperty { get; set; } + } +} diff --git a/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs b/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs index 7a1ecd929b9..2af2e56f876 100644 --- a/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/ServicePlatform/Retries/RetryAcknowledgementBehaviorTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Tests.ServicePlatform.Retries; +#nullable enable + +namespace NServiceBus.Core.Tests.ServicePlatform.Retries; using System; using System.Collections.Generic; @@ -37,12 +39,13 @@ public async Task Should_confirm_successful_retries_to_acknowledgement_queue() Assert.That(outgoingMessage.Message.Headers.ContainsKey("ServiceControl.Retry.Successful"), Is.True); - Assert.That(outgoingMessage.Message.Body.Length, Is.EqualTo(0)); + Assert.That(outgoingMessage.Message.Body.Length, Is.Zero); Assert.That(outgoingMessage.Message.Headers[Headers.ControlMessageHeader], Is.EqualTo(bool.TrueString)); } var addressTag = outgoingMessage.RoutingStrategies.Single().Apply([]) as UnicastAddressTag; + Assert.That(addressTag, Is.Not.Null); using (Assert.EnterMultipleScope()) { Assert.That(addressTag.Destination, Is.EqualTo(acknowledgementQueue)); @@ -91,7 +94,7 @@ public void Should_not_confirm_when_processing_fails() using (Assert.EnterMultipleScope()) { Assert.That(exception, Is.SameAs(thrownException)); - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); } } @@ -109,7 +112,7 @@ public async Task Should_not_confirm_when_message_does_not_contain_acknowledgeme using (Assert.EnterMultipleScope()) { - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); Assert.That(context.Extensions.TryGet(out _), Is.False); } } @@ -127,7 +130,7 @@ public async Task Should_not_confirm_when_message_does_not_contain_retry_header( using (Assert.EnterMultipleScope()) { - Assert.That(routingPipeline.ForkInvocations.Count, Is.EqualTo(0)); + Assert.That(routingPipeline.ForkInvocations, Is.Empty); Assert.That(context.Extensions.TryGet(out _), Is.False); } } diff --git a/src/NServiceBus.Core.Tests/StandardsTests.cs b/src/NServiceBus.Core.Tests/StandardsTests.cs index 192b967e1f9..1147b05df0e 100644 --- a/src/NServiceBus.Core.Tests/StandardsTests.cs +++ b/src/NServiceBus.Core.Tests/StandardsTests.cs @@ -51,15 +51,12 @@ public void NonPublicShouldHaveSimpleNamespace() } } - static bool IsCompilerGenerated(Type x) - { - return Attribute.IsDefined(x, typeof(CompilerGeneratedAttribute), false); - } + static bool IsCompilerGenerated(Type x) => Attribute.IsDefined(x, typeof(CompilerGeneratedAttribute), false); [Test] public void LoggersShouldBeStaticField() { - foreach (var type in typeof(EndpointCreator).Assembly.GetTypes()) + foreach (var type in typeof(EndpointCreator).Assembly.GetTypes().Where(t => !IsCompilerGenerated(t))) { // Logging namespace contains special adapter types that are not expected to have static loggers if (type.Namespace == "NServiceBus.Logging") @@ -100,20 +97,15 @@ public void VerifyAttributesAreSealed() } } - static IEnumerable GetBehaviors() - { - return typeof(EndpointCreator).Assembly.GetTypes() + static IEnumerable GetBehaviors() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => type.GetInterfaces().Any(face => face.Name == nameof(NServiceBus.Pipeline.IBehavior)) && !type.IsAbstract && !type.IsGenericType); - } - static IEnumerable GetFeatures() - { - return typeof(EndpointCreator).Assembly.GetTypes() + + static IEnumerable GetFeatures() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => typeof(Feature).IsAssignableFrom(type) && type.IsPublic && !type.IsAbstract); - } - static IEnumerable GetAttributeTypes() - { - return typeof(EndpointCreator).Assembly.GetTypes() + static IEnumerable GetAttributeTypes() => + typeof(EndpointCreator).Assembly.GetTypes() .Where(type => typeof(Attribute).IsAssignableFrom(type)); - } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/StructConventionsTests.cs b/src/NServiceBus.Core.Tests/StructConventionsTests.cs index 82b12cfa338..ce73bc4a131 100644 --- a/src/NServiceBus.Core.Tests/StructConventionsTests.cs +++ b/src/NServiceBus.Core.Tests/StructConventionsTests.cs @@ -39,6 +39,12 @@ public void ApproveStructsWhichDontFollowStructGuidelines() continue; } + // InlineArray types are compiler-generated and don't follow normal struct conventions + if (type.GetCustomAttribute() != null) + { + continue; + } + // readonly structs can probably be ignored if (type.GetCustomAttribute() != null) { diff --git a/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs b/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs new file mode 100644 index 00000000000..25008216b99 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TestingFakesAnnotationParityTests.cs @@ -0,0 +1,113 @@ +namespace NServiceBus.Core.Tests; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using NServiceBus.Pipeline; +using NServiceBus.Testing; +using NUnit.Framework; + +[TestFixture] +public class TestingFakesAnnotationParityTests +{ + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Object_only_method_matches_interface_RequiresUnreferencedCode(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindObjectOnly(fakeType, methodName); + var iface = FindObjectOnly(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName} object-only overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName} object-only overload not found"); + + Assert.That(HasAttribute(fake), + Is.EqualTo(HasAttribute(iface)), + $"RequiresUnreferencedCode annotation mismatch on {fakeType.Name}.{methodName}(object)"); + } + + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Typed_method_matches_interface_annotations(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindTyped(fakeType, methodName); + var iface = FindTyped(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName} typed overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName} typed overload not found"); + + Assert.That(GetOverloadResolutionPriority(fake), Is.EqualTo(GetOverloadResolutionPriority(iface)), + $"OverloadResolutionPriority mismatch on {fakeType.Name}.{methodName}"); + + Assert.That(HasDynamicallyAccessedMembersOnGenericArgument(fake), + Is.EqualTo(HasDynamicallyAccessedMembersOnGenericArgument(iface)), + $"DynamicallyAccessedMembers on T mismatch on {fakeType.Name}.{methodName}"); + } + + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Send")] + [TestCase(typeof(TestablePipelineContext), typeof(IPipelineContext), "Publish")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Send")] + [TestCase(typeof(TestableMessageSession), typeof(IMessageSession), "Publish")] + [TestCase(typeof(TestableMessageProcessingContext), typeof(IMessageProcessingContext), "Reply")] + [TestCase(typeof(TestableOutgoingLogicalMessageContext), typeof(IOutgoingLogicalMessageContext), "UpdateMessage")] + public void Explicit_type_method_matches_interface_DynamicallyAccessedMembers(Type fakeType, Type interfaceType, string methodName) + { + var fake = FindExplicitType(fakeType, methodName); + var iface = FindExplicitType(interfaceType, methodName); + + Assert.That(fake, Is.Not.Null, $"{fakeType.Name}.{methodName}(object, Type) explicit-type overload not found"); + Assert.That(iface, Is.Not.Null, $"{interfaceType.Name}.{methodName}(object, Type) explicit-type overload not found"); + + Assert.That(HasDynamicallyAccessedMembersOnTypeParameter(fake), + Is.EqualTo(HasDynamicallyAccessedMembersOnTypeParameter(iface)), + $"DynamicallyAccessedMembers on messageType mismatch on {fakeType.Name}.{methodName}(object, Type)"); + } + + static MethodInfo FindObjectOnly(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && !m.IsGenericMethodDefinition + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType == typeof(object) + && !m.GetParameters().Any(p => p.ParameterType == typeof(Type))); + + static MethodInfo FindTyped(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && m.IsGenericMethodDefinition + && m.GetParameters().Length > 0 + && m.GetParameters()[0].ParameterType.IsGenericParameter); + + static MethodInfo FindExplicitType(Type type, string name) => + type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static) + .SingleOrDefault(m => m.Name == name + && !m.IsGenericMethodDefinition + && m.GetParameters().Length > 1 + && m.GetParameters()[0].ParameterType == typeof(object) + && m.GetParameters().Any(p => p.ParameterType == typeof(Type))); + + static bool HasAttribute(MethodInfo method) where T : Attribute => + method.GetCustomAttributes().Any(); + + static int? GetOverloadResolutionPriority(MethodInfo method) => + method.GetCustomAttributes() + .Select(a => (int?)a.Priority) + .FirstOrDefault(); + + static bool HasDynamicallyAccessedMembersOnGenericArgument(MethodInfo method) => + method.IsGenericMethodDefinition + && method.GetGenericArguments()[0].GetCustomAttributes().Any(); + + static bool HasDynamicallyAccessedMembersOnTypeParameter(MethodInfo method) => + method.GetParameters().First(p => p.ParameterType == typeof(Type)) + .GetCustomAttributes().Any(); +} diff --git a/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs b/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs index 9d990e1b31d..b76690d2b07 100644 --- a/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs +++ b/src/NServiceBus.Core.Tests/Transports/Learning/LearningTransportDispatcherTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using NServiceBus.Routing; using NUnit.Framework; @@ -10,6 +11,107 @@ public class LearningTransportDispatcherTests { + [Test] + public async Task Should_use_enclosed_message_hierarchy_for_multicast_subscribers() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"hierarchy-{Guid.NewGuid():N}"); + + try + { + AddSubscriber(path, typeof(ConcreteEvent), "concrete-subscriber"); + AddSubscriber(path, typeof(BaseEvent), "base-subscriber"); + AddSubscriber(path, typeof(IEventContract), "interface-subscriber"); + AddSubscriber(path, typeof(IHeaderOnlyEventContract), "header-only-subscriber"); + AddSubscriber(path, typeof(IReflectedOnlyEventContract), "reflected-only-subscriber"); + AddSubscriber(path, typeof(IEvent), "marker-interface-subscriber"); + + var hierarchy = string.Join(';', new[] { typeof(ConcreteEvent), typeof(BaseEvent), typeof(IEventContract), typeof(IHeaderOnlyEventContract), typeof(IEvent) } + .Select(static type => type.AssemblyQualifiedName)); + var message = new OutgoingMessage("id", new Dictionary { [Headers.EnclosedMessageTypes] = hierarchy }, ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(typeof(ConcreteEvent))); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(HasDispatchedMessage(path, "concrete-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "base-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "interface-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "header-only-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "reflected-only-subscriber"), Is.False); + Assert.That(HasDispatchedMessage(path, "marker-interface-subscriber"), Is.False); + } + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + + [Test] + public async Task Should_parse_generic_type_names_from_enclosed_message_hierarchy() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"generic-hierarchy-{Guid.NewGuid():N}"); + + try + { + var eventType = typeof(GenericEvent); + AddSubscriber(path, eventType, "generic-subscriber"); + + var message = new OutgoingMessage("id", new Dictionary { [Headers.EnclosedMessageTypes] = eventType.AssemblyQualifiedName! }, ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(eventType)); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + Assert.That(HasDispatchedMessage(path, "generic-subscriber"), Is.True); + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + + [Test] + public async Task Should_infer_hierarchy_when_enclosed_message_types_header_is_missing() + { + var path = Path.Combine(TestContext.CurrentContext.TestDirectory, $"missing-hierarchy-{Guid.NewGuid():N}"); + + try + { + AddSubscriber(path, typeof(ConcreteEvent), "concrete-subscriber"); + AddSubscriber(path, typeof(BaseEvent), "base-subscriber"); + AddSubscriber(path, typeof(IEventContract), "interface-subscriber"); + + var message = new OutgoingMessage("id", [], ReadOnlyMemory.Empty); + var operation = new TransportOperation(message, new MulticastAddressTag(typeof(ConcreteEvent))); + var dispatcher = new LearningTransportDispatcher(path, 64); + + await dispatcher.Dispatch(new TransportOperations(operation), new TransportTransaction()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(HasDispatchedMessage(path, "concrete-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "base-subscriber"), Is.True); + Assert.That(HasDispatchedMessage(path, "interface-subscriber"), Is.True); + } + } + finally + { + if (Directory.Exists(path)) + { + Directory.Delete(path, true); + } + } + } + [Test] public async Task Should_throw_for_size_above_threshold() { @@ -25,6 +127,25 @@ public async Task Should_throw_for_size_above_threshold() Assert.That(ex.Message, Does.Contain("The total size of the 'TestMessage' message")); } + static void AddSubscriber(string path, Type eventType, string subscriber) + { + var eventDirectory = Path.Combine(path, ".events", eventType.FullName!); + Directory.CreateDirectory(eventDirectory); + File.WriteAllText(Path.Combine(eventDirectory, $"{subscriber}.subscription"), subscriber); + } + + static bool HasDispatchedMessage(string path, string subscriber) => + Directory.Exists(Path.Combine(path, subscriber)) && Directory.EnumerateFiles(Path.Combine(path, subscriber), "*.metadata.txt").Any(); + const int MessageSizeLimit = (64 * 1024) - headerSize; const int headerSize = 57; -} \ No newline at end of file + + sealed class ConcreteEvent : BaseEvent, IEventContract, IReflectedOnlyEventContract; + class BaseEvent : IEvent; + interface IEventContract : IEvent; + interface IHeaderOnlyEventContract : IEvent; + interface IReflectedOnlyEventContract : IEvent; +} + +sealed class GenericEvent : IEvent; +sealed class NestedEvent; \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs index 67f86708167..c0c8fae72e5 100644 --- a/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs +++ b/src/NServiceBus.Core.Tests/Transports/ReceivePropertiesTests.cs @@ -34,7 +34,7 @@ public void Should_wrap_provided_dictionary() } [Test] - public void Should_be_same_reference_as_source() + public void Should_copy_from_provided_dictionary() { var source = new Dictionary { ["Key"] = "Value" }; var properties = new ReceiveProperties(source); diff --git a/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs new file mode 100644 index 00000000000..4aa6a8c5db0 --- /dev/null +++ b/src/NServiceBus.Core.Tests/TrimmedEndpoint/PackageConsumerTests.cs @@ -0,0 +1,210 @@ +namespace NServiceBus.Core.Tests.TrimmedEndpoint; + +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class PackageConsumerTests +{ + [Test] + [CancelAfter(1_800_000)] + public async Task Packaged_consumer_gets_interceptors_and_runtime_feature_switch_without_explicit_props(CancellationToken cancellationToken = default) + { + var root = Path.Combine(Path.GetTempPath(), "nservicebus-package-tests", Guid.NewGuid().ToString("N")); + var feed = Path.Combine(root, "feed"); + var consumer = Path.Combine(root, "consumer"); + var publishOutput = Path.Combine(root, "publish"); + // A dedicated packages folder keeps the restore isolated from any stale global-cache extraction of the + // same package version, so the freshly packed NServiceBus.targets is always the one that is imported. + var packagesDir = Path.Combine(root, "packages"); + try + { + // 1. Pack NServiceBus.Core into a local feed. + var coreProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "NServiceBus.Core", + "NServiceBus.Core.csproj")); + + var packResult = await RunProcess("dotnet", $"pack \"{coreProject}\" -c Release -o \"{feed}\" --nologo", cancellationToken); + Assert.That(packResult.ExitCode, Is.Zero, $"Pack failed:{Environment.NewLine}{packResult.Output}"); + + var nupkg = Directory.GetFiles(feed, "NServiceBus.*.nupkg").SingleOrDefault(); + Assert.That(nupkg, Is.Not.Null, "No NServiceBus package was produced by the pack."); + + using (var archive = ZipFile.OpenRead(nupkg!)) + using (Assert.EnterMultipleScope()) + { + Assert.That(archive.GetEntry("build/net10.0/NServiceBus.targets"), Is.Not.Null); + Assert.That(archive.GetEntry("buildTransitive/net10.0/NServiceBus.targets"), Is.Not.Null); + } + + var packageVersion = Path.GetFileNameWithoutExtension(nupkg)["NServiceBus.".Length..]; + + // 2. Create a consumer that references the PACKAGE (no project reference, no explicit + // CompilerVisibleProperty / InterceptorsNamespaces): everything must flow from the packed + // buildTransitive NServiceBus.props. + Directory.CreateDirectory(consumer); + await File.WriteAllTextAsync(Path.Combine(consumer, "nuget.config"), $$""" + + + + + + + + + """, cancellationToken); + + await File.WriteAllTextAsync(Path.Combine(consumer, "PackageConsumer.csproj"), $$""" + + + Exe + net10.0 + preview + enable + enable + true + + + + + + """, cancellationToken); + + await File.WriteAllTextAsync(Path.Combine(consumer, "Program.cs"), """ + using System.Text.Json; + using System.Text.Json.Serialization; + using NServiceBus; + + var configuration = new EndpointConfiguration("PackageConsumer"); + configuration.AssemblyScanner().Disable = true; + configuration.UseSerialization().Options(new JsonSerializerOptions + { + TypeInfoResolver = ConsumerJsonContext.Default + }); + configuration.UseTransport().StorageDirectory(Path.Combine(Path.GetTempPath(), "nservicebus-package-consumer")); + configuration.AddMessageType(); + configuration.AddHandler(); + + var endpoint = await Endpoint.Start(configuration); + IMessageSession session = endpoint; + await session.SendLocal(new MyCommand { SomeValue = "hello" }); + + for (var i = 0; i < 100 && !MyHandler.Invoked; i++) + { + await Task.Delay(100); + } + + await endpoint.Stop(); + + if (!MyHandler.Invoked) + { + Console.Error.WriteLine("Handler was not invoked."); + return 1; + } + + Console.WriteLine("TRIM-VALIDATION-SUCCESS"); + return 0; + + [Handler] + public class MyHandler : IHandleMessages + { + public static bool Invoked; + + public Task Handle(MyCommand message, IMessageHandlerContext context) + { + Invoked = true; + return Task.CompletedTask; + } + } + + public class MyCommand : ICommand + { + public string SomeValue { get; set; } = string.Empty; + } + + [JsonSerializable(typeof(MyCommand))] + public partial class ConsumerJsonContext : JsonSerializerContext + { + } + """, cancellationToken); + + // 3. Publish the consumer trimmed. Interceptor support and the runtime trim signal must come from the + // packed NServiceBus.props, not from explicit project settings. + var consumerProject = Path.Combine(consumer, "PackageConsumer.csproj"); + var publishResult = await RunProcess("dotnet", + $"publish \"{consumerProject}\" -c Release -p:TreatWarningsAsErrors=false -p:RestorePackagesPath=\"{packagesDir}\" -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"Consumer publish failed:{Environment.NewLine}{publishResult.Output}"); + + // The AddMessageType/AddHandler calls must be intercepted; otherwise the RequiresUnreferencedCode + // fallback surfaces as IL2026 at the consumer's own call sites. + var consumerTrimWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && line.Contains("IL2026")) + .ToArray(); + Assert.That(consumerTrimWarnings, Is.Empty, "Packaged interceptor support did not suppress IL2026 for the consumer source."); + + // The build-transitive NServiceBus.targets must have emitted the strict registered-only message + // metadata switch into the runtime configuration of this trimmed executable. + var runtimeConfigPath = Path.Combine(publishOutput, "PackageConsumer.runtimeconfig.json"); + Assert.That(File.Exists(runtimeConfigPath), Is.True, "Published consumer has no runtimeconfig.json."); + var runtimeConfig = System.Text.Json.JsonDocument.Parse(await File.ReadAllTextAsync(runtimeConfigPath, cancellationToken)); + var configProperties = runtimeConfig.RootElement.GetProperty("runtimeOptions").GetProperty("configProperties"); + Assert.That(configProperties.TryGetProperty(AppContextSwitches.StrictRegisteredOnlyMessageMetadataSwitchName, out var switchValue), Is.True, "Strict registered-only message metadata switch missing from runtimeconfig.json."); + Assert.That(switchValue.GetBoolean(), Is.True, "Strict registered-only message metadata switch must be true for a trimmed executable."); + + // 4. Run the packaged consumer executable. + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "PackageConsumer.exe" : "PackageConsumer"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"Packaged consumer failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(root, recursive: true); + } + catch (Exception ex) + { + TestContext.Progress.WriteLine($"Failed to clean up test directory {root}: {ex.Message}"); + } + } + } + + static async Task RunProcess(string fileName, string arguments, CancellationToken cancellationToken) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + + return new ProcessResult(process.ExitCode, (await outputTask) + Environment.NewLine + (await errorTask)); + } + + sealed record ProcessResult(int ExitCode, string Output); +} diff --git a/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs new file mode 100644 index 00000000000..1f2d7ffd9cd --- /dev/null +++ b/src/NServiceBus.Core.Tests/TrimmedEndpointTests.cs @@ -0,0 +1,186 @@ +#nullable enable + +namespace NServiceBus.Core.Tests.TrimmedEndpoint; + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; + +[TestFixture] +public class TrimmedEndpointTests +{ + [Test] + [CancelAfter(600_000)] + public async Task Scanner_disabled_endpoint_publishes_trimmed_and_processes_a_message(CancellationToken cancellationToken = default) + { + var sampleProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "TrimmedEndpoint", + "TrimmedEndpoint.csproj")); + + var publishOutput = Path.Combine(Path.GetTempPath(), "nservicebus-trimmed-tests", Guid.NewGuid().ToString("N")); + try + { + var publishResult = await RunProcess("dotnet", + $"publish \"{sampleProject}\" -c Release -p:TreatWarningsAsErrors=false -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"Publish failed:{Environment.NewLine}{publishResult.Output}"); + + // The AddMessageType and AddHandler calls in the sample are intercepted by source generators. If they + // were not intercepted, the RequiresUnreferencedCode fallback would surface as IL2026 trim warnings at + // the sample's own call sites. Trim warnings inside NServiceBus.Core itself fail the Core build via + // EnableTrimAnalyzer. + var sampleTrimWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && line.Contains("IL2026")) + .ToArray(); + Assert.That(sampleTrimWarnings, Is.Empty, "Interception of AddMessageType/AddHandler failed, IL2026 warnings were emitted for the sample source."); + + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "TrimmedEndpoint.exe" : "TrimmedEndpoint"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"Trimmed endpoint failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(publishOutput, recursive: true); + } + catch + { + // best-effort cleanup + } + } + } + + [Test] + // NativeAOT publish on a cold cache can take substantially longer than a trimmed publish. + [CancelAfter(1_200_000)] + public async Task Scanner_disabled_endpoint_publishes_and_runs_with_native_aot(CancellationToken cancellationToken = default) + { + var runtimeIdentifier = RuntimeIdentifierForCurrentPlatform(); + if (runtimeIdentifier is null) + { + Assert.Ignore("NativeAOT is not supported on this platform/architecture."); + return; + } + + var sampleProject = Path.GetFullPath(Path.Combine( + TestContext.CurrentContext.TestDirectory, + "..", "..", "..", "..", + "TrimmedEndpoint", + "TrimmedEndpoint.csproj")); + + var publishOutput = Path.Combine(Path.GetTempPath(), "nservicebus-aot-tests", Guid.NewGuid().ToString("N")); + try + { + var publishResult = await RunProcess("dotnet", + $"publish \"{sampleProject}\" -c Release -p:RunAotValidation=true -p:TreatWarningsAsErrors=false -r {runtimeIdentifier} -o \"{publishOutput}\" --nologo", + cancellationToken); + + Assert.That(publishResult.ExitCode, Is.Zero, $"NativeAOT publish failed:{Environment.NewLine}{publishResult.Output}"); + + // The sample source must not rely on dynamic code: any IL2026/IL3050 at the sample's own call sites + // would indicate an intercepted registration fell back to reflection. + var sampleAotWarnings = publishResult.Output.Split(Environment.NewLine) + .Where(line => line.Contains("Program.cs") && (line.Contains("IL2026") || line.Contains("IL3050"))) + .ToArray(); + Assert.That(sampleAotWarnings, Is.Empty, "Interception of AddMessageType/AddHandler failed, AOT warnings were emitted for the sample source."); + + var executable = Path.Combine(publishOutput, OperatingSystem.IsWindows() ? "TrimmedEndpoint.exe" : "TrimmedEndpoint"); + var runResult = await RunProcess(executable, "", cancellationToken); + + using (Assert.EnterMultipleScope()) + { + Assert.That(runResult.ExitCode, Is.Zero, $"NativeAOT endpoint failed:{Environment.NewLine}{runResult.Output}"); + Assert.That(runResult.Output, Does.Contain("TRIM-VALIDATION-SUCCESS")); + } + } + finally + { + try + { + Directory.Delete(publishOutput, recursive: true); + } + catch + { + // best-effort cleanup + } + } + } + + static string? RuntimeIdentifierForCurrentPlatform() + { + var architecture = RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.X86 => null, + Architecture.Arm => null, + Architecture.Arm64 => "arm64", + Architecture.Wasm => null, + Architecture.S390x => null, + Architecture.LoongArch64 => null, + Architecture.Armv6 => null, + Architecture.Ppc64le => null, + Architecture.RiscV64 => null, + _ => null + }; + + if (architecture is null) + { + return null; + } + + if (OperatingSystem.IsWindows()) + { + return $"win-{architecture}"; + } + + if (OperatingSystem.IsLinux()) + { + return $"linux-{architecture}"; + } + + if (OperatingSystem.IsMacOS()) + { + return $"osx-{architecture}"; + } + + return null; + } + + static async Task RunProcess(string fileName, string arguments, CancellationToken cancellationToken) + { + var startInfo = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + }; + + using var process = Process.Start(startInfo)!; + + var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = process.StandardError.ReadToEndAsync(cancellationToken); + + await process.WaitForExitAsync(cancellationToken); + + return new ProcessResult(process.ExitCode, (await outputTask) + Environment.NewLine + (await errorTask)); + } + + sealed record ProcessResult(int ExitCode, string Output); +} diff --git a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs new file mode 100644 index 00000000000..9683dbd95ea --- /dev/null +++ b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs @@ -0,0 +1,995 @@ +namespace NServiceBus.Core.Tests; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using NServiceBus.Testing; +using NUnit.Framework; + +[TestFixture] +public class TypedMessageInstanceOverloadsTests +{ + [Test] + public async Task Send_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_interface_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (IMyMessage)new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_explicit_generic_call_via_IMessageSession_uses_generic_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send(message); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + Assert.That(session.SendGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task Send_with_destination_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Send("destination", message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_with_destination_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Send("destination", message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.SendLocal(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.SendLocal(message); + Assert.That(session.SendObjectCount, Is.EqualTo(1)); + Assert.That(session.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_object_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (object)new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_interface_typed_variable_via_IMessageSession_uses_object_overload() + { + var session = new TrackingMessageSession(); + var message = (IMyMessage)new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(1)); + Assert.That(session.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_generic_call_via_IMessageSession_uses_generic_overload() + { + var session = new TrackingMessageSession(); + var message = new MyMessage(); + await session.Publish(message); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + Assert.That(session.PublishGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task Reply_object_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = (object)new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_typed_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_interface_typed_variable_via_IMessageProcessingContext_uses_object_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = (IMyMessage)new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(1)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_generic_call_via_IMessageProcessingContext_uses_generic_overload() + { + var context = new TrackingMessageProcessingContext(); + var message = new MyMessage(); + await context.Reply(message); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + Assert.That(context.ReplyGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_Send_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_generic_call_via_IPipelineContext_uses_generic_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send(message); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + Assert.That(context.SendGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_Publish_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(1)); + Assert.That(context.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(1)); + Assert.That(context.PublishGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_generic_call_via_IPipelineContext_uses_generic_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Publish(message); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + Assert.That(context.PublishGenericCount, Is.EqualTo(1)); + } + + [Test] + public async Task PipelineContext_SendLocal_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.SendLocal(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_SendLocal_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.SendLocal(message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_object_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = (object)new MyMessage(); + await context.Send("destination", message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_typed_variable_via_IPipelineContext_uses_object_overload() + { + var context = new TrackingPipelineContext(); + var message = new MyMessage(); + await context.Send("destination", message); + Assert.That(context.SendObjectCount, Is.EqualTo(1)); + Assert.That(context.SendGenericCount, Is.EqualTo(0)); + } + + [Test] + public void Testable_outgoing_context_ordinary_call_uses_runtime_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = (IMyMessage)new MyMessage(); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_generic_call_uses_specified_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = new MyMessage(); + + context.UpdateMessage(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_type_validates_declared_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + object message = new MyMessage(); + + var ex = Assert.Throws(() => context.UpdateMessage(message, typeof(string))); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Testable_outgoing_context_explicit_type_rejects_null_instance() + { + var context = new TestableOutgoingLogicalMessageContext(); + + Assert.Throws(() => context.UpdateMessage(null!, typeof(IMyMessage))); + } + + [Test] + public void Testable_outgoing_context_explicit_type_rejects_null_message_type() + { + var context = new TestableOutgoingLogicalMessageContext(); + var message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessage(message, null!)); + } + + [Test] + public void Testable_outgoing_context_explicit_type_preserves_declared_type_and_instance() + { + var context = new TestableOutgoingLogicalMessageContext(); + object message = new MyMessage(); + + context.UpdateMessage(message, typeof(IMyMessage)); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + + [Test] + public void Testable_incoming_context_ordinary_call_uses_runtime_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = (IMyMessage)new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_generic_call_uses_specified_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_validates_declared_type() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, typeof(MyOtherMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_message_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, null!)); + } + + [Test] + public void Testable_incoming_context_explicit_type_preserves_declared_type_and_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + context.UpdateMessageInstance(message, typeof(IMyMessage)); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + + [Test] + public async Task Default_interface_fallback_Send_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + var message = new MyMessage(); + await session.Send(message, new SendOptions()); + Assert.That(legacy.SendObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Publish_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + var message = new MyMessage(); + await session.Publish(message, new PublishOptions()); + Assert.That(legacy.PublishObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Reply_uses_object_overload() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + var message = new MyMessage(); + await context.Reply(message, new ReplyOptions()); + Assert.That(legacy.ReplyObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Send_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_explicit_type_with_options_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Send_with_destination_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Send("destination", message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task SendLocal_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.SendLocal(message, typeof(IMyMessage)); + Assert.That(session.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_type_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage)); + Assert.That(session.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Publish_explicit_type_with_options_via_IMessageSession_uses_explicit_type_overload() + { + var session = new TrackingMessageSession(); + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(session.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(session.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_type_via_IMessageProcessingContext_uses_explicit_type_overload() + { + var context = new TrackingMessageProcessingContext(); + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage)); + Assert.That(context.ReplyExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Reply_explicit_type_with_options_via_IMessageProcessingContext_uses_explicit_type_overload() + { + var context = new TrackingMessageProcessingContext(); + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage), new ReplyOptions()); + Assert.That(context.ReplyExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.ReplyObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send(message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_explicit_type_with_options_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Send_with_destination_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Send("destination", message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_SendLocal_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.SendLocal(message, typeof(IMyMessage)); + Assert.That(context.SendExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.SendObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_type_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Publish(message, typeof(IMyMessage)); + Assert.That(context.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task PipelineContext_Publish_explicit_type_with_options_via_IPipelineContext_uses_explicit_type_overload() + { + var context = new TrackingPipelineContext(); + object message = new MyMessage(); + await context.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(context.PublishExplicitTypeCount, Is.EqualTo(1)); + Assert.That(context.PublishObjectCount, Is.EqualTo(0)); + } + + [Test] + public async Task Default_interface_fallback_Send_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + await session.Send(message, typeof(IMyMessage), new SendOptions()); + Assert.That(legacy.SendObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Publish_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + await session.Publish(message, typeof(IMyMessage), new PublishOptions()); + Assert.That(legacy.PublishObjectCount, Is.EqualTo(1)); + } + + [Test] + public async Task Default_interface_fallback_Reply_explicit_type_uses_object_overload() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + object message = new MyMessage(); + await context.Reply(message, typeof(IMyMessage), new ReplyOptions()); + Assert.That(legacy.ReplyObjectCount, Is.EqualTo(1)); + } + + [Test] + public void Send_explicit_type_with_unrelated_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await session.Send(message, typeof(MyOtherMessage), new SendOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Send_explicit_type_with_null_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Send(message, null!, new SendOptions())); + } + + [Test] + public void Send_explicit_type_with_null_message_throws() + { + var session = new TestableMessageSession(); + Assert.ThrowsAsync(async () => + await session.Send(null!, typeof(IMyMessage), new SendOptions())); + } + + [Test] + public void Default_interface_fallback_Send_explicit_type_with_null_message_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + Assert.ThrowsAsync(async () => + await session.Send(null!, typeof(IMyMessage), new SendOptions())); + } + + [Test] + public void Publish_explicit_type_with_unrelated_type_throws() + { + var session = new TestableMessageSession(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await session.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Reply_explicit_type_with_unrelated_type_throws() + { + var context = new TestableMessageProcessingContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Reply(message, typeof(MyOtherMessage), new ReplyOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void PipelineContext_Send_explicit_type_with_unrelated_type_throws() + { + var context = new TestablePipelineContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Send(message, typeof(MyOtherMessage), new SendOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void PipelineContext_Publish_explicit_type_with_unrelated_type_throws() + { + var context = new TestablePipelineContext(); + object message = new MyMessage(); + var ex = Assert.ThrowsAsync(async () => + await context.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + Assert.That(ex!.ParamName, Is.EqualTo("message")); + } + + [Test] + public void Default_interface_fallback_Send_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Send(message, typeof(MyOtherMessage), new SendOptions())); + } + + [Test] + public void Default_interface_fallback_Publish_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageSession(); + IMessageSession session = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await session.Publish(message, typeof(MyOtherMessage), new PublishOptions())); + } + + [Test] + public void Default_interface_fallback_Reply_explicit_type_with_unrelated_type_throws() + { + var legacy = new LegacyMessageProcessingContext(); + IMessageProcessingContext context = legacy; + object message = new MyMessage(); + Assert.ThrowsAsync(async () => + await context.Reply(message, typeof(MyOtherMessage), new ReplyOptions())); + } + + public interface IMyMessage + { + } + + public class MyMessage : IMyMessage + { + } + + public class MyOtherMessage + { + } + + class TrackingMessageSession : TestableMessageSession + { + public int SendObjectCount; + public int SendGenericCount; + public int SendExplicitTypeCount; + public int PublishObjectCount; + public int PublishGenericCount; + public int PublishExplicitTypeCount; + + public override Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) + { + if (!trackingGenericSend && !trackingExplicitTypeSend) + { + SendObjectCount++; + } + + return base.Send(message, options, cancellationToken); + } + + public override Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options, CancellationToken cancellationToken = default) + { + SendGenericCount++; + trackingGenericSend = true; + try + { + return base.Send(message, options, cancellationToken); + } + finally + { + trackingGenericSend = false; + } + } + + public override Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options, CancellationToken cancellationToken = default) + { + SendExplicitTypeCount++; + trackingExplicitTypeSend = true; + try + { + return base.Send(message, messageType, options, cancellationToken); + } + finally + { + trackingExplicitTypeSend = false; + } + } + + public override Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) + { + if (!trackingGenericPublish && !trackingExplicitTypePublish) + { + PublishObjectCount++; + } + + return base.Publish(message, options, cancellationToken); + } + + public override Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options, CancellationToken cancellationToken = default) + { + PublishGenericCount++; + trackingGenericPublish = true; + try + { + return base.Publish(message, options, cancellationToken); + } + finally + { + trackingGenericPublish = false; + } + } + + public override Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options, CancellationToken cancellationToken = default) + { + PublishExplicitTypeCount++; + trackingExplicitTypePublish = true; + try + { + return base.Publish(message, messageType, options, cancellationToken); + } + finally + { + trackingExplicitTypePublish = false; + } + } + + bool trackingGenericSend; + bool trackingGenericPublish; + bool trackingExplicitTypeSend; + bool trackingExplicitTypePublish; + } + + class TrackingPipelineContext : TestablePipelineContext + { + public int SendObjectCount; + public int SendGenericCount; + public int SendExplicitTypeCount; + public int PublishObjectCount; + public int PublishGenericCount; + public int PublishExplicitTypeCount; + + public override Task Send(object message, SendOptions options) + { + if (!trackingGenericSend && !trackingExplicitTypeSend) + { + SendObjectCount++; + } + + return base.Send(message, options); + } + + public override Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + SendGenericCount++; + trackingGenericSend = true; + try + { + return base.Send(message, options); + } + finally + { + trackingGenericSend = false; + } + } + + public override Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + SendExplicitTypeCount++; + trackingExplicitTypeSend = true; + try + { + return base.Send(message, messageType, options); + } + finally + { + trackingExplicitTypeSend = false; + } + } + + public override Task Publish(object message, PublishOptions options) + { + if (!trackingGenericPublish && !trackingExplicitTypePublish) + { + PublishObjectCount++; + } + + return base.Publish(message, options); + } + + public override Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + PublishGenericCount++; + trackingGenericPublish = true; + try + { + return base.Publish(message, options); + } + finally + { + trackingGenericPublish = false; + } + } + + public override Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + PublishExplicitTypeCount++; + trackingExplicitTypePublish = true; + try + { + return base.Publish(message, messageType, options); + } + finally + { + trackingExplicitTypePublish = false; + } + } + + bool trackingGenericSend; + bool trackingGenericPublish; + bool trackingExplicitTypeSend; + bool trackingExplicitTypePublish; + } + + class TrackingMessageProcessingContext : TestableMessageProcessingContext + { + public int ReplyObjectCount; + public int ReplyGenericCount; + public int ReplyExplicitTypeCount; + + public override Task Reply(object message, ReplyOptions options) + { + if (!trackingGenericReply && !trackingExplicitTypeReply) + { + ReplyObjectCount++; + } + + return base.Reply(message, options); + } + + public override Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + ReplyGenericCount++; + trackingGenericReply = true; + try + { + return base.Reply(message, options); + } + finally + { + trackingGenericReply = false; + } + } + + public override Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + ReplyExplicitTypeCount++; + trackingExplicitTypeReply = true; + try + { + return base.Reply(message, messageType, options); + } + finally + { + trackingExplicitTypeReply = false; + } + } + + bool trackingGenericReply; + bool trackingExplicitTypeReply; + } + + class LegacyMessageSession : IMessageSession + { + public int SendObjectCount; + public int PublishObjectCount; + + public Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + SendObjectCount++; + return Task.CompletedTask; + } + + public Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + PublishObjectCount++; + return Task.CompletedTask; + } + + public Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + => throw new NotImplementedException(); + + public Task Subscribe(Type eventType, SubscribeOptions subscribeOptions, CancellationToken cancellationToken = default) => Task.CompletedTask; + public Task Unsubscribe(Type eventType, UnsubscribeOptions unsubscribeOptions, CancellationToken cancellationToken = default) => Task.CompletedTask; + } + + class LegacyMessageProcessingContext : IMessageProcessingContext + { + public int ReplyObjectCount; + + CancellationToken ICancellableContext.CancellationToken => default; + public Extensibility.ContextBag Extensions { get; set; } = new Extensibility.ContextBag(); + + public Task Reply(object message, ReplyOptions options) + { + ReplyObjectCount++; + return Task.CompletedTask; + } + + public Task Reply(Action messageConstructor, ReplyOptions options) => throw new NotImplementedException(); + + public string MessageId => throw new NotImplementedException(); + public string ReplyToAddress => throw new NotImplementedException(); + public IReadOnlyDictionary MessageHeaders => throw new NotImplementedException(); + public Task ForwardCurrentMessageTo(string destination) => throw new NotImplementedException(); + + public Task Send(object message, SendOptions options) => throw new NotImplementedException(); + public Task Send(Action messageConstructor, SendOptions options) => throw new NotImplementedException(); + public Task Publish(object message, PublishOptions options) => throw new NotImplementedException(); + public Task Publish(Action messageConstructor, PublishOptions publishOptions) => throw new NotImplementedException(); + } +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs b/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs new file mode 100644 index 00000000000..afbf0d52ace --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Config/MessageTypeRegistrationExtensionsTests.cs @@ -0,0 +1,92 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageTypeRegistrationExtensionsTests +{ + [Test] + public void Should_register_message_type_with_runtime_inferred_hierarchy() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + var metadata = registry.GetMessageMetadata(typeof(MyEvent)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyEvent))); + Assert.That(metadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + } + + [Test] + public void Should_register_message_type_when_registry_is_already_initialized() + { + var config = new EndpointConfiguration("test"); + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + config.AddMessageType(); + + Assert.That(registry.GetAllMessages().Select(m => m.MessageType), Does.Contain(typeof(MyEvent))); + } + + [Test] + public void Should_not_register_type_rejected_by_conventions() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + var registry = config.Settings.GetOrCreate(); + registry.Initialize(new Conventions().IsMessageType, true); + + using (Assert.EnterMultipleScope()) + { + Assert.That(registry.GetAllMessages(), Is.Empty); + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(NotAMessage))); + Assert.That(exception?.Message, Does.Contain("Could not find metadata")); + } + } + + [Test] + public void Should_register_message_type_when_conventions_are_configured_after_registration() + { + var config = new EndpointConfiguration("test"); + config.AddMessageType(); + + config.Conventions().DefiningMessagesAs(type => type == typeof(UnobtrusiveMessage)); + + // Mirrors EndpointCreator.ConfigureMessageTypes: the deferred registration is evaluated against the finalized conventions at initialization time. + var registry = config.Settings.GetOrCreate(); + registry.Initialize(config.Conventions().Conventions.IsMessageType, true); + + var metadata = registry.GetAllMessages().Single(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(metadata.MessageType, Is.EqualTo(typeof(UnobtrusiveMessage))); + Assert.That(metadata.MessageHierarchy, Is.EqualTo(new[] { typeof(UnobtrusiveMessage) })); + } + } + + [Test] + public void Should_throw_when_configuration_is_null() + { + Assert.Throws(() => MessageTypeRegistrationExtensions.AddMessageType(null)); + } + + class MyEvent : ConcreteParent1, IInterfaceParent1; + class NotAMessage; + class UnobtrusiveMessage; + class ConcreteParent1 : ConcreteParentBase; + class ConcreteParentBase : IMessage; + interface IInterfaceParent1 : IInterfaceParent1Base; + interface IInterfaceParent1Base : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs b/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs index 64067a843df..481720e95e0 100644 --- a/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs +++ b/src/NServiceBus.Core.Tests/Unicast/MessageOperationsTests.cs @@ -76,6 +76,47 @@ public async Task When_sending_should_clone_headersAsync() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_sending_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, new SendOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.SendPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_sending_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, typeof(IMyMessage), new SendOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.SendPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_sending_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Send(new FakeRootContext(), message, new SendOptions()); + + Assert.That(messageOperations.SendPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + [Test] public async Task When_replying_message_interface_should_set_interface_as_message_typeAsync() { @@ -145,6 +186,47 @@ public async Task When_replying_should_clone_headersAsync() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_replying_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, new ReplyOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_replying_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, typeof(IMyMessage), new ReplyOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_replying_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Reply(new FakeRootContext(), message, new ReplyOptions()); + + Assert.That(messageOperations.ReplyPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + [Test] public async Task When_publishing_event_interface_should_set_interface_as_message_typeAsync() { @@ -214,11 +296,52 @@ public async Task When_publishing_should_clone_headers() Assert.That(optionsHeaders["header1"], Is.EqualTo("header1 value")); } + [Test] + public async Task When_publishing_typed_instance_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + var message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, new PublishOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.PublishPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_publishing_instance_with_explicit_type_should_preserve_declared_type_and_instance() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, typeof(IMyMessage), new PublishOptions()); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(messageOperations.PublishPipeline.LastContext.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public async Task When_publishing_instance_without_declared_type_should_use_runtime_type() + { + var messageOperations = new TestableMessageOperations(); + object message = new MyMessage(); + + await messageOperations.Publish(new FakeRootContext(), message, new PublishOptions()); + + Assert.That(messageOperations.PublishPipeline.LastContext.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + public interface IMyMessage { } - class MyMessage + class MyMessage : IMyMessage { } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs new file mode 100644 index 00000000000..bafe42fd7e1 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCacheOnlyLookupTests.cs @@ -0,0 +1,112 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryCacheOnlyLookupTests +{ + [Test] + public void Should_return_metadata_for_registered_type() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage), out var metadata); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.True); + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + } + + [Test] + public void Should_not_register_unregistered_type_when_lookup_misses() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage), out _); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.False); + Assert.That(registry.GetAllMessages().Select(m => m.MessageType), Does.Not.Contain(typeof(MyMessage))); + } + } + + [Test] + public void Should_return_metadata_for_registered_type_identifier() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName, out var metadata); + + using (Assert.EnterMultipleScope()) + { + Assert.That(found, Is.True); + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + } + + [Test] + public void Should_return_false_for_unknown_identifier_without_dynamic_type_loading() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var found = registry.TryGetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_for_loadable_type_that_is_not_registered() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName, out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_for_unregistered_type_that_matches_convention() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var found = registry.TryGetMessageMetadata(typeof(OtherMessage), out _); + + Assert.That(found, Is.False); + } + + [Test] + public void Should_return_false_without_throwing_after_a_negative_identifier_was_cached_by_legacy_lookup() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + registry.RegisterMessageTypes([typeof(MyMessage)]); + + // The legacy string lookup caches unresolved identifiers as a null entry. + var legacyResult = registry.GetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + Assert.That(legacyResult, Is.Null); + + // The cache-only lookup must treat the negative cache entry as a miss instead of dereferencing it. + var found = registry.TryGetMessageMetadata("Some.Namespace.SomeType, SomeAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", out _); + + Assert.That(found, Is.False); + } + + public class MyMessage : IMessage; + public class OtherMessage : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs new file mode 100644 index 00000000000..f8ddf124a8f --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryCharacterizationTests.cs @@ -0,0 +1,103 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using System.Linq; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryCharacterizationTests +{ + [Test] + public void Should_return_the_same_cached_metadata_instance_for_a_registered_concrete_type() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyEvent)]); + + var first = registry.GetMessageMetadata(typeof(MyEvent)); + var second = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(second, Is.SameAs(first)); + } + + [Test] + public void Should_include_registered_concrete_types_in_get_all_messages() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyEvent), typeof(MyOtherEvent)]); + + var allMessageTypes = registry.GetAllMessages().Select(m => m.MessageType).ToList(); + + Assert.That(allMessageTypes, Does.Contain(typeof(MyEvent)).And.Contain(typeof(MyOtherEvent))); + } + + [Test] + public void Should_use_the_explicitly_supplied_hierarchy_when_registered_before_initialization() + { + var registry = new MessageMetadataRegistry(); + registry.RegisterMessageTypeWithHierarchy(typeof(MyEvent), [typeof(ConcreteParent1), typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(ConcreteParent1) })); + } + + [Test] + public void Should_infer_the_hierarchy_at_runtime_when_registered_before_initialization_without_hierarchy() + { + var registry = new MessageMetadataRegistry(); + registry.RegisterMessageTypes([typeof(MyEvent)]); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + + [Test] + public void Should_resolve_and_register_unregistered_concrete_types_on_demand() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, false); + + var messageMetadata = registry.GetMessageMetadata(typeof(MyEvent)); + + using (Assert.EnterMultipleScope()) + { + Assert.That(messageMetadata.MessageType, Is.EqualTo(typeof(MyEvent))); + Assert.That(messageMetadata.MessageHierarchy, Is.EqualTo(new[] { typeof(MyEvent), typeof(IInterfaceParent1), typeof(ConcreteParent1), typeof(IInterfaceParent1Base), typeof(ConcreteParentBase) })); + } + } + + [Test] + public void Should_throw_an_actionable_exception_when_type_is_not_registered_and_not_a_message() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(string))); + + Assert.That(exception?.Message, Does.Contain("Could not find metadata for 'System.String'").And.Contain("included in initial scanning").And.Contain("implements either 'IMessage', 'IEvent' or 'ICommand'")); + } + + [Test] + public void Should_return_null_when_type_identifier_loads_a_type_that_is_not_a_message() + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + + var messageMetadata = registry.GetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName); + + Assert.That(messageMetadata, Is.Null); + } + + class MyEvent : ConcreteParent1, IInterfaceParent1; + class MyOtherEvent : IMessage; + class ConcreteParent1 : ConcreteParentBase; + class ConcreteParentBase : IMessage; + interface IInterfaceParent1 : IInterfaceParent1Base; + interface IInterfaceParent1Base : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs new file mode 100644 index 00000000000..25e4aff6079 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeInteractionMatrixTests.cs @@ -0,0 +1,145 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using NUnit.Framework; +using Unicast.Messages; + +// Interaction matrix for strict registered-only mode x configured dynamic type loading. +// +// Strict mode is the stronger non-overridable policy: it forbids Type.GetType-based string loading and all runtime +// hierarchy inference/registration on cache misses. With strict off, DynamicTypeLoadingEnabled only controls +// unresolved string/header Type.GetType loading, while Type-based legacy hierarchy inference still works. +// +// | Strict | DynamicLoading | Registered string id | Unregistered string id | Type cache miss | Bare pre-registration | Generated-hierarchy pre-registration | +// |--------|----------------|----------------------|------------------------|-----------------|-----------------------|--------------------------------------| +// | off | off | resolve | null (no Type.GetType) | register (inference) | register at init | register at init | +// | off | on | resolve | Type.GetType + register | register (inference) | register at init | register at init | +// | on | off | resolve | null (no load/register) | throw strict | throw strict at init | register at init | +// | on | on | resolve | null (no load/register) | throw strict | throw strict at init | register at init | +[TestFixture] +public class MessageMetadataRegistryStrictModeInteractionMatrixTests +{ + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void Registered_string_identifier_resolves_in_all_modes(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [TestCase(false, true)] + public void Unregistered_string_identifier_loads_and_registers_when_dynamic_loading_enabled_and_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyOtherMessage))); + } + + [TestCase(false, false)] + public void Unregistered_string_identifier_returns_null_when_dynamic_loading_disabled_and_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Unregistered_string_identifier_returns_null_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [TestCase(false, false)] + [TestCase(false, true)] + public void Type_cache_miss_registers_via_runtime_inference_when_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var metadata = registry.GetMessageMetadata(typeof(MyOtherMessage)); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyOtherMessage))); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Type_cache_miss_throws_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = CreateRegistry(strictMode, dynamicTypeLoading); + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(MyOtherMessage))); + + Assert.That(exception?.Message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("AddMessageType()") + .And.Contain("AddHandler()") + .And.Contain("AddSaga()")); + } + + [TestCase(false, false)] + [TestCase(false, true)] + public void Bare_pre_registration_registers_at_initialization_when_strict_off(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [TestCase(true, false)] + [TestCase(true, true)] + public void Bare_pre_registration_fails_at_initialization_in_strict_mode(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var exception = Assert.Throws(() => registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading)); + + Assert.That(exception?.Message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("AddMessageType()") + .And.Contain("AddHandler()") + .And.Contain("AddSaga()")); + } + + [TestCase(false, false)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(true, true)] + public void Generated_hierarchy_pre_registration_registers_at_initialization_in_all_modes(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + static MessageMetadataRegistry CreateRegistry(bool strictMode, bool dynamicTypeLoading) + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = strictMode }; + registry.Initialize(new Conventions().IsMessageType, dynamicTypeLoading); + return registry; + } + + public class MyMessage : IMessage; + public class MyOtherMessage : IMessage; +} diff --git a/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs new file mode 100644 index 00000000000..614daee47b5 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Unicast/Messages/MessageMetadataRegistryStrictModeTests.cs @@ -0,0 +1,96 @@ +namespace NServiceBus.Unicast.Tests; + +using System; +using NUnit.Framework; +using Unicast.Messages; + +[TestFixture] +public class MessageMetadataRegistryStrictModeTests +{ + [Test] + public void Should_throw_actionable_exception_for_unregistered_message_type() + { + var registry = CreateStrictRegistry(); + + var exception = Assert.Throws(() => registry.GetMessageMetadata(typeof(MyOtherMessage))); + + AssertStrictModeExceptionGuidance(exception); + } + + [Test] + public void Should_resolve_registered_message_type_in_strict_mode() + { + var registry = CreateStrictRegistry(); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage)); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Should_register_generated_hierarchy_pre_registration_during_initialization_in_strict_mode() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + + Assert.That(registry.GetMessageMetadata(typeof(MyMessage)).MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Should_fail_bare_pre_registration_during_initialization_in_strict_mode() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypes([typeof(MyMessage)]); + + var exception = Assert.Throws(() => registry.Initialize(new Conventions().IsMessageType, true)); + + AssertStrictModeExceptionGuidance(exception); + } + + [Test] + public void Should_return_null_for_unknown_identifier_in_strict_mode_without_dynamic_loading() + { + var registry = CreateStrictRegistry(); + + // The type loads from a real assembly, but strict mode must not resolve or register it. + var metadata = registry.GetMessageMetadata(typeof(EndpointConfiguration).AssemblyQualifiedName); + + Assert.That(metadata, Is.Null); + } + + [Test] + public void Should_return_metadata_for_registered_identifier_in_strict_mode() + { + var registry = CreateStrictRegistry(); + + var metadata = registry.GetMessageMetadata(typeof(MyMessage).AssemblyQualifiedName); + + Assert.That(metadata.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + static void AssertStrictModeExceptionGuidance(Exception exception) + { + var message = exception.Message; + + Assert.That(message, Does.Contain("strict registered-only message metadata mode") + .And.Contain("Ensure one of the following registration paths is used:") + .And.Contain("1. Register the message type before the endpoint starts using 'AddMessageType()' or 'RegisterMessageTypeWithHierarchy'.") + .And.Contain("2. If '") + .And.Contain("register the handler or saga with 'AddHandler()' or 'AddSaga()' and the message type with 'AddMessageType()'.") + .And.Contain("In either case, ensure") + .And.Contain("implements either 'IMessage', 'IEvent' or 'ICommand'") + .And.Contain("Unobtrusive Mode")); + } + + static MessageMetadataRegistry CreateStrictRegistry() + { + var registry = new MessageMetadataRegistry { StrictRegisteredOnlyMode = true }; + registry.RegisterMessageTypeWithHierarchy(typeof(MyMessage), [typeof(IMessage)]); + registry.Initialize(new Conventions().IsMessageType, true); + return registry; + } + + public class MyMessage : IMessage; + public class MyOtherMessage : IMessage; +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs b/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs index 2ea4d274604..bd70effd3a6 100644 --- a/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs +++ b/src/NServiceBus.Core.Tests/Unicast/RunningEndpointInstanceTest.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using Settings; using Testing; +using NServiceBus.Transport; [TestFixture] public class RunningEndpointInstanceTest @@ -17,10 +18,10 @@ static RunningEndpointInstance Create() var testInstance = new RunningEndpointInstance( settings, - null, + null!, new FeatureComponent(new FeatureComponent.Settings()), new TestableMessageSession(), - null, + null!, new CancellationTokenSource(), NoOpAsyncDisposable.Instance, new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); @@ -47,6 +48,59 @@ public async Task ShouldAllowMultipleDispose() Assert.That(async () => await testInstance.DisposeAsync(), Throws.Nothing); } + [Test] + public async Task DisposeAsync_should_pass_a_bounded_cancellation_token_to_transport_Shutdown() + { + var capturingTransport = new TokenCapturingTransportInfrastructure(); + + var testInstance = new RunningEndpointInstance( + new SettingsHolder(), + CreateEmptyReceiveComponent(), + new FeatureComponent(new FeatureComponent.Settings()), + new TestableMessageSession(), + capturingTransport, + new CancellationTokenSource(), + NoOpAsyncDisposable.Instance, + new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); + + await testInstance.DisposeAsync(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(capturingTransport.ShutdownWasCalled, Is.True, + "transport.Shutdown was not called during DisposeAsync."); + Assert.That(capturingTransport.ObservedToken.CanBeCanceled, Is.True, + "DisposeAsync passed CancellationToken.None to StopCore (which then passes it to " + + "transport.Shutdown). Disposal must be bounded by an internal cancellation token so " + + "a stuck transport cannot hang shutdown indefinitely. " + + "See RunningEndpointInstance.DisposeAsync — `await StopCore()` is missing a token."); + } + } + + [Test] + public async Task DisposeAsync_should_complete_even_when_transport_Shutdown_hangs() + { + var hangingTransport = new HangingTransportInfrastructure(); + + var testInstance = new RunningEndpointInstance( + new SettingsHolder(), + CreateEmptyReceiveComponent(), + new FeatureComponent(new FeatureComponent.Settings()), + new TestableMessageSession(), + hangingTransport, + new CancellationTokenSource(), + NoOpAsyncDisposable.Instance, + new EndpointLogSlot("RunningEndpointInstanceTest", endpointIdentifier: null)); + + var dispose = testInstance.DisposeAsync().AsTask(); + var winner = await Task.WhenAny(dispose, Task.Delay(TimeSpan.FromSeconds(5))); + + Assert.That(winner, Is.SameAs(dispose), + "DisposeAsync did not complete within 5s of a 250ms internal timeout — " + + "the disposeShutdownTimeout must fire and let disposal proceed past a stuck transport.Shutdown."); + Assert.DoesNotThrowAsync(() => dispose, "DisposeAsync threw an exception when transport.Shutdown hung. DisposeAsync should complete successfully even if transport.Shutdown does not."); + } + [Test] public async Task ShouldThrowExceptionAfterInvokingStop() { @@ -61,4 +115,32 @@ public async Task ShouldThrowExceptionAfterInvokingStop() Assert.Throws(() => testInstance.Subscribe(typeof(object), new SubscribeOptions()), "Invoking messaging operations on the endpoint instance after it has been triggered to stop is not supported."); Assert.Throws(() => testInstance.Unsubscribe(typeof(object), new UnsubscribeOptions()), "Invoking messaging operations on the endpoint instance after it has been triggered to stop is not supported."); } + + // Empty ReceiveComponent (no receivers) so Stop is a no-op and StopCore reaches + // transport.Shutdown without us having to stand up the full receive pipeline. + static ReceiveComponent CreateEmptyReceiveComponent() => + new(configuration: null!, activityFactory: null!, endpointLogSlot: null!); + + sealed class TokenCapturingTransportInfrastructure : TransportInfrastructure + { + public bool ShutdownWasCalled { get; private set; } + public CancellationToken ObservedToken { get; private set; } + + public override Task Shutdown(CancellationToken cancellationToken = default) + { + ShutdownWasCalled = true; + ObservedToken = cancellationToken; + return Task.CompletedTask; + } + + public override string ToTransportAddress(QueueAddress address) => address.BaseAddress; + } + + sealed class HangingTransportInfrastructure : TransportInfrastructure + { + public override Task Shutdown(CancellationToken cancellationToken = default) => + Task.Delay(Timeout.Infinite, cancellationToken); + + public override string ToTransportAddress(QueueAddress address) => address.BaseAddress; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs index ca2b095c302..2a543a2979b 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/InspectTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Utils.Reflection; +#nullable enable + +namespace NServiceBus.Core.Utils.Reflection; using System; using NUnit.Framework; @@ -19,7 +21,7 @@ public void Should_return_property_name() public class Target { - public string Property { get; set; } + public required string Property { get; set; } } } @@ -36,7 +38,7 @@ public void Should_throw() public class Target { - public string Field; + public required string Field; } } @@ -61,12 +63,12 @@ public void Should_throw_when_dots_not_allowed() public class Target1 { - public Target2 Property1 { get; set; } + public required Target2 Property1 { get; set; } } public class Target2 { - public string Property2 { get; set; } + public required string Property2 { get; set; } } } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs index 04449dec318..a71350a394b 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/MethodInfoExtensionTests.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Core.Utils.Reflection; using System.Reflection; @@ -10,7 +12,7 @@ public class MethodInfoExtensionTests public void When_method_has_no_return_type_Should_return_null() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithNoReturnInfo.InvokeGeneric(instance, [typeof(InputType)]); + var result = TypeWithMethods.MethodWithNoReturnInfo!.InvokeGeneric(instance, [typeof(InputType)]); Assert.That(result, Is.Null); } @@ -19,7 +21,7 @@ public void When_method_has_no_return_type_Should_return_null() public void When_method_has_return_type_Should_return_value() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithReturnInfo.InvokeGeneric(instance, [typeof(InputType)]); + var result = TypeWithMethods.MethodWithReturnInfo!.InvokeGeneric(instance, [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -28,7 +30,7 @@ public void When_method_has_return_type_Should_return_value() public void When_method_has_return_type_Should_return_strong_typed_value() { var instance = new TypeWithMethods(); - var result = TypeWithMethods.MethodWithReturnInfo.InvokeGeneric(instance, [], [typeof(InputType)]); + var result = TypeWithMethods.MethodWithReturnInfo!.InvokeGeneric(instance, [], [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -36,7 +38,7 @@ public void When_method_has_return_type_Should_return_strong_typed_value() [Test] public void When_static_method_has_no_return_type_Should_return_null() { - var result = TypeWithMethods.StaticMethodWithNoReturnInfo.InvokeGeneric(null, [], [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithNoReturnInfo!.InvokeGeneric(null, [], [typeof(InputType)]); Assert.That(result, Is.Null); } @@ -44,7 +46,7 @@ public void When_static_method_has_no_return_type_Should_return_null() [Test] public void When_static_method_has_return_type_Should_return_value() { - var result = TypeWithMethods.StaticMethodWithReturnInfo.InvokeGeneric(null, [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithReturnInfo!.InvokeGeneric(null, [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -52,7 +54,7 @@ public void When_static_method_has_return_type_Should_return_value() [Test] public void When_static_method_has_return_type_Should_return_strong_typed_value() { - var result = TypeWithMethods.StaticMethodWithReturnInfo.InvokeGeneric(null, [], [typeof(InputType)]); + var result = TypeWithMethods.StaticMethodWithReturnInfo!.InvokeGeneric(null, [], [typeof(InputType)]); Assert.That(result, Is.EqualTo(42)); } @@ -70,9 +72,9 @@ public static void StaticMethodWithNoReturn() { } public static int StaticMethodWithReturn() => 42; - public static MethodInfo StaticMethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithNoReturn", BindingFlags.Public | BindingFlags.Static); - public static MethodInfo MethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithNoReturn", BindingFlags.Public | BindingFlags.Instance); - public static MethodInfo MethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithReturn", BindingFlags.Public | BindingFlags.Instance); - public static MethodInfo StaticMethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithReturn", BindingFlags.Public | BindingFlags.Static); + public static MethodInfo? StaticMethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithNoReturn", BindingFlags.Public | BindingFlags.Static); + public static MethodInfo? MethodWithNoReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithNoReturn", BindingFlags.Public | BindingFlags.Instance); + public static MethodInfo? MethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("MethodWithReturn", BindingFlags.Public | BindingFlags.Instance); + public static MethodInfo? StaticMethodWithReturnInfo = typeof(TypeWithMethods).GetMethod("StaticMethodWithReturn", BindingFlags.Public | BindingFlags.Static); } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs b/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs index 5283837b9ec..bb80fb7dee9 100644 --- a/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs +++ b/src/NServiceBus.Core.Tests/Utils/Reflection/TypeExtensionMethodsTests.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Core.Utils.Reflection; +#nullable enable + +namespace NServiceBus.Core.Utils.Reflection; using System; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/AppContextSwitches.cs b/src/NServiceBus.Core/AppContextSwitches.cs index 4f1173bbc70..107af814173 100644 --- a/src/NServiceBus.Core/AppContextSwitches.cs +++ b/src/NServiceBus.Core/AppContextSwitches.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Particular.Obsoletes; static class AppContextSwitches @@ -15,6 +16,7 @@ enum SwitchState : byte } static SwitchState cachedUseV2DeterministicGuid; + static SwitchState cachedStrictRegisteredOnlyMessageMetadata; [PreObsolete("https://github.com/Particular/NServiceBus/issues/7734", Note = "In v11, DeterministicGuid (XxHash128) becomes the default and this switch will be inverted so that setting it to false opts into the legacy MD5 algorithm. Both the switch and LegacyDeterministicGuid will be removed in v12.", @@ -46,4 +48,31 @@ public static bool UseV2DeterministicGuid } internal static void ResetUseV2DeterministicGuid() => cachedUseV2DeterministicGuid = SwitchState.Unchecked; + + // Emitted by the build-transitive NServiceBus.targets into the runtime configuration of executable applications + // that are published with trimming or AOT enabled. Users can force the value via the + // NServiceBusEnableStrictRegisteredOnlyMessageMetadata MSBuild property or by setting the switch in code. + public const string StrictRegisteredOnlyMessageMetadataSwitchName = "NServiceBus.EnableStrictRegisteredOnlyMessageMetadata"; + + [FeatureSwitchDefinition(StrictRegisteredOnlyMessageMetadataSwitchName)] + public static bool IsStrictRegisteredOnlyMessageMetadataEnabled + { + get + { + var state = cachedStrictRegisteredOnlyMessageMetadata; + if (state != SwitchState.Unchecked) + { + return state == SwitchState.Enabled; + } + + state = AppContext.TryGetSwitch(StrictRegisteredOnlyMessageMetadataSwitchName, out var isEnabled) && isEnabled + ? SwitchState.Enabled + : SwitchState.Disabled; + cachedStrictRegisteredOnlyMessageMetadata = state; + + return state == SwitchState.Enabled; + } + } + + internal static void ResetStrictRegisteredOnlyMessageMetadata() => cachedStrictRegisteredOnlyMessageMetadata = SwitchState.Unchecked; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Audit/Audit.cs b/src/NServiceBus.Core/Audit/Audit.cs index b160dab1f0a..5c845134090 100644 --- a/src/NServiceBus.Core/Audit/Audit.cs +++ b/src/NServiceBus.Core/Audit/Audit.cs @@ -38,12 +38,12 @@ protected override void Setup(FeatureConfigurationContext context) context.Settings.Get().BindSending(auditConfig.Address); - context.Settings.AddStartupDiagnosticsSection("Manifest-AuditQueue", auditConfig.Address); - context.Settings.AddStartupDiagnosticsSection("Audit", new + context.Settings.AddStartupDiagnosticsSection("Manifest-AuditQueue", auditConfig.Address, StartupDiagnosticsJsonContext.Default.String); + context.Settings.AddStartupDiagnosticsSection("Audit", new AuditDiagnostics { AuditQueue = auditConfig.Address, AuditTTBR = auditConfig.TimeToBeReceived?.ToString("g") ?? "-" - }); + }, StartupDiagnosticsJsonContext.Default.AuditDiagnostics); Logger.InfoFormat($"Auditing processed messages to '{auditConfig.Address}'"); } diff --git a/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs b/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs index e3042278606..845c99d9f80 100644 --- a/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs +++ b/src/NServiceBus.Core/Conventions/NServiceBusMarkerInterfaceConvention.cs @@ -16,23 +16,24 @@ public class NServiceBusMarkerInterfaceConvention : IMessageConvention public bool IsCommandType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(ICommand).IsAssignableFrom(type) && typeof(ICommand) != type; + return typeof(ICommand).IsAssignableFrom(type) && !IsMarkerType(type); } /// public bool IsEventType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(IEvent).IsAssignableFrom(type) && typeof(IEvent) != type; + return typeof(IEvent).IsAssignableFrom(type) && !IsMarkerType(type); } /// public bool IsMessageType(Type type) { ArgumentNullException.ThrowIfNull(type); - return typeof(IMessage).IsAssignableFrom(type) && - typeof(IMessage) != type && - typeof(IEvent) != type && - typeof(ICommand) != type; + return typeof(IMessage).IsAssignableFrom(type) && !IsMarkerType(type); } + + internal static bool IsMarkerType(Type type) => type == typeof(IMessage) || type == typeof(IEvent) || type == typeof(ICommand); + + internal static bool IsMarkerType(string typeName) => typeName == typeof(IMessage).FullName || typeName == typeof(IEvent).FullName || typeName == typeof(ICommand).FullName; } \ No newline at end of file diff --git a/src/NServiceBus.Core/DateTimeOffsetHelper.cs b/src/NServiceBus.Core/DateTimeOffsetHelper.cs index 9152aa795b3..3dcfee01b6a 100644 --- a/src/NServiceBus.Core/DateTimeOffsetHelper.cs +++ b/src/NServiceBus.Core/DateTimeOffsetHelper.cs @@ -1,7 +1,6 @@ namespace NServiceBus; using System; -using System.Globalization; /// /// Common date time extensions. @@ -13,9 +12,57 @@ public static class DateTimeOffsetHelper /// public static string ToWireFormattedString(DateTimeOffset dateTime) { - return dateTime.ToUniversalTime().ToString(format, CultureInfo.InvariantCulture); + // Formatted by hand rather than via ToString(format): a custom format string is reparsed on every + // call, and that parsing dominates the cost. The layout here is fixed and ASCII, so the digits can be + // written straight into the string buffer instead. This mirrors how the framework formats its own + // standard date formats (see DateTimeFormat.TryFormatO), including writing each two digit field from + // a lookup table rather than dividing per digit. ToDateTimeOffset below already parses by hand for + // the same reason. + return string.Create(FormatLength, dateTime.ToUniversalTime(), static (destination, utc) => + { + var (yearHigh, yearLow) = Math.DivRem((uint)utc.Year, 100); + WriteTwoDigits(destination[..2], yearHigh); + WriteTwoDigits(destination.Slice(2, 2), yearLow); + destination[4] = '-'; + WriteTwoDigits(destination.Slice(5, 2), (uint)utc.Month); + destination[7] = '-'; + WriteTwoDigits(destination.Slice(8, 2), (uint)utc.Day); + destination[10] = ' '; + WriteTwoDigits(destination.Slice(11, 2), (uint)utc.Hour); + destination[13] = ':'; + WriteTwoDigits(destination.Slice(14, 2), (uint)utc.Minute); + destination[16] = ':'; + WriteTwoDigits(destination.Slice(17, 2), (uint)utc.Second); + destination[19] = ':'; + + var microseconds = (uint)(utc.Ticks % TimeSpan.TicksPerSecond / 10); + var (microHigh, microRest) = Math.DivRem(microseconds, 10000); + var (microMid, microLow) = Math.DivRem(microRest, 100); + WriteTwoDigits(destination.Slice(20, 2), microHigh); + WriteTwoDigits(destination.Slice(22, 2), microMid); + WriteTwoDigits(destination.Slice(24, 2), microLow); + + destination[26] = ' '; + destination[27] = 'Z'; + }); + + // Both characters come from the table in one copy, which the JIT turns into a single 4 byte move. + static void WriteTwoDigits(Span destination, uint value) => TwoDigits.Slice((int)value * 2, 2).CopyTo(destination); } + // Every two digit value from "00" to "99", indexed by value * 2. Spanning a literal allocates nothing. + static ReadOnlySpan TwoDigits => + "00010203040506070809" + + "10111213141516171819" + + "20212223242526272829" + + "30313233343536373839" + + "40414243444546474849" + + "50515253545556575859" + + "60616263646566676869" + + "70717273747576777879" + + "80818283848586878889" + + "90919293949596979899"; + /// /// Converts a wire formatted from to a UTC /// . @@ -117,5 +164,6 @@ public static DateTimeOffset ToDateTimeOffset(string wireFormattedString) } const string format = "yyyy-MM-dd HH:mm:ss:ffffff Z"; + const int FormatLength = 28; // format.Length, as a constant usable by string.Create const string errorMessage = "String was not recognized as a valid DateTime."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/DynamicMemberTypeAccess.cs b/src/NServiceBus.Core/DynamicMemberTypeAccess.cs index 4e3691e05f4..e356e741e92 100644 --- a/src/NServiceBus.Core/DynamicMemberTypeAccess.cs +++ b/src/NServiceBus.Core/DynamicMemberTypeAccess.cs @@ -4,6 +4,11 @@ namespace NServiceBus; static class DynamicMemberTypeAccess { + public const DynamicallyAccessedMemberTypes Message = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; + public const DynamicallyAccessedMemberTypes SagaData = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties; diff --git a/src/NServiceBus.Core/EndpointConfiguration.cs b/src/NServiceBus.Core/EndpointConfiguration.cs index b19c25d4948..c6de8f9959c 100644 --- a/src/NServiceBus.Core/EndpointConfiguration.cs +++ b/src/NServiceBus.Core/EndpointConfiguration.cs @@ -4,9 +4,12 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Transactions; using Configuration.AdvancedExtensibility; using Features; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; using Microsoft.Extensions.DependencyInjection; using Particular.Obsoletes; using Pipeline; @@ -33,6 +36,8 @@ public EndpointConfiguration(string endpointName) Settings.SetDefault("Transactions.IsolationLevel", IsolationLevel.ReadCommitted); Settings.SetDefault("Transactions.DefaultTimeout", TransactionManager.DefaultTimeout); + Settings.SetDefault(RuntimeFeature.IsDynamicCodeSupported ? CreateDynamicCodeMessageMapper() : new TrimmingSafeMessageMapper()); + Settings.Set(new AssemblyScanningComponent.Configuration(Settings)); Settings.Set(new HostingComponent.Settings(Settings)); Settings.Set(new InstallerComponent.Settings(Settings)); @@ -123,7 +128,7 @@ internal void FinalizeConfiguration(IList availableTypes) InvokeDiscoveredInitializers(availableTypes); } - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = EndpointCreator.TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = EndpointCreator.SuppressJustification)] void InvokeDiscoveredInitializers(IList availableTypes) { ActivateAndInvoke(availableTypes, t => t.Customize(this)); @@ -168,4 +173,10 @@ static void ActivateAndInvoke(IList types, Action action) where T : static bool HasDefaultConstructor([DynamicallyAccessedMembers(DynamicMemberTypeAccess.InitializationExtension)] Type type) => type.GetConstructor(Type.EmptyTypes) != null; } + + const string DynamicCodeMessageMapperSuppressJustification = "The MessageMapper relies on System.Reflection.Emit to generate interface proxies, which is only constructed when RuntimeFeature.IsDynamicCodeSupported is true, so it is unreachable when the application is trimmed or published as NativeAOT."; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = DynamicCodeMessageMapperSuppressJustification)] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL3050", Justification = DynamicCodeMessageMapperSuppressJustification)] + static MessageMapper CreateDynamicCodeMessageMapper() => new(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/EndpointCreator.cs b/src/NServiceBus.Core/EndpointCreator.cs index 627887e0ff7..9cbd8239550 100644 --- a/src/NServiceBus.Core/EndpointCreator.cs +++ b/src/NServiceBus.Core/EndpointCreator.cs @@ -7,7 +7,6 @@ namespace NServiceBus; using Features; using Logging; using MessageInterfaces; -using MessageInterfaces.MessageMapper.Reflection; using Microsoft.Extensions.DependencyInjection; using Pipeline; using Settings; @@ -34,6 +33,9 @@ public static EndpointCreator Create(EndpointConfiguration endpointConfiguration var assemblyScanningComponent = AssemblyScanningComponent.Initialize(assemblyScanningConfiguration, settings); assemblyScanningConfiguration.SetDefaultAvailableTypes(assemblyScanningComponent.AvailableTypes); + // The component is the authority on strict registered-only mode: it is only enabled when scanning is disabled + // AND the application is trimmed or dynamic code is unavailable. + assemblyScanningConfiguration.StrictRegisteredOnlyMode = assemblyScanningComponent.IsStrictRegisteredOnlyMode; endpointConfiguration.FinalizeConfiguration(assemblyScanningComponent.AvailableTypes); @@ -59,7 +61,7 @@ public static EndpointCreator Create(EndpointConfiguration endpointConfiguration return endpointCreator; - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] static void DiscoverInstallers(InstallerComponent.Settings installerSettings, List availableTypes) => installerSettings.AddScannedInstallers(availableTypes); } @@ -116,9 +118,6 @@ void Configure() var routingConfiguration = RoutingComponent.Configure(settings.Get()); - var messageMapper = new MessageMapper(); - settings.Set(messageMapper); - recoverabilityComponent = new RecoverabilityComponent(settings); var sagaSettings = settings.Get(); @@ -146,7 +145,7 @@ void Configure() pipelineSettings, hostingConfiguration); - sendComponent = SendComponent.Initialize(pipelineSettings, hostingConfiguration, routingComponent, messageMapper); + sendComponent = SendComponent.Initialize(pipelineSettings, hostingConfiguration, routingComponent, settings.Get()); envelopeComponent = new EnvelopeComponent(settings.Get()); @@ -167,12 +166,13 @@ void Configure() pipelineSettings.PreventChanges(); settings.AddStartupDiagnosticsSection("Endpoint", - new + new EndpointDiagnostics { Name = settings.EndpointName(), SendOnly = settings.Get("Endpoint.SendOnly"), NServiceBusVersion = VersionInformation.MajorMinorPatch - } + }, + StartupDiagnosticsJsonContext.Default.EndpointDiagnostics ); // Make Metrics a first class citizen in Core by enabling once and for all them when creating the endpoint @@ -181,20 +181,27 @@ void Configure() hostingComponent = HostingComponent.Initialize(hostingConfiguration); MessageSession = new MessageSession(hostingConfiguration.EndpointLogSlot); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = SuppressJustification)] static void DiscoverHandlers(ReceiveComponent.Settings receiveSettings, ICollection availableTypes) => receiveSettings.MessageHandlerRegistry.AddScannedHandlers(availableTypes); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = SuppressJustification)] static void DiscoverSagas(SagaComponent.Settings sagaSettings, ICollection availableTypes) => sagaSettings.AddDiscoveredSagas(availableTypes); - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingSuppressJustification)] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = SuppressJustification)] static void DiscoverFeatures(ICollection availableTypes, FeatureComponent.Settings featureSettings) => featureSettings.AddScannedTypes(availableTypes); } void ConfigureMessageTypes(IEnumerable messageTypesHandled) { - var allowDynamicTypeLoading = settings.IsDynamicTypeLoadingEnabled(); + var configuredDynamicTypeLoading = settings.IsDynamicTypeLoadingEnabled(); + var strictMode = settings.Get().StrictRegisteredOnlyMode; var messageMetadataRegistry = settings.GetOrCreate(); + // Strict mode is the stronger non-overridable policy: it must be in effect before Initialize so + // pre-initialization registrations are enforced against it, and it disables dynamic type loading. + var allowDynamicTypeLoading = configuredDynamicTypeLoading && !strictMode; + messageMetadataRegistry.StrictRegisteredOnlyMode = strictMode; messageMetadataRegistry.Initialize(conventions.IsMessageType, allowDynamicTypeLoading); messageMetadataRegistry.RegisterMessageTypes(hostingConfiguration.AvailableTypes); @@ -202,14 +209,14 @@ void ConfigureMessageTypes(IEnumerable messageTypesHandled) var foundMessages = messageMetadataRegistry.GetAllMessages(); - settings.AddStartupDiagnosticsSection("Messages", new + settings.AddStartupDiagnosticsSection("Messages", new MessagesDiagnostics { CustomConventionUsed = conventions.CustomMessageTypeConventionUsed, MessageConventions = conventions.RegisteredConventions, NumberOfMessagesFoundAtStartup = foundMessages.Length, - Messages = foundMessages.Select(m => m.MessageType.FullName), + Messages = foundMessages.Select(m => m.MessageType.FullName).ToArray(), AllowDynamicTypeLoading = allowDynamicTypeLoading - }); + }, StartupDiagnosticsJsonContext.Default.MessagesDiagnostics); } internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvider, string containerType, IAsyncDisposable serviceProviderLease) @@ -218,7 +225,7 @@ internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvi ArgumentNullException.ThrowIfNull(containerType); ArgumentNullException.ThrowIfNull(serviceProviderLease); - hostingConfiguration.AddStartupDiagnosticsSection("Container", new { Type = containerType }); + hostingConfiguration.AddStartupDiagnosticsSection("Container", new ContainerDiagnostics { Type = containerType }, StartupDiagnosticsJsonContext.Default.ContainerDiagnostics); return new StartableEndpoint(settings, featureComponent, @@ -250,5 +257,5 @@ internal StartableEndpoint CreateStartableEndpoint(IServiceProvider serviceProvi readonly HostingComponent.Configuration hostingConfiguration; readonly Conventions conventions; - internal const string TrimmingSuppressJustification = "The assembly scanning component has a guard that prevents it from being used when dynamic code is not available so we can safely call this."; + internal const string SuppressJustification = "The assembly scanning component has a guard that prevents it from being used when dynamic code is not available so we can safely call this."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs b/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs index f6a2cb5d887..023cd595aaa 100644 --- a/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs +++ b/src/NServiceBus.Core/Envelopes/EnvelopeComponent.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.Extensions.DependencyInjection; @@ -16,7 +17,7 @@ public class Settings { readonly Dictionary> factories = []; - public void AddEnvelopeHandler() where THandler : IEnvelopeHandler + public void AddEnvelopeHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : IEnvelopeHandler { if (factories.ContainsKey(typeof(THandler))) { diff --git a/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs b/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs index a1a0906f4b5..d15178013d6 100644 --- a/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs +++ b/src/NServiceBus.Core/Envelopes/EnvelopeConfigExtensions.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Features; /// @@ -15,7 +16,7 @@ public static class EnvelopeConfigExtensions /// /// Adds the envelope handler type. /// - public void AddEnvelopeHandler() where THandler : class, IEnvelopeHandler + public void AddEnvelopeHandler<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>() where THandler : class, IEnvelopeHandler { ArgumentNullException.ThrowIfNull(context); diff --git a/src/NServiceBus.Core/Extensibility/ContextBag.cs b/src/NServiceBus.Core/Extensibility/ContextBag.cs index 968e9b4a87d..2a43718a02b 100644 --- a/src/NServiceBus.Core/Extensibility/ContextBag.cs +++ b/src/NServiceBus.Core/Extensibility/ContextBag.cs @@ -5,6 +5,7 @@ namespace NServiceBus.Extensibility; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -48,6 +49,18 @@ public ContextBag(ContextBag? parentBag = null) public bool TryGet(string key, [NotNullWhen(true)] out T? result) { ArgumentException.ThrowIfNullOrWhiteSpace(key); + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + result = (T)slot.Value!; + return true; + } + } + if (stash?.TryGetValue(key, out var value) == true) { result = (T)value; @@ -116,6 +129,29 @@ public T Get(string key) public void Remove(string key) { ArgumentException.ThrowIfNullOrWhiteSpace(key); + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + // The number of slots in use decreases by one + count--; + + // If we're abandoning the last slot, in-use slots are still contiguous (order doesn't matter) + if (i != count) + { + // If removing from the begining/middle, swap the last slot into the hole + slots[i] = slots[count]; + } + + // Then clear out the (previously) last slot + slots[count] = default; + return; + } + } + _ = stash?.Remove(key); } @@ -127,7 +163,32 @@ public void Set(string key, T t) ArgumentException.ThrowIfNullOrWhiteSpace(key); ArgumentNullException.ThrowIfNull(t); - GetOrCreateStash()[key] = t; + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = t; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = t }; + count++; + return; + } + + var s = stash; + if (s is null) + { + s = []; + stash = s; + } + + s[key] = t; } /// @@ -151,23 +212,90 @@ internal void SetOnRoot(string key, T t) /// The source context. internal void Merge(ContextBag context) { - if (context.stash == null) + if (count == 0 && stash is null && context.stash is null) + { + var sourceCount = context.count; + + for (int i = 0; i < sourceCount; i++) + { + slots[i] = context.slots[i]; + } + + count = sourceCount; + return; + } + + for (int i = 0; i < context.count; i++) + { + ref var sourceSlot = ref context.slots[i]; + SetInlineOrStash(sourceSlot.Key!, sourceSlot.Value!); + } + + var sourceStash = context.stash; + if (sourceStash is null) + { + return; + } + + if (count == InlineArrayLength) { + var targetStash = stash ??= []; + + foreach (var kvp in sourceStash) + { + targetStash[kvp.Key] = kvp.Value; + } + return; } - var targetStash = GetOrCreateStash(); - foreach (var kvp in context.stash) + foreach (var kvp in sourceStash) + { + SetInlineOrStash(kvp.Key, kvp.Value); + } + + return; + + void SetInlineOrStash(string key, object value) { - targetStash[kvp.Key] = kvp.Value; + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = value; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot + { + Key = key, + Value = value + }; + count++; + return; + } + + (stash ??= [])[key] = value; } } - Dictionary GetOrCreateStash() + /// + /// Removes all entries from the context. + /// + internal void Clear() { - stash ??= []; + for (int i = 0; i < count; i++) + { + slots[i] = default; + } - return stash; + count = 0; + stash?.Clear(); } internal Func Invoker { get; set; } @@ -176,5 +304,26 @@ Dictionary GetOrCreateStash() private protected ContextBag root; + SlotArray slots; + int count; Dictionary? stash; + + // The number of slots that can be stored inline in the struct before falling back to a dictionary. + // This number was carefully chosen after benchmarking and analyzing typical usage patterns of the + // context bag in the main and recoverability pipelines. + // The goal was to minimize allocations while keeping lookups efficient for the most common cases. + // Do not blindly change this. + const int InlineArrayLength = 8; + + struct Slot + { + public string? Key; + public object? Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Features/FeatureComponent.cs b/src/NServiceBus.Core/Features/FeatureComponent.cs index 30d4cd9edbe..1f1855efe4c 100644 --- a/src/NServiceBus.Core/Features/FeatureComponent.cs +++ b/src/NServiceBus.Core/Features/FeatureComponent.cs @@ -17,7 +17,7 @@ public void Initialize(FeatureConfigurationContext featureConfigurationContext, { var featureStats = SetupFeatures(featureConfigurationContext, settings); - settings.AddStartupDiagnosticsSection("Features", featureStats); + settings.AddStartupDiagnosticsSection("Features", featureStats, StartupDiagnosticsJsonContext.Default.FeatureDiagnosticDataArray); } public FeatureDiagnosticData[] SetupFeatures(FeatureConfigurationContext featureConfigurationContext, SettingsHolder settings) diff --git a/src/NServiceBus.Core/Features/FeatureFactory.cs b/src/NServiceBus.Core/Features/FeatureFactory.cs index c60e2fc380c..404618d9554 100644 --- a/src/NServiceBus.Core/Features/FeatureFactory.cs +++ b/src/NServiceBus.Core/Features/FeatureFactory.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Features; using System; diff --git a/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs b/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs index f51783acd40..c41efed38de 100644 --- a/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs +++ b/src/NServiceBus.Core/Features/IFeatureStartupTaskController.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Features; using System; diff --git a/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs b/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs index be777e9dfa7..4b706cdcea2 100644 --- a/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs +++ b/src/NServiceBus.Core/Hosting/AddHostInfoHeadersBehavior.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs b/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs index f4a1d3545fb..7d6ba19794c 100644 --- a/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs +++ b/src/NServiceBus.Core/Hosting/AuditHostInformationBehavior.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs b/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs index f31b2035fe2..13e42160e77 100644 --- a/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs +++ b/src/NServiceBus.Core/Hosting/ExternallyManagedContainerHost.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs index 56dd3c0c41a..cd90b3dfbad 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanner.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs index 0300179bbb8..588f041b5c8 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScannerResults.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs index 5d78ac01362..f5887561e9d 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningComponent.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -14,7 +15,7 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, { if (configuration.AssemblyScannerConfiguration.Disable) { - return new AssemblyScanningComponent([]); + return new AssemblyScanningComponent([], configuration.StrictRegisteredOnlyMode); } if (!configuration.DynamicCodeSupported) @@ -39,13 +40,13 @@ public static AssemblyScanningComponent Initialize(Configuration configuration, // Deliberately strongly typed because we need to truncate this super large section when writing to the logs var assemblyScanningDiagnostics = new AssemblyScanningDiagnostics( - scannableAssemblies.Assemblies.Select(a => new AssemblyDetails(a.FullName, FileVersionRetriever.GetFileVersion(a))), + scannableAssemblies.Assemblies.Select(a => new AssemblyDetails(a.FullName ?? "Unknown assembly", FileVersionRetriever.GetFileVersion(a))), scannableAssemblies.SkippedFiles.Select(f => new SkippedFile(f.FilePath, f.SkipReason)), scannableAssemblies.ErrorsThrownDuringScanning, assemblyScannerSettings ); - settings.AddStartupDiagnosticsSection(AssemblyScanningDiagnostics.SectionName, assemblyScanningDiagnostics); + settings.AddStartupDiagnosticsSection(AssemblyScanningDiagnostics.SectionName, assemblyScanningDiagnostics, StartupDiagnosticsJsonContext.Default.AssemblyScanningDiagnostics); return new AssemblyScanningComponent(availableTypes); } @@ -69,15 +70,42 @@ static AssemblyScannerResults ScanAssemblies(AssemblyScannerConfiguration assemb return assemblyScanner.GetScannableAssemblies(); } - AssemblyScanningComponent(IList availableTypes) => AvailableTypes = availableTypes; + AssemblyScanningComponent(IList availableTypes, bool isStrictRegisteredOnlyMode = false) + { + AvailableTypes = availableTypes; + IsStrictRegisteredOnlyMode = isStrictRegisteredOnlyMode; + } public IList AvailableTypes { get; } + /// + /// When assembly scanning is disabled and the application is trimmed or dynamic code is unavailable, the endpoint + /// operates in strict registered-only message metadata mode: metadata is only resolved for types registered up + /// front (e.g. via the source-generated AddMessageType<T> registration) and no runtime registration or + /// dynamic type loading is performed. + /// + public bool IsStrictRegisteredOnlyMode { get; } + public class Configuration(SettingsHolder settings) { - // This is only used for testability until we have a dedicated AOT test project. Not called on the hot path so has no performance implications. + // Testability hook until a dedicated AOT test project exists. public bool DynamicCodeSupported { get; set; } = System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported; + // Mirrors the NServiceBus.EnableStrictRegisteredOnlyMessageMetadata runtime switch emitted by the + // build-transitive NServiceBus.targets for trimmed/AOT executables. Testability only; production code + // never assigns to it. + public bool StrictRegisteredOnlyMessageMetadataEnabled { get; set; } = AppContextSwitches.IsStrictRegisteredOnlyMessageMetadataEnabled; + + // The computed default only matters when scanning is disabled; EndpointCreator overwrites it with the + // component's decision after Initialize has run. + public bool StrictRegisteredOnlyMode + { + get => strictRegisteredOnlyMode ?? (StrictRegisteredOnlyMessageMetadataEnabled || !DynamicCodeSupported); + set => strictRegisteredOnlyMode = value; + } + + bool? strictRegisteredOnlyMode; + public List? UserProvidedTypes { get; set; } public AssemblyScannerConfiguration AssemblyScannerConfiguration => settings.GetOrCreate(); diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs index fc04c2e4caf..223233690f7 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyScanningDiagnostics.cs @@ -1,28 +1,23 @@ +#nullable enable + namespace NServiceBus; using System.Collections.Generic; using Hosting.Helpers; -sealed class AssemblyScanningDiagnostics( - IEnumerable assemblies, - IEnumerable skippedFiles, - bool errorsThrownDuringScanning, - AssemblyScannerConfiguration settings) +sealed class AssemblyScanningDiagnostics(IEnumerable assemblies, IEnumerable skippedFiles, bool errorsThrownDuringScanning, AssemblyScannerConfiguration settings) { public const string SectionName = "AssemblyScanning"; public IEnumerable Assemblies { get; } = assemblies; + public IEnumerable SkippedFiles { get; } = skippedFiles; + public bool ErrorsThrownDuringScanning { get; } = errorsThrownDuringScanning; + public AssemblyScannerConfiguration Settings { get; } = settings; - public AssemblyScanningDiagnostics CreateCompactedVersion() => - new( - [], - [], - ErrorsThrownDuringScanning, - Settings - ); + public AssemblyScanningDiagnostics CreateCompactedVersion() => new([], [], ErrorsThrownDuringScanning, Settings); } record AssemblyDetails(string FullName, string FileVersion); \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs index a65598f11f2..611532597c2 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/AssemblyValidator.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs b/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs index 96e70d0c18c..409276d13de 100644 --- a/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs +++ b/src/NServiceBus.Core/Hosting/Helpers/SkippedFile.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting.Helpers; /// diff --git a/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs b/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs index 81666aff691..9461a6b7b72 100644 --- a/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Hosting/HostInfoConfigurationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostInfoSettings.cs b/src/NServiceBus.Core/Hosting/HostInfoSettings.cs index e1e2f9a3aea..6740e4204d3 100644 --- a/src/NServiceBus.Core/Hosting/HostInfoSettings.cs +++ b/src/NServiceBus.Core/Hosting/HostInfoSettings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostInformation.cs b/src/NServiceBus.Core/Hosting/HostInformation.cs index a290d8b5fa2..a63f52afc2a 100644 --- a/src/NServiceBus.Core/Hosting/HostInformation.cs +++ b/src/NServiceBus.Core/Hosting/HostInformation.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Hosting; using System; diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs index 6326b1a5006..9895a21821b 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.Configuration.cs @@ -1,8 +1,10 @@ #nullable enable + namespace NServiceBus; using System; using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Hosting; @@ -89,7 +91,9 @@ internal Configuration(Settings settings, public string? DiagnosticsPath { get; } - public void AddStartupDiagnosticsSection(string sectionName, object section) => StartupDiagnostics.Add(sectionName, section); + public void AddStartupDiagnosticsSection(string sectionName, T section, JsonTypeInfo typeInfo) => StartupDiagnostics.Add(sectionName, section, typeInfo); + + public void AddStartupDiagnosticsSectionFactory(string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) => StartupDiagnostics.AddFactory(sectionName, sectionFactory, typeInfo); public HostInformation HostInformation { get; } diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs b/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs index d2b8bdb4027..624746407a2 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.Settings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/HostingComponent.cs b/src/NServiceBus.Core/Hosting/HostingComponent.cs index f79dafb60cc..8b0e44cd95f 100644 --- a/src/NServiceBus.Core/Hosting/HostingComponent.cs +++ b/src/NServiceBus.Core/Hosting/HostingComponent.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -40,25 +41,25 @@ public static HostingComponent Initialize(Configuration configuration) registration(serviceCollection); } - configuration.AddStartupDiagnosticsSection("Hosting", new + configuration.AddStartupDiagnosticsSection("Hosting", new HostingDiagnostics { - configuration.HostInformation.HostId, + HostId = configuration.HostInformation.HostId, HostDisplayName = configuration.HostInformation.DisplayName, - RuntimeEnvironment.MachineName, + MachineName = RuntimeEnvironment.MachineName, OSPlatform = Environment.OSVersion.Platform, OSVersion = Environment.OSVersion.VersionString, - GCSettings.IsServerGC, + IsServerGC = GCSettings.IsServerGC, GCLatencyMode = GCSettings.LatencyMode, - Environment.ProcessorCount, - Environment.Is64BitProcess, + ProcessorCount = Environment.ProcessorCount, + Is64BitProcess = Environment.Is64BitProcess, CLRVersion = Environment.Version, - Environment.WorkingSet, - Environment.SystemPageSize, + WorkingSet = Environment.WorkingSet, + SystemPageSize = Environment.SystemPageSize, HostName = Dns.GetHostName(), - Environment.UserName, + UserName = Environment.UserName, PathToExe = PathUtilities.SanitizedPath(Environment.CommandLine), InstallersEnabled = configuration.ShouldRunInstallers - }); + }, StartupDiagnosticsJsonContext.Default.HostingDiagnostics); return new HostingComponent(configuration, configuration.InstallerComponent); } diff --git a/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs b/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs index 161261be5c8..3cba9b26f88 100644 --- a/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs +++ b/src/NServiceBus.Core/Hosting/InternallyManagedContainerHost.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs index 1caf73a2f80..f48f6e5a3a4 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceCollectionAdapter.cs @@ -6,6 +6,7 @@ namespace NServiceBus; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; @@ -53,7 +54,6 @@ public void Clear() keyedDescriptors.Clear(); originalDescriptors.Clear(); - serviceTypeCounts.Clear(); } public bool Contains(ServiceDescriptor item) @@ -90,7 +90,6 @@ public bool Remove(ServiceDescriptor item) originalDescriptors.RemoveAt(index); keyedDescriptors.RemoveAt(index); _ = Inner.Remove(keyedDescriptor); - DecrementServiceTypeCount(keyedDescriptor.ServiceType); return true; } @@ -100,29 +99,39 @@ public void RemoveAt(int index) keyedDescriptors.RemoveAt(index); originalDescriptors.RemoveAt(index); _ = Inner.Remove(keyedDescriptor); - DecrementServiceTypeCount(keyedDescriptor.ServiceType); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public bool ContainsService(Type serviceType) + public bool ContainsLocalService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); - if (serviceTypeCounts.ContainsKey(serviceType)) + foreach (var descriptor in originalDescriptors) { - return true; + if (ServiceTypeMatches(descriptor.ServiceType, serviceType) && Equals(GetServiceKey(descriptor), serviceKey)) + { + return true; + } } - if (!serviceType.IsGenericType) + return false; + } + + public KeyedServiceKey GetLocalServiceKey(object? serviceKey) => new(ServiceKey, serviceKey); + + static bool ServiceTypeMatches(Type registeredServiceType, Type requestedServiceType) + { + if (registeredServiceType == requestedServiceType) { - return false; + return true; } - var definition = serviceType.GetGenericTypeDefinition(); - return serviceTypeCounts.ContainsKey(definition); + return requestedServiceType.IsGenericType && registeredServiceType == requestedServiceType.GetGenericTypeDefinition(); } + static object? GetServiceKey(ServiceDescriptor descriptor) => descriptor.IsKeyedService ? descriptor.ServiceKey : null; + ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) { ServiceDescriptor keyedDescriptor; @@ -149,7 +158,7 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) var resultingKey = key is null ? ServiceKey : key as KeyedServiceKey ?? new KeyedServiceKey(key); var keyedProvider = new KeyedServiceProviderAdapter(serviceProvider, resultingKey, this); return descriptor.Lifetime == ServiceLifetime.Singleton ? ActivatorUtilities.CreateInstance(keyedProvider, descriptor.KeyedImplementationType) : - factories.GetOrAdd(descriptor.KeyedImplementationType, type => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes))(keyedProvider, []); + factories.GetOrAdd(new TypeKey { Type = descriptor.KeyedImplementationType }, static typeKey => ActivatorUtilities.CreateFactory(typeKey.Type, Type.EmptyTypes))(keyedProvider, []); }, descriptor.Lifetime); UnsafeAccessor.GetImplementationType(keyedDescriptor) = descriptor.KeyedImplementationType; } @@ -181,7 +190,7 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) var resultingKey = key is null ? ServiceKey : key as KeyedServiceKey ?? new KeyedServiceKey(key); var keyedProvider = new KeyedServiceProviderAdapter(serviceProvider, resultingKey, this); return descriptor.Lifetime == ServiceLifetime.Singleton ? ActivatorUtilities.CreateInstance(keyedProvider, descriptor.ImplementationType) : - factories.GetOrAdd(descriptor.ImplementationType, type => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes))(keyedProvider, []); + factories.GetOrAdd(new TypeKey { Type = descriptor.ImplementationType }, static typeKey => ActivatorUtilities.CreateFactory(typeKey.Type, Type.EmptyTypes))(keyedProvider, []); }, descriptor.Lifetime); UnsafeAccessor.GetImplementationType(keyedDescriptor) = descriptor.ImplementationType; } @@ -191,38 +200,22 @@ ServiceDescriptor EnsureKeyedDescriptor(ServiceDescriptor descriptor) } } - if (!serviceTypeCounts.TryAdd(keyedDescriptor.ServiceType, 1)) - { - serviceTypeCounts[keyedDescriptor.ServiceType]++; - } - return keyedDescriptor; } - void DecrementServiceTypeCount(Type serviceType) - { - if (!serviceTypeCounts.TryGetValue(serviceType, out var count)) - { - return; - } - - if (count <= 1) - { - _ = serviceTypeCounts.Remove(serviceType); - return; - } - - serviceTypeCounts[serviceType] = count - 1; - } - static class UnsafeAccessor { [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_implementationType")] public static extern ref Type GetImplementationType(ServiceDescriptor descriptor); } + readonly record struct TypeKey + { + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + public Type Type { get; init; } + } + readonly List originalDescriptors = []; readonly List keyedDescriptors = []; - readonly Dictionary serviceTypeCounts = []; - readonly ConcurrentDictionary factories = new(); + readonly ConcurrentDictionary factories = new(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs index 14f8372b62c..1502a927e64 100644 --- a/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs +++ b/src/NServiceBus.Core/Hosting/KeyedServices/KeyedServiceProviderAdapter.cs @@ -4,7 +4,9 @@ namespace NServiceBus; using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,23 +29,25 @@ public KeyedServiceProviderAdapter(IServiceProvider serviceProvider, object serv keyedScopeFactory = new KeyedServiceScopeFactory(serviceProvider.GetRequiredService(), serviceKeyedServiceKey, serviceCollection); } - public bool IsService(Type serviceType) => serviceCollection.ContainsService(serviceType); - - public bool IsKeyedService(Type serviceType, object? serviceKey) + public bool IsService(Type serviceType) { - if (!serviceCollection.ContainsService(serviceType)) - { - return false; - } + ArgumentNullException.ThrowIfNull(serviceType); - if (serviceKey is KeyedServiceKey key) + if (IsServiceProvider(serviceType) || IsScopeFactory(serviceType) || IsServicesRequest(serviceType)) { - return Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey); + return true; } - return false; + return ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey) || ContainsRootEndpointKeyedService(serviceType) || ContainsRootService(serviceType); + } + + public bool IsKeyedService(Type serviceType, object? serviceKey) + { + var computedKey = GetOrCreateComputedKey(serviceKey); + return ContainsLocalEndpointService(serviceType, computedKey) || ContainsRootKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object? GetService(Type serviceType) { ArgumentNullException.ThrowIfNull(serviceType); @@ -61,15 +65,24 @@ public bool IsKeyedService(Type serviceType, object? serviceKey) if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, serviceKeyedServiceKey) - ? serviceProvider.GetKeyedService(serviceType, serviceKeyedServiceKey) - : serviceProvider.GetService(serviceType); + if (ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedService(serviceType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(serviceType) ? serviceProvider.GetKeyedService(serviceType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetService(serviceType); } var itemType = serviceType.GetGenericArguments()[0]; - return IsKeyedService(itemType, serviceKeyedServiceKey) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey) : serviceProvider.GetServices(itemType); + if (ContainsLocalEndpointService(itemType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object GetRequiredService(Type serviceType) { ArgumentNullException.ThrowIfNull(serviceType); @@ -87,15 +100,24 @@ public object GetRequiredService(Type serviceType) if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, serviceKeyedServiceKey) - ? serviceProvider.GetRequiredKeyedService(serviceType, serviceKeyedServiceKey) - : serviceProvider.GetRequiredService(serviceType); + if (ContainsLocalEndpointService(serviceType, serviceKeyedServiceKey)) + { + return serviceProvider.GetRequiredKeyedService(serviceType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(serviceType) ? serviceProvider.GetRequiredKeyedService(serviceType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetRequiredService(serviceType); } var itemType = serviceType.GetGenericArguments()[0]; - return IsKeyedService(itemType, serviceKeyedServiceKey) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey) : serviceProvider.GetServices(itemType); + if (ContainsLocalEndpointService(itemType, serviceKeyedServiceKey)) + { + return serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(serviceKeyedServiceKey)); + } + + return ContainsRootEndpointKeyedService(itemType) ? serviceProvider.GetKeyedServices(itemType, serviceKeyedServiceKey.BaseKey) : serviceProvider.GetServices(itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object? GetKeyedService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); @@ -114,22 +136,23 @@ public object GetRequiredService(Type serviceType) var computedKey = GetOrCreateComputedKey(serviceKey); if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, computedKey) - ? serviceProvider.GetKeyedService(serviceType, computedKey) + return ContainsLocalEndpointService(serviceType, computedKey) + ? serviceProvider.GetKeyedService(serviceType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } var itemType = serviceType.GetGenericArguments()[0]; if (!Equals(computedKey, anyKey)) { - return IsKeyedService(itemType, computedKey) - ? serviceProvider.GetKeyedServices(itemType, computedKey) + return ContainsLocalEndpointService(itemType, computedKey) + ? serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedServices(itemType, GetBaseKeyOrServiceKey(serviceKey)); } return GetAllServices(serviceProvider, itemType); } + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] public object GetRequiredKeyedService(Type serviceType, object? serviceKey) { ArgumentNullException.ThrowIfNull(serviceType); @@ -148,16 +171,16 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) var computedKey = GetOrCreateComputedKey(serviceKey); if (!IsServicesRequest(serviceType)) { - return IsKeyedService(serviceType, computedKey) - ? serviceProvider.GetRequiredKeyedService(serviceType, computedKey) + return ContainsLocalEndpointService(serviceType, computedKey) + ? serviceProvider.GetRequiredKeyedService(serviceType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetRequiredKeyedService(serviceType, GetBaseKeyOrServiceKey(serviceKey)); } var itemType = serviceType.GetGenericArguments()[0]; if (!Equals(computedKey, anyKey)) { - return IsKeyedService(itemType, computedKey) - ? serviceProvider.GetKeyedServices(itemType, computedKey) + return ContainsLocalEndpointService(itemType, computedKey) + ? serviceProvider.GetKeyedServices(itemType, GetLocalEndpointServiceKey(computedKey)) : serviceProvider.GetKeyedServices(itemType, GetBaseKeyOrServiceKey(serviceKey)); } @@ -204,6 +227,55 @@ public void Dispose() // this allows resolving services by either the KeyedServiceKey or the base key and makes the experience consistent static object? GetBaseKeyOrServiceKey(object? serviceKey) => serviceKey is KeyedServiceKey key ? key.BaseKey : serviceKey; + bool ContainsLocalEndpointService(Type serviceType, KeyedServiceKey key) => + Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey) && + localEndpointServiceLookup.GetOrAdd((serviceType, key.ServiceKey), static (lookupKey, collection) => + collection.ContainsLocalService(lookupKey.ServiceType, lookupKey.ServiceKey), serviceCollection); + + KeyedServiceKey GetLocalEndpointServiceKey(KeyedServiceKey key) => serviceCollection.GetLocalServiceKey(key.ServiceKey); + + bool ContainsRootEndpointKeyedService(Type serviceType) => ContainsRootKeyedService(serviceType, serviceKeyedServiceKey.BaseKey); + + bool ContainsRootService(Type serviceType) => + rootServiceLookup.GetOrAdd(serviceType, static (lookupServiceType, state) => + { + var rootServiceProbe = state.ServiceProvider?.GetService(); + if (rootServiceProbe?.IsService(lookupServiceType) is true) + { + return true; + } + + foreach (var descriptor in state.ServiceCollection.Inner) + { + if (!descriptor.IsKeyedService && ServiceTypeMatches(descriptor.ServiceType, lookupServiceType)) + { + return true; + } + } + + return false; + }, (ServiceProvider: serviceProvider, ServiceCollection: serviceCollection)); + + bool ContainsRootKeyedService(Type serviceType, object? serviceKey) => + rootKeyedServiceLookup.GetOrAdd((serviceType, serviceKey), static (lookupKey, state) => + { + var rootKeyedServiceProbe = state.ServiceProvider?.GetService(); + if (rootKeyedServiceProbe?.IsKeyedService(lookupKey.ServiceType, lookupKey.ServiceKey) is true) + { + return true; + } + + foreach (var descriptor in state.ServiceCollection.Inner) + { + if (descriptor.IsKeyedService && ServiceTypeMatches(descriptor.ServiceType, lookupKey.ServiceType) && Equals(lookupKey.ServiceKey, descriptor.ServiceKey)) + { + return true; + } + } + + return false; + }, (ServiceProvider: serviceProvider, ServiceCollection: serviceCollection)); + KeyedServiceKey GetOrCreateComputedKey(object? serviceKey) { if (serviceKey is KeyedServiceKey key && Equals(serviceKeyedServiceKey.BaseKey, key.BaseKey)) @@ -214,6 +286,11 @@ KeyedServiceKey GetOrCreateComputedKey(object? serviceKey) return new KeyedServiceKey(serviceKeyedServiceKey, serviceKey); } + // The Microsoft.Extensions.DependencyInjection interfaces implemented here are unannotated, + // so adding RequiresDynamicCode to these members would be an IL3051 mismatch. + const string Justification = "Resolving IEnumerable registrations by runtime Type requires the Type-based GetServices/GetKeyedServices overloads which require dynamic code. Mirrors the framework's own unannotated IServiceProvider behavior and cannot be made AOT-safe while supporting Type-based resolution."; + + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = Justification)] static object GetAllServices(IServiceProvider serviceProvider, Type itemType) { Type genericEnumerable = typeof(List<>).MakeGenericType(itemType); @@ -226,6 +303,7 @@ static object GetAllServices(IServiceProvider serviceProvider, Type itemType) } static bool IsServicesRequest(Type serviceType) => serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>); + static bool ServiceTypeMatches(Type registeredServiceType, Type requestedServiceType) => registeredServiceType == requestedServiceType || (requestedServiceType.IsGenericType && registeredServiceType == requestedServiceType.GetGenericTypeDefinition()); static bool IsServiceProvider(Type serviceType) => serviceType == typeof(IServiceProvider) || serviceType == typeof(ISupportRequiredService) || serviceType == typeof(IServiceProviderIsKeyedService) || serviceType == typeof(IServiceProviderIsService); static bool IsScopeFactory(Type serviceType) => serviceType == typeof(IServiceScopeFactory); @@ -236,4 +314,7 @@ static object GetAllServices(IServiceProvider serviceProvider, Type itemType) readonly bool ownsProvider; readonly KeyedServiceKey anyKey; readonly KeyedServiceScopeFactory keyedScopeFactory; + readonly ConcurrentDictionary<(Type ServiceType, object? ServiceKey), bool> localEndpointServiceLookup = new(); + readonly ConcurrentDictionary rootServiceLookup = new(); + readonly ConcurrentDictionary<(Type ServiceType, object? ServiceKey), bool> rootKeyedServiceLookup = new(); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs new file mode 100644 index 00000000000..4001e87d9a6 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/AuditDiagnostics.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace NServiceBus; + +sealed class AuditDiagnostics +{ + public required string AuditQueue { get; init; } + public required string AuditTTBR { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs new file mode 100644 index 00000000000..ffd20362abb --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ContainerDiagnostics.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace NServiceBus; + +sealed class ContainerDiagnostics +{ + public required string Type { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs index dd136baf126..7768bc0ac13 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/DiagnosticSettingsExtensions.cs @@ -1,10 +1,13 @@ #nullable enable + namespace NServiceBus; using System; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Configuration.AdvancedExtensibility; +using Particular.Obsoletes; using Settings; /// @@ -15,6 +18,9 @@ public static class DiagnosticSettingsExtensions /// /// Adds a section to the startup diagnostics. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7883", + ReplacementTypeOrMember = "AddStartupDiagnosticsSection(IReadOnlySettings, string, T, JsonTypeInfo)", + Note = "The non-generic overload uses reflection-based serialization which is not AOT/trimming safe. Use the generic overload with a JsonTypeInfo instead.")] public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, string sectionName, object section) { ArgumentNullException.ThrowIfNull(settings); @@ -24,6 +30,42 @@ public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, settings.Get().StartupDiagnostics.Add(sectionName, section); } + /// + /// Adds a section to the startup diagnostics with a strongly-typed value and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is cheap to compute. + /// For expensive operations that should only run when diagnostics are actually written, use + /// instead. + /// + public static void AddStartupDiagnosticsSection(this IReadOnlySettings settings, string sectionName, T section, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + ArgumentNullException.ThrowIfNull(section); + ArgumentNullException.ThrowIfNull(typeInfo); + + settings.Get().StartupDiagnostics.Add(sectionName, section, typeInfo); + } + + /// + /// Adds a section to the startup diagnostics with a factory that is evaluated lazily and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is expensive to compute and should only be evaluated + /// when diagnostics are actually written. For cheap values, prefer + /// instead. + /// + public static void AddStartupDiagnosticsSectionFactory(this IReadOnlySettings settings, string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(settings); + ArgumentException.ThrowIfNullOrWhiteSpace(sectionName); + ArgumentNullException.ThrowIfNull(sectionFactory); + ArgumentNullException.ThrowIfNull(typeInfo); + + settings.Get().StartupDiagnostics.AddFactory(sectionName, sectionFactory, typeInfo); + } + /// /// Configures a custom path where host diagnostics is written. /// diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs new file mode 100644 index 00000000000..78c109d96e2 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/EndpointDiagnostics.cs @@ -0,0 +1,10 @@ +#nullable enable + +namespace NServiceBus; + +sealed class EndpointDiagnostics +{ + public required string Name { get; init; } + public required bool SendOnly { get; init; } + public required string NServiceBusVersion { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs index bec5e59e01a..9c1108bdc2c 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/Host.cs @@ -1,63 +1,10 @@ #nullable enable + namespace NServiceBus; using System; -using System.Reflection; -using IODirectory = System.IO.Directory; static class Host { - public static string GetOutputDirectory() - { - Assembly? systemWebAssembly = null; - - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly.GetName().Name != "System.Web") - { - continue; - } - - systemWebAssembly = assembly; - break; - } - - var httpRuntime = systemWebAssembly?.GetType("System.Web.HttpRuntime"); - var appDomainAppId = httpRuntime?.GetProperty("AppDomainAppId", BindingFlags.Public | BindingFlags.Static); - var result = appDomainAppId?.GetValue(null); - - return result == null ? AppDomain.CurrentDomain.BaseDirectory : DeriveAppDataPath(systemWebAssembly!); - } - - static string DeriveAppDataPath(Assembly systemWebAssembly) - { - var appDataPath = TryMapPath(systemWebAssembly) ?? throw new Exception(GetMapPathError("Failed since MapPath returned null.")); - - if (IODirectory.Exists(appDataPath)) - { - return appDataPath; - } - - throw new Exception(GetMapPathError($"Failed since path returned ({appDataPath}) does not exist. Ensure this directory is created and restart the endpoint.")); - } - - static readonly object[] parameters = ["~/App_Data/"]; - - static string? TryMapPath(Assembly systemWebAssembly) - { - try - { - var hostingEnvironment = systemWebAssembly.GetType("System.Web.Hosting.HostingEnvironment"); - var mapPath = hostingEnvironment?.GetMethod("MapPath", BindingFlags.Static | BindingFlags.Public); - var result = mapPath?.Invoke(null, parameters) as string; - - return result; - } - catch (Exception exception) - { - throw new Exception(GetMapPathError("Failed since MapPath threw an exception."), exception); - } - } - - static string GetMapPathError(string reason) => $"Detected running in a website and attempted to use HostingEnvironment.MapPath(\"~/App_Data/\") to derive the logging path. {reason}"; + public static string GetOutputDirectory() => AppDomain.CurrentDomain.BaseDirectory; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs index 1b71d976129..c2229fa3b31 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriter.cs @@ -1,11 +1,16 @@ -#nullable enable +#nullable enable + namespace NServiceBus; using System; +using System.Buffers; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using System.Text.Json; using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; using Logging; @@ -16,26 +21,13 @@ public async Task Write(List en { const int LogSafeThreshold = 30000; // Derived from application insights limits - var deduplicatedEntries = DeduplicateEntries(entries); - var dictionary = deduplicatedEntries - .OrderBy(e => e.Name) - //Note: this will allow for filtering out 'slow' operations to create a cut-down diagnostics should the need arise in future - .Select(e => new { e.Name, Data = e.Data is Func func ? func() : e.Data }) - .ToDictionary(e => e.Name, e => e.Data, StringComparer.OrdinalIgnoreCase); + var resolvedEntries = ResolveEntries(entries); if (writeDiagnosticsToLog) { - var logDictionary = new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); - // Always compact AssemblyScanning section when writing to log - if (logDictionary.TryGetValue(AssemblyScanningDiagnostics.SectionName, out var assemblyScanningSection) && - assemblyScanningSection is AssemblyScanningDiagnostics assemblyScanningDiagnostics) - { - logDictionary[AssemblyScanningDiagnostics.SectionName] = assemblyScanningDiagnostics.CreateCompactedVersion(); - } - try { - var data = JsonSerializer.Serialize(logDictionary, diagnosticsOptions); + var data = SerializeToJson(resolvedEntries, forLog: true); // Safety net: truncate if still exceeds threshold (e.g., due to other large sections) if (data.Length > LogSafeThreshold) { @@ -53,7 +45,7 @@ public async Task Write(List en try { - var data = JsonSerializer.Serialize(dictionary, diagnosticsOptions); + var data = SerializeToJson(resolvedEntries, forLog: false); await diagnosticsWriter(data, cancellationToken).ConfigureAwait(false); } catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) @@ -67,6 +59,85 @@ public async Task Write(List en } } + static List ResolveEntries(List entries) + { + var deduplicated = DeduplicateEntries(entries); + return + [ + .. deduplicated + .OrderBy(e => e.Name) + .Select(e => + { + object value; + if (e.Factory is not null) + { + value = e.Factory(); + } + else if (e.Data is Func func) + { + value = func(); + } + else + { + value = e.Data; + } + + return new ResolvedEntry(e.Name, value, e.JsonTypeInfo); + }) + ]; + } + + static string SerializeToJson(List resolvedEntries, bool forLog) + { + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer); + + writer.WriteStartObject(); + + foreach (var entry in resolvedEntries) + { + var value = entry.Value; + var jsonTypeInfo = entry.JsonTypeInfo; + + // Compact AssemblyScanning section only for log output + if (forLog && value is AssemblyScanningDiagnostics assemblyScanning) + { + value = assemblyScanning.CreateCompactedVersion(); + } + + writer.WritePropertyName(entry.Name); + + if (jsonTypeInfo != null) + { + // AOT-safe path: use the provided JsonTypeInfo + JsonSerializer.Serialize(writer, value, jsonTypeInfo); + } + else + { + SerializeWithReflection(writer, value, entry.Name); + } + } + + writer.WriteEndObject(); + writer.Flush(); + + return Encoding.UTF8.GetString(buffer.WrittenSpan); + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Guarded by JsonSerializer.IsReflectionEnabledByDefault check; throws before reaching this call when reflection is disabled.")] + [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Guarded by JsonSerializer.IsReflectionEnabledByDefault check; throws before reaching this call when reflection is disabled.")] + static void SerializeWithReflection(Utf8JsonWriter jsonWriter, object? entryValue, string entryName) + { + if (!JsonSerializer.IsReflectionEnabledByDefault) + { + throw new InvalidOperationException( + $"Startup diagnostics section '{entryName}' was registered without JSON type metadata. " + + "Use the overload accepting JsonTypeInfo when reflection serialization is disabled."); + } + + JsonSerializer.Serialize(jsonWriter, entryValue, diagnosticsOptions); + } + } + static IEnumerable DeduplicateEntries(List entries) { var countMap = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -82,26 +153,42 @@ public async Task Write(List en countMap[entry.Name] += 1; var entryNewName = $"{entry.Name}-{countMap[entry.Name]}"; + // Ensure the synthesized name does not collide with an existing entry + while (!countMap.TryAdd(entryNewName, 1)) + { + countMap[entry.Name] += 1; + entryNewName = $"{entry.Name}-{countMap[entry.Name]}"; + } + Logger.Warn($"A duplicate diagnostic entry was renamed from {entry.Name} to {entryNewName}."); yield return new StartupDiagnosticEntries.StartupDiagnosticEntry { Name = entryNewName, - Data = entry.Data + Data = entry.Data, + JsonTypeInfo = entry.JsonTypeInfo, + Factory = entry.Factory }; } } } + readonly struct ResolvedEntry(string name, object value, JsonTypeInfo? jsonTypeInfo) + { + public string Name { get; } = name; + public object Value { get; } = value; + public JsonTypeInfo? JsonTypeInfo { get; } = jsonTypeInfo; + } + static readonly JsonSerializerOptions diagnosticsOptions = new() { Converters = { new TypeConverter() } }; /// - /// By default System.Text.Json would throw with "Serialization and deserialization of 'System.Type' instances are not supported" which normally + /// By default, System.Text.Json would throw with "Serialization and deserialization of 'System.Type' instances are not supported" which normally /// would make sense because it can be considered unsafe to serialize and deserialize types. We add a custom converter here to make - /// sure when diagnostics entries accidentally use types it will just print the full name as a string. We never intent to read these things + /// sure when diagnostics entries accidentally use types it will just print the full name as a string. We never intend to read these things /// back so this is a safe approach. /// sealed class TypeConverter : JsonConverter @@ -113,4 +200,4 @@ sealed class TypeConverter : JsonConverter } static readonly ILog Logger = LogManager.GetLogger(); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs index 68b7ee00785..a17b15fac02 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostStartupDiagnosticsWriterFactory.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs new file mode 100644 index 00000000000..723f4b3f072 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/HostingDiagnostics.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Runtime; + +sealed class HostingDiagnostics +{ + public required Guid HostId { get; init; } + public required string HostDisplayName { get; init; } + public required string MachineName { get; init; } + public required PlatformID OSPlatform { get; init; } + public required string OSVersion { get; init; } + public required bool IsServerGC { get; init; } + public required GCLatencyMode GCLatencyMode { get; init; } + public required int ProcessorCount { get; init; } + public required bool Is64BitProcess { get; init; } + public required Version CLRVersion { get; init; } + public required long WorkingSet { get; init; } + public required int SystemPageSize { get; init; } + public required string HostName { get; init; } + public required string UserName { get; init; } + public required string PathToExe { get; init; } + public required bool InstallersEnabled { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs new file mode 100644 index 00000000000..da132222450 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/InstallationDiagnostics.cs @@ -0,0 +1,8 @@ +#nullable enable + +namespace NServiceBus; + +sealed class InstallationDiagnostics +{ + public required string[] InstallersEnabled { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs index 8ae2d000fae..c8ca59250e9 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/JsonPrettyPrinter.cs @@ -1,6 +1,9 @@ #nullable enable + namespace NServiceBus; +using System.Buffers; +using System.Text; using System.Text.Encodings.Web; using System.Text.Json; @@ -9,14 +12,18 @@ static class JsonPrettyPrinter internal static string Print(string input) { using var doc = JsonDocument.Parse(input); - var root = doc.RootElement; + var buffer = new ArrayBufferWriter(); + using var writer = new Utf8JsonWriter(buffer, jsonWriterOptions); + + doc.RootElement.WriteTo(writer); - return JsonSerializer.Serialize(root, jsonSerializerOptions); + writer.Flush(); + return Encoding.UTF8.GetString(buffer.WrittenSpan); } - static readonly JsonSerializerOptions jsonSerializerOptions = new() + static readonly JsonWriterOptions jsonWriterOptions = new() { - WriteIndented = true, + Indented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs new file mode 100644 index 00000000000..632fa6802fd --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/LicensingDiagnostics.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace NServiceBus; + +using System; + +sealed class LicensingDiagnostics +{ + public string? RegisteredTo { get; init; } + public string? LicenseType { get; init; } + public string? Edition { get; init; } + public string? Tier { get; init; } + public int? LicenseStatus { get; init; } + public string? LicenseLocation { get; init; } + public required string ValidApplications { get; init; } + public bool? CommercialLicense { get; init; } + public required bool IsExpired { get; init; } + public DateTime? ExpirationDate { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs new file mode 100644 index 00000000000..4be5f85de51 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/MessagesDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace NServiceBus; + +sealed class MessagesDiagnostics +{ + public required bool CustomConventionUsed { get; init; } + public required string[] MessageConventions { get; init; } + public required int NumberOfMessagesFoundAtStartup { get; init; } + public required string[] Messages { get; init; } + public required bool AllowDynamicTypeLoading { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs new file mode 100644 index 00000000000..be0e32dfee8 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/PersistenceDiagnosticsEntry.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace NServiceBus; + +sealed class PersistenceDiagnosticsEntry +{ + public required string Type { get; init; } + public required string Version { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs new file mode 100644 index 00000000000..7eaab0d15e2 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/QueueAddressDiagnostics.cs @@ -0,0 +1,13 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class QueueAddressDiagnostics +{ + public required string BaseAddress { get; init; } + public string? Discriminator { get; init; } + public required Dictionary Properties { get; init; } + public string? Qualifier { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs new file mode 100644 index 00000000000..86757277cd8 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/ReceivingDiagnostics.cs @@ -0,0 +1,23 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class ReceivingDiagnostics +{ + public required QueueAddressDiagnostics LocalQueueAddress { get; init; } + public QueueAddressDiagnostics? InstanceSpecificQueueAddress { get; init; } + public required bool PurgeOnStartup { get; init; } + public required string TransactionMode { get; init; } + public required int MaxConcurrency { get; init; } + public required SatelliteDiagnostics[] Satellites { get; init; } + public required Dictionary> MessageHandlers { get; init; } +} + +sealed class SatelliteDiagnostics +{ + public required string Name { get; init; } + public required QueueAddressDiagnostics ReceiveAddress { get; init; } + public required int MaxConcurrency { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs new file mode 100644 index 00000000000..190a1429133 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/RecoverabilityDiagnostics.cs @@ -0,0 +1,12 @@ +#nullable enable + +namespace NServiceBus; + +sealed class RecoverabilityDiagnostics +{ + public required int ImmediateRetries { get; init; } + public required int DelayedRetries { get; init; } + public required string DelayedRetriesTimeIncrease { get; init; } + public required string ErrorQueue { get; init; } + public required string[] UnrecoverableExceptions { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs new file mode 100644 index 00000000000..b3e811f94a4 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/SerializationDiagnostics.cs @@ -0,0 +1,26 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; + +sealed class SerializationDiagnostics +{ + public required MainSerializerDiagnostics MainSerializer { get; init; } + public required List AdditionalDeserializers { get; init; } + public required bool AllowMessageTypeInference { get; init; } +} + +sealed class MainSerializerDiagnostics +{ + public required string Type { get; init; } + public required string Version { get; init; } + public required string ContentType { get; init; } +} + +sealed class AdditionalDeserializerDiagnostics +{ + public required string Type { get; init; } + public required string Version { get; init; } + public required string ContentType { get; init; } +} diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs index 23fa7eed86a..36b67e831a1 100644 --- a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticEntries.cs @@ -1,7 +1,11 @@ #nullable enable + namespace NServiceBus; +using System; using System.Collections.Generic; +using System.Text.Json.Serialization.Metadata; +using Particular.Obsoletes; /// /// Holds diagnostics entries to be written at startup. @@ -11,6 +15,9 @@ public class StartupDiagnosticEntries /// /// Adds a new section to the diagnostics. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7883", + ReplacementTypeOrMember = "Add(string, T, JsonTypeInfo)", + Note = "The non-generic overload uses reflection-based serialization which is not AOT/trimming safe. Use the generic overload with a JsonTypeInfo instead.")] public void Add(string sectionName, object section) => entries.Add(new StartupDiagnosticEntry { @@ -18,6 +25,45 @@ public void Add(string sectionName, object section) => Data = section }); + /// + /// Adds a new section to the diagnostics with a strongly-typed value and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is cheap to compute. + /// For expensive operations that should only run when diagnostics are actually written, use instead. + /// + public void Add(string sectionName, T section, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(typeInfo); + ArgumentNullException.ThrowIfNull(section); + entries.Add(new StartupDiagnosticEntry + { + Name = sectionName, + Data = section, + JsonTypeInfo = typeInfo + }); + } + + /// + /// Adds a new section to the diagnostics with a factory that is evaluated lazily and its JSON type info for AOT-safe serialization. + /// + /// + /// Use this overload when the diagnostics value is expensive to compute and should only be evaluated + /// when diagnostics are actually written. For cheap values, prefer instead. + /// + public void AddFactory(string sectionName, Func sectionFactory, JsonTypeInfo typeInfo) + { + ArgumentNullException.ThrowIfNull(sectionFactory); + ArgumentNullException.ThrowIfNull(typeInfo); + entries.Add(new StartupDiagnosticEntry + { + Name = sectionName, + Data = null!, + JsonTypeInfo = typeInfo, + Factory = () => sectionFactory()! + }); + } + internal readonly List entries = []; /// @@ -34,5 +80,9 @@ public class StartupDiagnosticEntry /// The actual diagnostics data. /// public required object Data { get; set; } + + internal JsonTypeInfo? JsonTypeInfo { get; set; } + + internal Func? Factory { get; set; } } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs new file mode 100644 index 00000000000..6979171c2b5 --- /dev/null +++ b/src/NServiceBus.Core/Hosting/StartupDiagnostics/StartupDiagnosticsJsonContext.cs @@ -0,0 +1,40 @@ +#nullable enable + +namespace NServiceBus; + +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Features; +using Hosting.Helpers; + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(List))] +[JsonSerializable(typeof(HostingDiagnostics))] +[JsonSerializable(typeof(EndpointDiagnostics))] +[JsonSerializable(typeof(MessagesDiagnostics))] +[JsonSerializable(typeof(ContainerDiagnostics))] +[JsonSerializable(typeof(SerializationDiagnostics))] +[JsonSerializable(typeof(MainSerializerDiagnostics))] +[JsonSerializable(typeof(AdditionalDeserializerDiagnostics))] +[JsonSerializable(typeof(FeatureDiagnosticData[]))] +[JsonSerializable(typeof(InstallationDiagnostics))] +[JsonSerializable(typeof(ReceivingDiagnostics))] +[JsonSerializable(typeof(QueueAddressDiagnostics))] +[JsonSerializable(typeof(SatelliteDiagnostics))] +[JsonSerializable(typeof(RecoverabilityDiagnostics))] +[JsonSerializable(typeof(AuditDiagnostics))] +[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(PersistenceDiagnosticsEntry))] +[JsonSerializable(typeof(LicensingDiagnostics))] +[JsonSerializable(typeof(AssemblyScanningDiagnostics))] +[JsonSerializable(typeof(AssemblyDetails))] +[JsonSerializable(typeof(SkippedFile))] +[JsonSerializable(typeof(AssemblyScannerConfiguration))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType[]))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType.SchemaProperty[]))] +[JsonSerializable(typeof(ReceiveComponentManifestMessageType.SchemaProperty))] +[JsonSerializable(typeof(Dictionary>))] +sealed partial class StartupDiagnosticsJsonContext : JsonSerializerContext +{ +} diff --git a/src/NServiceBus.Core/IMessageCreator.cs b/src/NServiceBus.Core/IMessageCreator.cs index a662c7e7347..898ced73164 100644 --- a/src/NServiceBus.Core/IMessageCreator.cs +++ b/src/NServiceBus.Core/IMessageCreator.cs @@ -3,18 +3,21 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; /// /// The abstraction for creating interface-based messages. /// public interface IMessageCreator { + internal const DynamicallyAccessedMemberTypes CreatorMembersRequired = DynamicMemberTypeAccess.Message; + /// /// Creates an instance of the message type T. /// /// The type of message interface to instantiate. /// A message object that implements the interface T. - T CreateInstance(); + T CreateInstance<[DynamicallyAccessedMembers(CreatorMembersRequired)] T>(); /// /// Creates an instance of the message type T and fills it with data. @@ -22,12 +25,12 @@ public interface IMessageCreator /// The type of message interface to instantiate. /// An action to set various properties of the instantiated object. /// A message object that implements the interface T. - T CreateInstance(Action action); + T CreateInstance<[DynamicallyAccessedMembers(CreatorMembersRequired)] T>(Action action); /// /// Creates an instance of the given message type. /// /// The type of message to instantiate. /// A message object that implements the given interface. - object CreateInstance(Type messageType); + object CreateInstance([DynamicallyAccessedMembers(CreatorMembersRequired)] Type messageType); } \ No newline at end of file diff --git a/src/NServiceBus.Core/IMessageProcessingContext.cs b/src/NServiceBus.Core/IMessageProcessingContext.cs index 23844af9e19..a957ff97498 100644 --- a/src/NServiceBus.Core/IMessageProcessingContext.cs +++ b/src/NServiceBus.Core/IMessageProcessingContext.cs @@ -2,7 +2,10 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// The context of the currently processed message within the processing pipeline. @@ -29,15 +32,49 @@ public interface IMessageProcessingContext : IPipelineContext /// /// The message to send. /// Options for this reply. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Reply(T, ReplyOptions) or Reply(object, Type, ReplyOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Reply(object message, ReplyOptions options); /// - /// Instantiates a message of type T and performs a regular . + /// Sends the typed message to the endpoint which sent the message currently being handled. /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// Options for this reply. + [OverloadResolutionPriority(-1)] + Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + return Reply(message!, typeof(T), options); + } + + /// + /// Sends the message to the endpoint which sent the message currently being handled with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// Options for this reply. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Reply(message, options); + } + + /// + /// Instantiates a message of type T and performs a regular Reply. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Options for this reply. - Task Reply(Action messageConstructor, ReplyOptions options); + Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options); /// /// Forwards the current message being handled to the destination maintaining diff --git a/src/NServiceBus.Core/IMessageSession.cs b/src/NServiceBus.Core/IMessageSession.cs index a7fdd7b6c33..d5f3e20d525 100644 --- a/src/NServiceBus.Core/IMessageSession.cs +++ b/src/NServiceBus.Core/IMessageSession.cs @@ -3,8 +3,11 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// A session which provides basic message operations. @@ -17,16 +20,52 @@ public interface IMessageSession /// The message to send. /// The options for the send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default); + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + /// A to observe. + [OverloadResolutionPriority(-1)] + Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + return Send(message!, typeof(T), sendOptions, cancellationToken); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the send. + /// A to observe. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, sendOptions, cancellationToken); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. /// A to observe. - Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default); + Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default); /// /// Publish the message to subscribers. @@ -34,16 +73,52 @@ public interface IMessageSession /// The message to publish. /// The options for the publish. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default); + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + /// A to observe. + [OverloadResolutionPriority(-1)] + Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + return Publish(message!, typeof(T), publishOptions, cancellationToken); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the publish. + /// A to observe. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, publishOptions, cancellationToken); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. /// A to observe. - Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default); + Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default); /// /// Subscribes to receive published messages of the specified type. diff --git a/src/NServiceBus.Core/IPipelineContext.cs b/src/NServiceBus.Core/IPipelineContext.cs index a52eaf2e1dc..a071bbd5bb9 100644 --- a/src/NServiceBus.Core/IPipelineContext.cs +++ b/src/NServiceBus.Core/IPipelineContext.cs @@ -1,8 +1,11 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Extensibility; +using Particular.Obsoletes; /// /// The context for the current message handling pipeline. @@ -14,28 +17,96 @@ public interface IPipelineContext : ICancellableContext, IExtendable /// /// The message to send. /// The options for the send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions) or Send(object, Type, SendOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Send(object message, SendOptions options); + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return Send(message!, typeof(T), options); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the send. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. - Task Send(Action messageConstructor, SendOptions options); + Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options); /// /// Publish the message to subscribers. /// /// The message to publish. /// The options for the publish. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions) or Publish(object, Type, PublishOptions)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] Task Publish(object message, PublishOptions options); + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return Publish(message!, typeof(T), options); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// The options for the publish. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. - Task Publish(Action messageConstructor, PublishOptions publishOptions); + Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions); } \ No newline at end of file diff --git a/src/NServiceBus.Core/IdGeneration/CombGuid.cs b/src/NServiceBus.Core/IdGeneration/CombGuid.cs index 619ef8d1b4f..6e7a5e5ef01 100644 --- a/src/NServiceBus.Core/IdGeneration/CombGuid.cs +++ b/src/NServiceBus.Core/IdGeneration/CombGuid.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Installation/InstallerComponent.cs b/src/NServiceBus.Core/Installation/InstallerComponent.cs index d2ce0c12a23..06897953545 100644 --- a/src/NServiceBus.Core/Installation/InstallerComponent.cs +++ b/src/NServiceBus.Core/Installation/InstallerComponent.cs @@ -14,10 +14,10 @@ namespace NServiceBus; class InstallerComponent(InstallerComponent.Settings settings) { - public void Initialize(IReadOnlySettings globalSettings) => globalSettings.AddStartupDiagnosticsSection("Installation", new + public void Initialize(IReadOnlySettings globalSettings) => globalSettings.AddStartupDiagnosticsSection("Installation", new InstallationDiagnostics { - InstallersEnabled = settings.Installers.Select(i => i.InstallerType.FullName).ToArray() - }); + InstallersEnabled = settings.Installers.Select(i => i.InstallerType.FullName!).ToArray() + }, StartupDiagnosticsJsonContext.Default.InstallationDiagnostics); public async Task RunInstallers(IServiceProvider serviceProvider, CancellationToken cancellationToken = default) { diff --git a/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs b/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs index 6a0c9e9ab95..e91181b10c0 100644 --- a/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs +++ b/src/NServiceBus.Core/Licensing/ActiveLicenseFindResultExtensions.cs @@ -6,5 +6,5 @@ namespace NServiceBus; static class ActiveLicenseFindResultExtensions { - public static bool HasLicenseExpired(this ActiveLicenseFindResult result) => result.License.HasExpired(); + public static bool HasLicenseExpired(this ActiveLicenseFindResult result) => result.License?.HasExpired() ?? true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Licensing/LicenseManager.cs b/src/NServiceBus.Core/Licensing/LicenseManager.cs index 7e8f616cc61..7241894eee7 100644 --- a/src/NServiceBus.Core/Licensing/LicenseManager.cs +++ b/src/NServiceBus.Core/Licensing/LicenseManager.cs @@ -22,7 +22,7 @@ public static ActiveLicenseFindResult InitializeLicense(string? licenseText, str LogFindResults(result); - var licenseStatus = result.License.GetLicenseStatus(); + var licenseStatus = result.License?.GetLicenseStatus(); LogLicenseStatus(licenseStatus, Logger, result.License, developerLicenseUrl); if (licenseStatus == LicenseStatus.InvalidDueToExpiredTrial) @@ -33,39 +33,32 @@ public static ActiveLicenseFindResult InitializeLicense(string? licenseText, str return result; } - public static void LogLicenseStatus(LicenseStatus licenseStatus, ILog logger, License license, string developerLicenseUrl) + public static void LogLicenseStatus(LicenseStatus? licenseStatus, ILog logger, License? license, string developerLicenseUrl) { - var whenLicenseExpiresPhrase = GetRemainingDaysString(license.GetDaysUntilLicenseExpires()); - var whenUpgradeProtectedExpiresPhrase = GetRemainingDaysString(license.GetDaysUntilUpgradeProtectionExpires()); + var whenLicenseExpiresPhrase = GetRemainingDaysString(license?.GetDaysUntilLicenseExpires()); switch (licenseStatus) { case LicenseStatus.Valid: break; - case LicenseStatus.ValidWithExpiredUpgradeProtection: - logger.Warn("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); - break; case LicenseStatus.ValidWithExpiringTrial: - logger.Warn(license.IsExtendedTrial + logger.Warn(license?.IsExtendedTrial ?? false ? $"Development license expiring {whenLicenseExpiresPhrase}. If you’re still in development, renew your license for free at {developerLicenseUrl} otherwise email contact@particular.net" : $"Trial license expiring {whenLicenseExpiresPhrase}. Get your free development license at {developerLicenseUrl}"); break; case LicenseStatus.ValidWithExpiringSubscription: logger.Warn($"License expiring {whenLicenseExpiresPhrase}. Contact us to renew your license: contact@particular.net"); break; - case LicenseStatus.ValidWithExpiringUpgradeProtection: - logger.Warn($"Upgrade protection expiring {whenUpgradeProtectedExpiresPhrase}. Contact us to renew your license: contact@particular.net"); - break; case LicenseStatus.InvalidDueToExpiredTrial: - logger.Error(license.IsExtendedTrial + logger.Error(license?.IsExtendedTrial ?? false ? $"Development license expired. If you’re still in development, renew your license for free at {developerLicenseUrl} otherwise email contact@particular.net" : $"Trial license expired. Get your free development license at {developerLicenseUrl}"); break; case LicenseStatus.InvalidDueToExpiredSubscription: logger.Error("License expired. Contact us to renew your license: contact@particular.net"); break; - case LicenseStatus.InvalidDueToExpiredUpgradeProtection: - logger.Error("Upgrade protection expired. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); + case LicenseStatus.InvalidDueToUpgradeProtectionNoLongerBeingSupported: + logger.Error("Licenses with ugprade protection are no longer supported. In order for us to continue to provide you with support and new versions of the Particular Service Platform, contact us to renew your license: contact@particular.net"); break; default: break; @@ -137,7 +130,7 @@ static void OpenDeveloperLicensePage(string developerLicenseUrl) static string CreateDeveloperLicenseUrl(ActiveLicenseFindResult result) { var version = VersionInformation.MajorMinorPatch; - var isRenewal = result.License.IsExtendedTrial ? "1" : "0"; + var isRenewal = result.License?.IsExtendedTrial ?? false ? "1" : "0"; var platform = GetPlatformCode(); var frameworkVersion = GetFrameworkVersion(); return $"https://particular.net/license/nservicebus?v={version}&t={isRenewal}&p={platform}&f={frameworkVersion}"; diff --git a/src/NServiceBus.Core/Licensing/LicenseReminder.cs b/src/NServiceBus.Core/Licensing/LicenseReminder.cs index dcc6fa40ae6..68cbdc23442 100644 --- a/src/NServiceBus.Core/Licensing/LicenseReminder.cs +++ b/src/NServiceBus.Core/Licensing/LicenseReminder.cs @@ -21,7 +21,7 @@ protected override void Setup(FeatureConfigurationContext context) { var result = LicenseManager.InitializeLicense(context.Settings.Get(LicenseTextSettingsKey), context.Settings.Get(LicenseFilePathSettingsKey)); - context.Settings.AddStartupDiagnosticsSection("Licensing", GenerateLicenseDiagnostics(result)); + context.Settings.AddStartupDiagnosticsSection("Licensing", GenerateLicenseDiagnostics(result), StartupDiagnosticsJsonContext.Default.LicensingDiagnostics); if (!result.HasLicenseExpired()) { @@ -42,20 +42,19 @@ protected override void Setup(FeatureConfigurationContext context) } } - static object GenerateLicenseDiagnostics(ActiveLicenseFindResult result) => - new + static LicensingDiagnostics GenerateLicenseDiagnostics(ActiveLicenseFindResult result) => + new LicensingDiagnostics { - result.License.RegisteredTo, - result.License.LicenseType, - result.License.Edition, - Tier = result.License.Edition, - LicenseStatus = result.License.GetLicenseStatus(), + RegisteredTo = result.License?.RegisteredTo, + LicenseType = result.License?.LicenseType, + Edition = result.License?.Edition, + Tier = result.License?.Edition, + LicenseStatus = (int?)result.License?.GetLicenseStatus(), LicenseLocation = result.Location, - ValidApplications = string.Join(",", result.License.ValidApplications), - CommercialLicense = result.License.IsCommercialLicense, + ValidApplications = string.Join(",", result.License?.ValidApplications ?? []), + CommercialLicense = result.License?.IsCommercialLicense, IsExpired = result.HasLicenseExpired(), - result.License.ExpirationDate, - UpgradeProtectionExpirationDate = result.License.UpgradeProtectionExpiration + ExpirationDate = result.License?.ExpirationDate, }; public const string LicenseTextSettingsKey = "LicenseText"; diff --git a/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs b/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs index 321978e8160..87588d5e4b6 100644 --- a/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs +++ b/src/NServiceBus.Core/Logging/ExternalLoggerFactoryAdapter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -11,6 +12,8 @@ sealed class ExternalLoggerFactoryAdapter(ILoggerFactory externalFactory, Micros readonly ILogger scopeLogger = microsoftLoggerFactory.CreateLogger(MicrosoftLoggerFactoryAdapter.ScopeLoggerName); public ILog GetLogger(Type type) => GetLogger(type.FullName ?? type.Name); + public ILog GetLogger(string name) => externalFactory.GetLogger(name); + public IDisposable BeginScope(LogScopeState scopeState) => scopeLogger.BeginScope(scopeState) ?? NullScope.Instance; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs b/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs index 874798a6abf..3eeb2378f9c 100644 --- a/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs +++ b/src/NServiceBus.Core/Logging/IUnsupportedDefaultFactoryLoggerFactory.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; interface IUnsupportedDefaultFactoryLoggerFactory; \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs index 8ec267e7642..a0f11e5452f 100644 --- a/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs +++ b/src/NServiceBus.Core/Logging/MicrosoftLoggerAdapter.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -8,9 +9,6 @@ namespace NServiceBus; sealed class MicrosoftLoggerAdapter(ILogger logger) : ILog { - static readonly Func MessageFormatter = static (state, _) => state ?? string.Empty; - static readonly Func<(string format, object?[] args), Exception?, string> FormatMessageFormatter = static (state, _) => string.Format(state.format, state.args); - public bool IsDebugEnabled => logger.IsEnabled(LogLevel.Debug); public bool IsInfoEnabled => logger.IsEnabled(LogLevel.Information); public bool IsWarnEnabled => logger.IsEnabled(LogLevel.Warning); @@ -47,6 +45,7 @@ sealed class MicrosoftLoggerAdapter(ILogger logger) : ILog public void FatalFormat(string format, params object?[] args) => WriteFormat(LogLevel.Critical, format, args); +#pragma warning disable CA2254 // Template should be a static expression -- this is infrastructure, not application code void Write(LogLevel level, string? message, Exception? exception = null) { if (!logger.IsEnabled(level)) @@ -54,7 +53,7 @@ void Write(LogLevel level, string? message, Exception? exception = null) return; } - logger.Log(level, default, message, exception, MessageFormatter); + logger.Log(level, default, exception, message); } void WriteFormat(LogLevel level, string format, object?[] args) @@ -64,6 +63,7 @@ void WriteFormat(LogLevel level, string format, object?[] args) return; } - logger.Log(level, default, (format, args), null, FormatMessageFormatter); + logger.Log(level, default, null, format, args); } +#pragma warning restore CA2254 } \ No newline at end of file diff --git a/src/NServiceBus.Core/Logging/RollingLogger.cs b/src/NServiceBus.Core/Logging/RollingLogger.cs index a1bbcd0467b..f2c6288bb05 100644 --- a/src/NServiceBus.Core/Logging/RollingLogger.cs +++ b/src/NServiceBus.Core/Logging/RollingLogger.cs @@ -44,15 +44,27 @@ protected virtual void AppendLine(string message) void SyncFileSystem() { - if (!HasCurrentDateChanged() && !IsCurrentFileTooLarge()) + try { - return; + if (!HasCurrentDateChanged() && !IsCurrentFileTooLarge()) + { + return; + } + var today = GetDate(); + var nsbLogFiles = GetNsbLogFiles(targetDirectory).ToList(); + CalculateNewFileName(nsbLogFiles, today); + lastWriteDate = today; + PurgeOldFiles(nsbLogFiles); + } + catch (Exception exception) when (exception is IOException or UnauthorizedAccessException) + { + // Tolerate environmental file system failures like the log directory or its files being removed + // by overlapping processes, deployments, slot swaps or cleanups. Anything else is a bug and propagates. + // The synchronization state is only committed once the filename calculation succeeded so a failed + // attempt is retried on the next write. + var errorMessage = $"NServiceBus.RollingLogger Could not synchronize log files in directory '{targetDirectory}'. Exception: {exception}"; + Trace.WriteLine(errorMessage); } - var today = GetDate(); - lastWriteDate = today; - var nsbLogFiles = GetNsbLogFiles(targetDirectory).ToList(); - CalculateNewFileName(nsbLogFiles, today); - PurgeOldFiles(nsbLogFiles); } bool HasCurrentDateChanged() => GetDate() != lastWriteDate; @@ -126,6 +138,23 @@ static bool TryDeriveLogInformationFromPath(string file, [NotNullWhen(true)] out static bool TryParseDate(string datePart, out DateTimeOffset dateTime) => DateTimeOffset.TryParseExact(datePart, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out dateTime); + protected virtual long GetFileSizeOrZero(string path) + { + try + { + return new FileInfo(path).Length; + } + catch (Exception exception) when (exception is FileNotFoundException or DirectoryNotFoundException) + { + // The file can vanish between enumerating it and reading its metadata, e.g. removed by + // overlapping processes, deployments or external cleanup. Treat it as empty so the + // sequence number is reused and the file is recreated by the next append. Any other + // failure, like a transient I/O error or an ACL that denies metadata reads, aborts the + // synchronization instead of resetting the tracked size and reusing the oversized file. + return 0; + } + } + void CalculateNewFileName(List logFiles, DateTimeOffset today) { var logFile = GetTodaysNewest(logFiles, today); @@ -137,7 +166,7 @@ void CalculateNewFileName(List logFiles, DateTimeOffset today) } else { - var existingFileSize = new FileInfo(logFile.Path).Length; + var existingFileSize = GetFileSizeOrZero(logFile.Path); if (existingFileSize > maxFileSize) { sequenceNumber = logFile.SequenceNumber + 1; diff --git a/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs index a94f53915f9..f2277466392 100644 --- a/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs +++ b/src/NServiceBus.Core/MessageInterfaces/IMessageMapper.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.MessageInterfaces; using System; diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs index b8142771726..be1ebfcef4e 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/ConcreteProxyCreator.cs @@ -1,15 +1,18 @@ #nullable enable + namespace NServiceBus; using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Reflection.Emit; class ConcreteProxyCreator { + [RequiresDynamicCode("Generating message proxies uses System.Reflection.Emit to emit concrete types at runtime, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] public ConcreteProxyCreator() { var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("NServiceBusMessageProxies"), AssemblyBuilderAccess.Run); @@ -20,6 +23,8 @@ public ConcreteProxyCreator() /// Generates the concrete implementation of the given type. /// Only properties on the given type are generated in the concrete implementation. /// + [RequiresDynamicCode("Generating message proxies uses System.Reflection.Emit to emit concrete types at runtime, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] + [RequiresUnreferencedCode("Generating message proxies reflects over all properties and custom attributes of the source interface, which cannot be statically analyzed by the trimmer.")] public Type CreateTypeFrom(Type type) { var typeBuilder = moduleBuilder.DefineType($"{type.FullName}{SUFFIX}", @@ -136,6 +141,7 @@ static void AddCustomAttributeToProperty(CustomAttributeData attributeData, Prop /// /// Returns all properties on the given type, going up the inheritance hierarchy. /// + [RequiresUnreferencedCode("Reflects over all properties and implemented interfaces of the given type, which cannot be statically analyzed by the trimmer.")] static List GetAllProperties(Type type) { var props = new List(type.GetProperties()); diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs index b3640a3e3b4..6c8db4b311b 100644 --- a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/Reflection/MessageMapper.cs @@ -1,10 +1,12 @@ #nullable enable + namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; @@ -13,6 +15,8 @@ namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; /// /// Uses reflection to map between interfaces and their generated concrete implementations. /// +[RequiresDynamicCode("The message mapper generates concrete proxies for interface-based messages using System.Reflection.Emit, which is not supported when dynamic code generation is unavailable (e.g. trimming or NativeAOT).")] +[RequiresUnreferencedCode("The message mapper reflects over all fields, properties, constructors and custom attributes of the provided message types to generate concrete proxies, which cannot be statically analyzed by the trimmer.")] public class MessageMapper : IMessageMapper { /// @@ -77,12 +81,12 @@ public void Initialize(IEnumerable? types) /// /// Calls the and returns its result cast to . /// - public T CreateInstance() => (T)CreateInstance(typeof(T)); + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => (T)CreateInstance(typeof(T)); /// /// Calls the generic CreateInstance and performs the given action on the result. /// - public T CreateInstance(Action action) + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) { var result = CreateInstance(); @@ -95,13 +99,13 @@ public T CreateInstance(Action action) /// If the given type is an interface, finds its generated concrete implementation, instantiates it, and returns the /// result. /// - public object CreateInstance(Type t) + public object CreateInstance([DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) { ArgumentNullException.ThrowIfNull(t); InitType(t); - if ((t.IsInterface || t.IsAbstract) && GetMappedTypeFor(t) is Type mapped) + if ((t.IsInterface || t.IsAbstract) && GetMappedTypeFor(t) is { } mapped) { return RuntimeHelpers.GetUninitializedObject(mapped); } diff --git a/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs new file mode 100644 index 00000000000..46210b62a21 --- /dev/null +++ b/src/NServiceBus.Core/MessageInterfaces/MessageMapper/TrimmingSafeMessageMapper.cs @@ -0,0 +1,90 @@ +#nullable enable + +namespace NServiceBus.MessageInterfaces.MessageMapper.Reflection; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +/// +/// A trimming/AOT-safe that does not rely on dynamic code generation. +/// +/// +/// Interface-based messages require a generated concrete proxy and therefore cannot be supported when +/// dynamic code generation is unavailable (trimming / NativeAOT). This mapper maps types as identity +/// (concrete types stay as-is) and instantiates concrete types directly. Any attempt to create or +/// resolve a proxy for an interface or an abstract class throws at the point of use, +/// rather than at startup. +/// +public sealed class TrimmingSafeMessageMapper : IMessageMapper +{ + /// + /// Does nothing. Proxy generation is not supported without dynamic code. + /// + public void Initialize(IEnumerable? types) + { + // Intentionally a no-op. Interface mapping is not available without dynamic code generation, + // and concrete types do not require initialization. + } + + /// + /// Returns the given concrete type unchanged. Interface or abstract types cannot be mapped to a generated + /// concrete implementation without dynamic code generation. Failing here (rather than returning the + /// interface and letting the serializer fail opaquely later) keeps the deserialization error actionable. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; publish a concrete type that implements the interface instead. + public Type? GetMappedTypeFor(Type t) + { + ArgumentNullException.ThrowIfNull(t); + + if (t.IsInterface || t.IsAbstract) + { + throw new NotSupportedException( + $"Mapping interface or abstract type '{t.FullName}' to a concrete implementation is not supported when dynamic code generation is unavailable (e.g. under trimming or NativeAOT), because no proxy can be generated. " + + "Publish a concrete type that implements the interface instead of the interface itself so it can be deserialized directly."); + } + + return t; + } + + /// + /// Returns null. No name-to-type mapping is available without dynamic code generation. + /// + public Type? GetMappedTypeFor(string typeName) => null; + + /// + /// Instantiates a concrete message type. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>() => (T)CreateInstance(typeof(T)); + + /// + /// Instantiates a concrete message type, then applies the given action. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public T CreateInstance<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action action) + { + var result = CreateInstance(); + action(result); + return result; + } + + /// + /// Instantiates a concrete message type. + /// + /// Thrown when is an interface or abstract type. Interface-based messages require a proxy that can only be generated with dynamic code; use a concrete type instead. + public object CreateInstance([DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] Type t) + { + ArgumentNullException.ThrowIfNull(t); + + if (t.IsInterface || t.IsAbstract) + { + throw new NotSupportedException( + $"Creating an instance of interface or abstract type '{t.FullName}' is not supported when dynamic code generation is unavailable (e.g. under trimming or NativeAOT). " + + "Use concrete message types instead of interface-based messages in these scenarios."); + } + + return RuntimeHelpers.GetUninitializedObject(t); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs index 6f1e1423048..ef9de36e969 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -46,11 +47,28 @@ await mutator.MutateIncoming(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessageInstance(mutatorContext.Message); + UpdateMessageInstance(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateIncomingMessageContext.Message setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessageInstance(IIncomingLogicalMessageContext context, MutateIncomingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessageInstance(mutatorContext.Message, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateIncomingMessageContext.Message which is marked as RequiresUnreferencedCode + context.UpdateMessageInstance(mutatorContext.Message); + } + } + volatile bool hasIncomingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 07f6b1ac709..e3864da6f75 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -4,7 +4,9 @@ namespace NServiceBus.MessageMutator; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -29,14 +31,43 @@ public MutateIncomingMessageContext(object message, Dictionary h public object Message { get => message; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessageInstance(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessageInstance(T)' or 'UpdateMessageInstance(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); MessageInstanceChanged = true; + // The setter declares no message type, so drop any type declared by an earlier replacement. + ReplacementMessageType = null; message = value; } } + /// + /// Replaces the current incoming message with the provided typed message instance. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) => UpdateMessageInstance(newMessage!, typeof(T)); + + /// + /// Replaces the current incoming message with the provided message instance and message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + message = newMessage; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current incoming headers. /// @@ -50,4 +81,7 @@ public object Message object message; internal bool MessageInstanceChanged; + + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs index 8fedadcebe2..7e6551878ea 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -52,11 +53,28 @@ await mutator.MutateOutgoing(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessage(mutatorContext.OutgoingMessage); + UpdateMessage(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateOutgoingMessageContext.OutgoingMessage setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessage(IOutgoingLogicalMessageContext context, MutateOutgoingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateOutgoingMessageContext.OutgoingMessage which is marked as RequiresUnreferencedCode + context.UpdateMessage(mutatorContext.OutgoingMessage); + } + } + volatile bool hasOutgoingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index 8eba75e3684..e5b40077c6a 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -6,6 +6,7 @@ namespace NServiceBus.MessageMutator; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -32,14 +33,46 @@ public MutateOutgoingMessageContext(object outgoingMessage, Dictionary outgoingMessage; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessage(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessage(T)' or 'UpdateMessage(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); MessageInstanceChanged = true; + // The setter declares no message type, so drop any type declared by an earlier replacement. + ReplacementMessageType = null; outgoingMessage = value; } } + /// + /// Replaces the current outgoing message with the provided typed message instance. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) + { + UpdateMessage(newMessage!, typeof(T)); + } + + /// + /// Replaces the current outgoing message with the provided message instance and message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessage(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + outgoingMessage = newMessage; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current outgoing headers. /// @@ -73,5 +106,11 @@ public bool TryGetIncomingHeaders([NotNullWhen(true)] out IReadOnlyDictionary + /// The declared logical message type of when a mutator supplied an explicit type, otherwise . + /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType { get; set; } + object outgoingMessage; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs index 3e9aca6f2bb..4f1fba59ffd 100644 --- a/src/NServiceBus.Core/MessageProcessingContextExtensions.cs +++ b/src/NServiceBus.Core/MessageProcessingContextExtensions.cs @@ -1,7 +1,10 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -13,6 +16,10 @@ public static class MessageProcessingContextExtensions /// /// Object being extended. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Reply(this IMessageProcessingContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Reply(this IMessageProcessingContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -21,13 +28,45 @@ public static Task Reply(this IMessageProcessingContext context, object message) return context.Reply(message, new ReplyOptions()); } + /// + /// Sends the typed message to the endpoint which sent the message currently being handled on this thread. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageProcessingContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Reply(message, new ReplyOptions()); + } + + /// + /// Sends the message with the specified message type to the endpoint which sent the message currently being handled on this thread. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Reply(this IMessageProcessingContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Reply(message, messageType, new ReplyOptions()); + } + /// /// Instantiates a message of type T and performs a regular Reply. /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. - public static Task Reply(this IMessageProcessingContext context, Action messageConstructor) + public static Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageProcessingContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/MessageSession.cs b/src/NServiceBus.Core/MessageSession.cs index 5eb95fdab40..566200a86a5 100644 --- a/src/NServiceBus.Core/MessageSession.cs +++ b/src/NServiceBus.Core/MessageSession.cs @@ -4,9 +4,11 @@ namespace NServiceBus; using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Logging; +using Particular.Obsoletes; class MessageSession : IMessageSession { @@ -79,6 +81,10 @@ CancellationTokenSource CreateOperationLinkedTokenSource(CancellationToken cance } } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public async Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -90,7 +96,31 @@ public async Task Send(object message, SendOptions sendOptions, CancellationToke await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, sendOptions).ConfigureAwait(false); } - public async Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + [OverloadResolutionPriority(-1)] + public async Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(sendOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, sendOptions).ConfigureAwait(false); + } + + public async Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(sendOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Send(CreateContext(linkedTokenSource.Token), message, messageType, sendOptions).ConfigureAwait(false); + } + + public async Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(sendOptions); @@ -101,6 +131,10 @@ public async Task Send(Action messageConstructor, SendOptions sendOptions, await messageOperations.Send(CreateContext(linkedTokenSource.Token), messageConstructor, sendOptions).ConfigureAwait(false); } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public async Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -112,7 +146,31 @@ public async Task Publish(object message, PublishOptions publishOptions, Cancell await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, publishOptions).ConfigureAwait(false); } - public async Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + [OverloadResolutionPriority(-1)] + public async Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(publishOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, publishOptions).ConfigureAwait(false); + } + + public async Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(publishOptions); + + using var _ = LogManager.BeginSlotScope(loggingSlot); + await WaitUntilInitialized(cancellationToken).ConfigureAwait(false); + using var linkedTokenSource = CreateOperationLinkedTokenSource(cancellationToken); + await messageOperations.Publish(CreateContext(linkedTokenSource.Token), message, messageType, publishOptions).ConfigureAwait(false); + } + + public async Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(publishOptions); diff --git a/src/NServiceBus.Core/MessageSessionExtensions.cs b/src/NServiceBus.Core/MessageSessionExtensions.cs index 6dc2c7a88f6..4b7b10b26d1 100644 --- a/src/NServiceBus.Core/MessageSessionExtensions.cs +++ b/src/NServiceBus.Core/MessageSessionExtensions.cs @@ -1,8 +1,11 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -15,6 +18,10 @@ public static class MessageSessionExtensions /// The instance of to use for the action. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -23,17 +30,51 @@ public static Task Send(this IMessageSession session, object message, Cancellati return session.Send(message, new SendOptions(), cancellationToken); } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + return session.Send(message, new SendOptions(), cancellationToken); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Send(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return session.Send(message, messageType, new SendOptions(), cancellationToken); + } + /// /// Instantiates a message of and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// A to observe. /// /// The message will be sent to the destination configured for . /// - public static Task Send(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -48,6 +89,10 @@ public static Task Send(this IMessageSession session, Action messageConstr /// The address of the destination to which the message will be sent. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IMessageSession, string, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IMessageSession session, string destination, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -61,15 +106,61 @@ public static Task Send(this IMessageSession session, string destination, object return session.Send(message, options, cancellationToken); } + /// + /// Sends the typed message to the given destination. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, string destination, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return session.Send(message, options, cancellationToken); + } + + /// + /// Sends the message with the specified message type to the given destination. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Send(this IMessageSession session, string destination, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return session.Send(message, messageType, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it to the given destination. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// The destination to which the message will be sent. /// An action which initializes properties of the message. /// A to observe. - public static Task Send(this IMessageSession session, string destination, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, string destination, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentException.ThrowIfNullOrWhiteSpace(destination); @@ -88,6 +179,10 @@ public static Task Send(this IMessageSession session, string destination, Act /// Object being extended. /// The message to send. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "SendLocal(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task SendLocal(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -100,14 +195,56 @@ public static Task SendLocal(this IMessageSession session, object message, Cance return session.Send(message, options, cancellationToken); } + /// + /// Sends the typed message back to the current endpoint. Shortcut for sendOptions.RouteToThisEndpoint(). + /// + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task SendLocal<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return session.Send(message, options, cancellationToken); + } + + /// + /// Sends the message with the specified message type back to the current endpoint. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task SendLocal(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return session.Send(message, messageType, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it back to the current endpoint. Shortcut for sendOptions.RouteToThisEndpoint(). /// - /// The type of message, usually an interface. + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. /// A to observe. - public static Task SendLocal(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task SendLocal<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -125,6 +262,10 @@ public static Task SendLocal(this IMessageSession session, Action messageC /// The instance of to use for the action. /// The message to publish. /// A to observe. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(this IMessageSession, T, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Publish(this IMessageSession session, object message, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -133,13 +274,47 @@ public static Task Publish(this IMessageSession session, object message, Cancell return session.Publish(message, new PublishOptions(), cancellationToken); } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to publish. + /// A to observe. + [OverloadResolutionPriority(-1)] + public static Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IMessageSession session, T message, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + + return session.Publish(message, new PublishOptions(), cancellationToken); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// A to observe. + /// or is . + /// is not assignable to . + public static Task Publish(this IMessageSession session, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(session); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return session.Publish(message, messageType, new PublishOptions(), cancellationToken); + } + /// /// Publish the message to subscribers. /// /// The instance of to use for the action. /// A to observe. /// The message type. - public static Task Publish(this IMessageSession session, CancellationToken cancellationToken = default) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); @@ -149,11 +324,11 @@ public static Task Publish(this IMessageSession session, CancellationToken ca /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// A to observe. - public static Task Publish(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IMessageSession session, Action messageConstructor, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 1aa0b8f960e..254b1a91285 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -6,21 +6,26 @@ true ..\NServiceBus.snk true + true + + + + - + - + - + @@ -28,7 +33,7 @@ NServiceBus Build, version, and monitor better microservices with the most powerful service platform for .NET - $(TargetsForTfmSpecificContentInPackage);AddPropsFileToPackage + $(TargetsForTfmSpecificContentInPackage);AddPropsAndTargetsFilesToPackage @@ -40,15 +45,15 @@ - + + - diff --git a/src/NServiceBus.Core/NServiceBus.props b/src/NServiceBus.Core/NServiceBus.props index f4dd9059c3f..f2fa7ae714f 100644 --- a/src/NServiceBus.Core/NServiceBus.props +++ b/src/NServiceBus.Core/NServiceBus.props @@ -8,4 +8,13 @@ $(InterceptorsNamespaces);NServiceBus + + + + + + + + + diff --git a/src/NServiceBus.Core/NServiceBus.targets b/src/NServiceBus.Core/NServiceBus.targets new file mode 100644 index 00000000000..969762b0adb --- /dev/null +++ b/src/NServiceBus.Core/NServiceBus.targets @@ -0,0 +1,21 @@ + + + + + true + + + + + + + diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs index 3f784962a9b..023cec6aadb 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityExtensions.cs @@ -11,13 +11,13 @@ static class ActivityExtensions public const string OutgoingActivityKey = "NServiceBus.Diagnostics.Activity.Outgoing"; public const string IncomingActivityKey = "NServiceBus.Diagnostics.Activity.Incoming"; - public static bool TryGetRecordingOutgoingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) - => pipelineContext.TryGetRecordingPipelineActivity(OutgoingActivityKey, out activity); + public static bool TryGetOutgoingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) + => pipelineContext.TryGetPipelineActivity(OutgoingActivityKey, out activity); - public static bool TryGetRecordingIncomingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) - => pipelineContext.TryGetRecordingPipelineActivity(IncomingActivityKey, out activity); + public static bool TryGetIncomingPipelineActivity(this ContextBag pipelineContext, [NotNullWhen(true)] out Activity? activity) + => pipelineContext.TryGetPipelineActivity(IncomingActivityKey, out activity); - static bool TryGetRecordingPipelineActivity(this ContextBag pipelineContext, string activityKey, [NotNullWhen(true)] out Activity? activity) + static bool TryGetPipelineActivity(this ContextBag pipelineContext, string activityKey, [NotNullWhen(true)] out Activity? activity) { if (Activity.Current is not null // Cheaper to check than searching the pipeline context to start with. If there is no ambient activity, there can't be an activity in the context. && pipelineContext.TryGet(activityKey, out activity) // Search activity in context bag diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs index 4669e372477..dec4c1374f1 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/ActivityFactory.cs @@ -6,12 +6,12 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; -using Extensibility; -using Logging; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Pipeline; using Transport; -sealed class ActivityFactory(InstrumentationOptions options) : IActivityFactory +sealed partial class ActivityFactory(InstrumentationOptions options) : IActivityFactory { public InstrumentationOptions Options { get; } = options; @@ -223,7 +223,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi } } - public void RecordError(Activity? activity, Exception exception, ContextBag context) + public void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider) { if (activity == null) { @@ -239,7 +239,9 @@ public void RecordError(Activity? activity, Exception exception, ContextBag cont { if (Options.ExceptionRecordingMode == ExceptionRecordingMode.Logs) { - Logger.Error($"An exception occurred while executing '{activity.DisplayName}'.", exception); + // The factory is created before the container exists, so the logger is resolved on first use. + logger ??= serviceProvider.GetRequiredService>(); + LogExceptionWhileExecuting(logger, exception, activity.DisplayName); } else { @@ -257,5 +259,8 @@ public void RecordError(Activity? activity, Exception exception, ContextBag cont const string ExceptionRecordedFlag = "otel.exception.recorded"; - static readonly ILog Logger = LogManager.GetLogger(); + ILogger? logger; + + [LoggerMessage(LogLevel.Error, "An exception occurred while executing '{DisplayName}'.")] + static partial void LogExceptionWhileExecuting(ILogger logger, Exception exception, string displayName); } \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs index 44943a4ce04..edec8ea2b24 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/IActivityFactory.cs @@ -4,7 +4,6 @@ namespace NServiceBus; using System; using System.Diagnostics; -using Extensibility; using Pipeline; using Transport; @@ -16,5 +15,5 @@ interface IActivityFactory Activity? StartHandlerActivity(MessageHandler messageHandler); Activity? StartRecoverabilityActivity(ErrorContext context); void UpdateActivityFromRecoverabilityAction(Activity activity, RecoverabilityAction recoverabilityAction, string receiveAddress); - void RecordError(Activity? activity, Exception exception, ContextBag context); + void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider); } \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs index 5a025496460..b161531fa7b 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/NoOpActivityFactory.cs @@ -4,7 +4,6 @@ namespace NServiceBus; using System; using System.Diagnostics; -using Extensibility; using Pipeline; using Transport; @@ -24,7 +23,7 @@ public void UpdateActivityFromRecoverabilityAction(Activity activity, Recoverabi { } - public void RecordError(Activity? activity, Exception exception, ContextBag context) + public void RecordError(Activity? activity, Exception exception, IServiceProvider serviceProvider) { } } \ No newline at end of file diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs index 0a1a5c50881..4d87387d42f 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/PopulateRecoverabilityTraceMetadataBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs index f5f531b6324..dfa025c6099 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/SubscribeDiagnosticsBehavior.cs @@ -10,7 +10,7 @@ class SubscribeDiagnosticsBehavior : IBehavior next) { - if (context.Extensions.TryGetRecordingOutgoingPipelineActivity(out var activity)) + if (context.Extensions.TryGetOutgoingPipelineActivity(out var activity)) { activity.SetTag(ActivityTags.EventTypes, string.Join(",", (object[])context.EventTypes)); } diff --git a/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs b/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs index fd5f0936f24..93cc7433afe 100644 --- a/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs +++ b/src/NServiceBus.Core/OpenTelemetry/Tracing/UnsubscribeDiagnosticsBehavior.cs @@ -10,7 +10,7 @@ class UnsubscribeDiagnosticsBehavior : IBehavior next) { - if (context.Extensions.TryGetRecordingOutgoingPipelineActivity(out var activity)) + if (context.Extensions.TryGetOutgoingPipelineActivity(out var activity)) { activity.SetTag(ActivityTags.EventTypes, context.EventType.FullName); } diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs index 2b1f974cb14..5a164e15345 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersistence.cs @@ -3,7 +3,10 @@ namespace NServiceBus.Features; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; using Microsoft.Extensions.DependencyInjection; using NServiceBus.Sagas; @@ -26,12 +29,30 @@ public LearningSagaPersistence() protected override void Setup(FeatureConfigurationContext context) { var storageLocation = context.Settings.Get(StorageLocationKey); + var serializerOptions = context.Settings.GetOrDefault(SerializerOptionsKey) ?? GetDefaultOptions(); var allSagas = context.Settings.Get(); - context.Services.AddSingleton(new SagaManifestCollection(allSagas, storageLocation, sagaName => sagaName.Replace("+", ""))); + context.Services.AddSingleton(new SagaManifestCollection(allSagas, storageLocation, sagaName => sagaName.Replace("+", ""), serializerOptions)); context.Services.AddSingleton(); } + static JsonSerializerOptions GetDefaultOptions() + { + var options = new JsonSerializerOptions(); + if (JsonSerializer.IsReflectionEnabledByDefault) + { + options.Converters.Add(CreateDefaultConverter()); + } + return options; + } + + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only used when System.Text.Json reflection is enabled.")] + static JsonStringEnumConverter CreateDefaultConverter() => new(); + internal static readonly string StorageLocationKey = "LearningSagaPersistence.StorageLocation"; + internal static readonly string SerializerOptionsKey = "LearningSagaPersistence.SerializerOptions"; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs index dd0c9579d31..1628ae77f84 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/LearningSagaPersisterConfigurationExtensions.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Text.Json; using Features; /// @@ -22,4 +23,17 @@ public static void SagaStorageDirectory(this PersistenceExtensions + /// Configures the to use for serializing saga data. + /// + /// The persistence extensions to extend. + /// The to use. + public static void SagaSerializerOptions(this PersistenceExtensions persistenceExtensions, JsonSerializerOptions options) + { + ArgumentNullException.ThrowIfNull(persistenceExtensions); + ArgumentNullException.ThrowIfNull(options); + + persistenceExtensions.Settings.Set(LearningSagaPersistence.SerializerOptionsKey, options); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs index 30dc6474a77..996665ca547 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifest.cs @@ -4,11 +4,13 @@ namespace NServiceBus; using System; using System.IO; +using System.Text.Json; class SagaManifest { public required string StorageDirectory { get; init; } public required Type SagaEntityType { get; init; } + public required JsonSerializerOptions SerializerOptions { get; init; } public string GetFilePath(Guid sagaId) => Path.Combine(StorageDirectory, sagaId + ".json"); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs index beccb2f38db..93d14296801 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaManifestCollection.cs @@ -5,12 +5,14 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.IO; +using System.Text.Json; using Sagas; class SagaManifestCollection { - public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocation, Func sagaNameConverter) + public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocation, Func sagaNameConverter, JsonSerializerOptions? serializerOptions = null) { + serializerOptions ??= new JsonSerializerOptions(); foreach (var metadata in sagas) { var sagaStorageDir = Path.Combine(storageLocation, sagaNameConverter(metadata.SagaType.FullName!)); @@ -23,7 +25,8 @@ public SagaManifestCollection(SagaMetadataCollection sagas, string storageLocati var manifest = new SagaManifest { StorageDirectory = sagaStorageDir, - SagaEntityType = metadata.SagaEntityType + SagaEntityType = metadata.SagaEntityType, + SerializerOptions = serializerOptions }; sagaManifests[metadata.SagaEntityType] = manifest; diff --git a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs index 253df6a8213..f4ae89986f9 100644 --- a/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs +++ b/src/NServiceBus.Core/Persistence/Learning/SagaPersister/SagaStorageFile.cs @@ -3,15 +3,20 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; using System.Threading; using System.Threading.Tasks; class SagaStorageFile : IDisposable, IAsyncDisposable { - SagaStorageFile(FileStream fileStream) => this.fileStream = fileStream; + SagaStorageFile(FileStream fileStream, JsonSerializerOptions options) + { + this.fileStream = fileStream; + this.options = options; + } public void Dispose() { @@ -58,17 +63,17 @@ public async ValueTask DisposeAsync() return noSagaFoundResult; } - return OpenWithRetryOnConcurrency(filePath, FileMode.Open, cancellationToken)!; + return OpenWithRetryOnConcurrency(filePath, FileMode.Open, manifest.SerializerOptions, cancellationToken)!; } public static Task Create(Guid sagaId, SagaManifest manifest, CancellationToken cancellationToken = default) { var filePath = manifest.GetFilePath(sagaId); - return OpenWithRetryOnConcurrency(filePath, FileMode.CreateNew, cancellationToken); + return OpenWithRetryOnConcurrency(filePath, FileMode.CreateNew, manifest.SerializerOptions, cancellationToken); } - static async Task OpenWithRetryOnConcurrency(string filePath, FileMode fileAccess, CancellationToken cancellationToken) + static async Task OpenWithRetryOnConcurrency(string filePath, FileMode fileAccess, JsonSerializerOptions options, CancellationToken cancellationToken) { var numRetries = 0; @@ -78,7 +83,7 @@ static async Task OpenWithRetryOnConcurrency(string filePath, F try { - return new SagaStorageFile(new FileStream(filePath, fileAccess, FileAccess.ReadWrite, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous)); + return new SagaStorageFile(new FileStream(filePath, fileAccess, FileAccess.ReadWrite, FileShare.None, DefaultBufferSize, FileOptions.Asynchronous), options); } catch (IOException) { @@ -101,8 +106,19 @@ public async Task Write(IContainSagaData sagaData, CancellationToken cancellatio ObjectDisposedException.ThrowIf(fileStream is null, this); fileStream.Position = 0; - await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Options, cancellationToken) - .ConfigureAwait(false); + + var sagaDataType = sagaData.GetType(); + var typeInfo = options.ResolveTypeInfo(sagaDataType); + if (typeInfo is not null) + { + await JsonSerializer.SerializeAsync(fileStream, sagaData, typeInfo, cancellationToken) + .ConfigureAwait(false); + } + else + { + await SerializeWithReflectionAsync(fileStream, sagaData, sagaDataType, options, cancellationToken) + .ConfigureAwait(false); + } // Because the file is opened in ReadWrite mode, leftover content from last write // could be left behind if the new content is shorter. @@ -115,17 +131,46 @@ await JsonSerializer.SerializeAsync(fileStream, sagaData, sagaData.GetType(), Op { ObjectDisposedException.ThrowIf(fileStream is null, this); - return JsonSerializer.DeserializeAsync(fileStream, Options, cancellationToken); + var typeInfo = options.ResolveTypeInfo(typeof(TSagaData)); + if (typeInfo is not null) + { + return ReadWithTypeInfoAsync(fileStream, typeInfo, cancellationToken); + } + else + { + return DeserializeWithReflectionAsync(fileStream, options, cancellationToken); + } } + static async ValueTask ReadWithTypeInfoAsync(Stream stream, JsonTypeInfo typeInfo, CancellationToken cancellationToken) where TSagaData : class + => (TSagaData?)await JsonSerializer.DeserializeAsync(stream, typeInfo, cancellationToken).ConfigureAwait(false); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static Task SerializeWithReflectionAsync(Stream stream, object value, Type inputType, JsonSerializerOptions options, CancellationToken cancellationToken) + => JsonSerializer.SerializeAsync(stream, value, inputType, options, cancellationToken); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static async ValueTask DeserializeWithReflectionAsync(Stream stream, JsonSerializerOptions options, CancellationToken cancellationToken) where TSagaData : class + => (TSagaData?)await JsonSerializer.DeserializeAsync(stream, typeof(TSagaData), options, cancellationToken).ConfigureAwait(false); + FileStream? fileStream; + readonly JsonSerializerOptions options; bool isCompleted; const int DefaultBufferSize = 4096; static readonly Task noSagaFoundResult = Task.FromResult(null); - - static readonly JsonSerializerOptions Options = new() - { - Converters = { new JsonStringEnumConverter() } - }; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs index 8df2f4bd36f..033156b5bbc 100644 --- a/src/NServiceBus.Core/Persistence/PersistenceComponent.cs +++ b/src/NServiceBus.Core/Persistence/PersistenceComponent.cs @@ -21,7 +21,7 @@ public Configuration Initialize(SettingsHolder settings) } var resultingSupportedStorages = new List<(StorageType Storage, StorageType.Options Options)>(); - var diagnostics = new Dictionary(); + var diagnostics = new Dictionary(); foreach (var enabledPersistence in persistenceSettings.Enabled) { @@ -34,15 +34,15 @@ public Configuration Initialize(SettingsHolder settings) persistenceDefinition.Apply(storageType.Storage, settings.Get()); resultingSupportedStorages.Add(storageType); - diagnostics.Add(storageType.Storage.ToString(), new + diagnostics.Add(storageType.Storage.ToString(), new PersistenceDiagnosticsEntry { - Type = persistenceDefinition.FullName, + Type = persistenceDefinition.FullName!, Version = FileVersionRetriever.GetFileVersion(persistenceDefinition.GetType()) }); } } - settings.AddStartupDiagnosticsSection("Persistence", diagnostics); + settings.AddStartupDiagnosticsSection("Persistence", diagnostics, StartupDiagnosticsJsonContext.Default.DictionaryStringPersistenceDiagnosticsEntry); return new Configuration(settings, persistenceSettings.Enabled, resultingSupportedStorages); } diff --git a/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs b/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs index c8b54f4c2da..6d52bc95c56 100644 --- a/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs +++ b/src/NServiceBus.Core/Pipeline/BehaviorTypeChecker.cs @@ -3,12 +3,13 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using Pipeline; static class BehaviorTypeChecker { - public static void ThrowIfInvalid(Type behavior, string paramName) + public static void ThrowIfInvalid([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string paramName) { ArgumentNullException.ThrowIfNull(behavior); if (behavior.IsAbstract) diff --git a/src/NServiceBus.Core/Pipeline/IBehavior.cs b/src/NServiceBus.Core/Pipeline/IBehavior.cs index 8568c4a48e9..b6f517264a6 100644 --- a/src/NServiceBus.Core/Pipeline/IBehavior.cs +++ b/src/NServiceBus.Core/Pipeline/IBehavior.cs @@ -32,6 +32,7 @@ InvokerNode IBehavior.CreateInvokerNode(InvokerNode? next) else { Func fn = next.Invoke; + // SAFETY: Pipeline wiring guarantees next is invoked only with TOutContext (a reference type); the delegate retype is representation-preserving. nextFunc = Unsafe.As, Func>(ref fn); } diff --git a/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs b/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs index e21f90b406e..566ffd3929f 100644 --- a/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs +++ b/src/NServiceBus.Core/Pipeline/IPipelineTerminator.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; /// /// Marker interface for pipeline terminators. diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs index 4af169ac758..f554a585482 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs @@ -2,7 +2,11 @@ namespace NServiceBus.Pipeline; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; /// /// A context of behavior execution in logical message processing stage. @@ -25,8 +29,40 @@ public interface IIncomingLogicalMessageContext : IIncomingContext bool MessageHandled { get; set; } /// - /// Updates the message instance contained in . + /// Updates the message instance contained in . /// /// The new instance. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] void UpdateMessageInstance(object newInstance); + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + UpdateMessageInstance(newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs index 02fed74a535..e07880c2cfa 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingContext.cs @@ -4,6 +4,8 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -25,32 +27,68 @@ protected IncomingContext(string messageId, string replyToAddress, IReadOnlyDict public IReadOnlyDictionary MessageHeaders { get; } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions options) { return MessageOperations.Send(this, message, options); } - public Task Send(Action messageConstructor, SendOptions options) + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return MessageOperations.Send(this, message, options); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + return MessageOperations.Send(this, message, messageType, options); + } + + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions options) { return MessageOperations.Publish(this, message, options); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions) + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return MessageOperations.Publish(this, message, options); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + return MessageOperations.Publish(this, message, messageType, options); + } + + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Reply(object message, ReplyOptions options) { return MessageOperations.Reply(this, message, options); } - public Task Reply(Action messageConstructor, ReplyOptions options) + [OverloadResolutionPriority(-1)] + public Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { + return MessageOperations.Reply(this, message, options); + } + + public Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + return MessageOperations.Reply(this, message, messageType, options); + } + + public Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, ReplyOptions options) { return MessageOperations.Reply(this, messageConstructor, options); } diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs index 9ed9aee336f..147493cd987 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs @@ -4,7 +4,10 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; +using Particular.Obsoletes; using Pipeline; class IncomingLogicalMessageContext : IncomingContext, IIncomingLogicalMessageContext @@ -28,6 +31,10 @@ public IncomingLogicalMessageContext(LogicalMessage logicalMessage, string messa public bool MessageHandled { get; set; } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public void UpdateMessageInstance(object newInstance) { ArgumentNullException.ThrowIfNull(newInstance); @@ -45,4 +52,41 @@ public void UpdateMessageInstance(object newInstance) Message.Metadata = newLogicalMessage.Metadata; } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + => UpdateMessageInstance(newInstance!, typeof(T)); + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(newInstance); + ArgumentNullException.ThrowIfNull(messageType); + MessageTypeValidator.Validate(newInstance, messageType); + + var sameInstance = ReferenceEquals(Message.Instance, newInstance); + + Message.Instance = newInstance; + + if (sameInstance && Message.Metadata.MessageType == messageType) + { + return; + } + + var factory = Builder.GetRequiredService(); + var newLogicalMessage = factory.Create(messageType, newInstance); + + Message.Metadata = newLogicalMessage.Metadata; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs index e1385d74fef..91d46f88ebc 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/LoadHandlersConnector.cs @@ -65,7 +65,7 @@ public override async Task Invoke(IIncomingLogicalMessageContext context, Func + /// Creates a new using the specified metadata and message instance without invoking the message mapper. + /// + /// The metadata for the message. + /// The message instance. + /// A new . +#pragma warning disable CA1822 // Mark members as static + public LogicalMessage Create(MessageMetadata metadata, object message) + { + ArgumentNullException.ThrowIfNull(metadata); + ArgumentNullException.ThrowIfNull(message); + + return new LogicalMessage(metadata, message); + } +#pragma warning restore CA1822 // Mark members as static + readonly IMessageMapper messageMapper; readonly MessageMetadataRegistry messageMetadataRegistry; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs b/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs index 3f83eb301ea..38cc34cef82 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/MessageHandlerInvoker.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs index 76003cd78f6..8ba0e81163b 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/TransportReceiveToPhysicalMessageConnector.cs @@ -6,20 +6,31 @@ namespace NServiceBus; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; -using Logging; +using Microsoft.Extensions.Logging; using Outbox; using Pipeline; using Routing; using Transport; +using LogLevel = Microsoft.Extensions.Logging.LogLevel; using TransportOperation = Outbox.TransportOperation; -class TransportReceiveToPhysicalMessageConnector( +partial class TransportReceiveToPhysicalMessageConnector( IOutboxStorage outboxStorage, PipelineMetrics pipelineMetrics, - InstrumentationOptions instrumentationOptions) + InstrumentationOptions instrumentationOptions, + ILogger logger) : IStageForkConnector { - public async Task Invoke(ITransportReceiveContext context, Func next) + // When no outbox is configured the storage is a no-op that discards whatever it is handed, so building an + // OutboxMessage for it is pure waste on every message that dispatches anything. + readonly bool outboxEnabled = outboxStorage is not NoOpOutboxStorage; + + // Invoke is deliberately not async: it has no state machine of its own, so exactly one is boxed per + // message, sized for the path actually taken. + public Task Invoke(ITransportReceiveContext context, Func next) + => outboxEnabled ? InvokeWithOutbox(context, next) : InvokeWithoutOutbox(context, next); + + async Task InvokeWithOutbox(ITransportReceiveContext context, Func next) { var processingStartedAt = Stopwatch.GetTimestamp(); var messageId = context.Message.MessageId; @@ -29,6 +40,8 @@ public async Task Invoke(ITransportReceiveContext context, Func 0) { - var batchDispatchContext = this.CreateBatchDispatchContext(pendingTransportOperations.Operations, physicalMessageContext); - - Activity? activity = null; - if (instrumentationOptions.EmitMessageDispatchingEvents) - { - if (context.Extensions.TryGetRecordingIncomingPipelineActivity(out activity)) - { - activity.AddEvent(new ActivityEvent("Start dispatching", tags: new ActivityTagsCollection { { "message-count", batchDispatchContext.Operations.Count } })); - } - } - - await this.Fork(batchDispatchContext).ConfigureAwait(false); - activity?.AddEvent(new ActivityEvent("Finished dispatching")); + var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); + await Dispatch(batchDispatchContext).ConfigureAwait(false); } await outboxStorage.SetAsDispatched(messageId, context.Extensions, context.CancellationToken).ConfigureAwait(false); - if (pendingTransportOperations.HasOperations || deduplicationEntry == null) + if (operations.Length > 0 || deduplicationEntry == null) { pipelineMetrics.RecordCriticalTimeAndTotalProcessed(context); } } + async Task InvokeWithoutOutbox(ITransportReceiveContext context, Func next) + { + // Without an outbox there is nothing to deduplicate against: NoOpOutboxStorage.Get always returns null + // and SetAsDispatched does nothing, so only the fresh-processing path is reachable and neither call is + // made. No outbox transaction is parked in the context either; the storage session substitutes the + // no-op transaction when none is present. + var processingStartedAt = Stopwatch.GetTimestamp(); + var physicalMessageContext = this.CreateIncomingPhysicalMessageContext(context.Message, context); + + var pendingTransportOperations = new PendingTransportOperations(); + physicalMessageContext.Extensions.Set(pendingTransportOperations); + + await next(physicalMessageContext).ConfigureAwait(false); + + // Materialized once: PendingTransportOperations.Operations snapshots a ConcurrentStack on every access. + var operations = pendingTransportOperations.Operations; + + var elapsedTime = Stopwatch.GetElapsedTime(processingStartedAt); + pipelineMetrics.RecordProcessingTime(context, elapsedTime); + + physicalMessageContext.Extensions.Remove(); + + if (operations.Length > 0) + { + var batchDispatchContext = this.CreateBatchDispatchContext(operations, physicalMessageContext); + await Dispatch(batchDispatchContext).ConfigureAwait(false); + } + + pipelineMetrics.RecordCriticalTimeAndTotalProcessed(context); + } + + // Both paths dispatch through here so the event instrumentation lives in one place. The activity is resolved + // from the batch dispatch context; child context bags read through to their parent, so this sees the same + // activity as the root receive context. When the events are off (the pre-v11 default) this returns the Fork + // task directly and adds no state machine of its own. + Task Dispatch(IBatchDispatchContext batchDispatchContext) + { + if (instrumentationOptions.EmitMessageDispatchingEvents && + batchDispatchContext.Extensions.TryGetIncomingPipelineActivity(out var activity)) + { + return DispatchWithEvents(batchDispatchContext, activity); + } + + return this.Fork(batchDispatchContext); + } + + async Task DispatchWithEvents(IBatchDispatchContext batchDispatchContext, Activity activity) + { + activity.AddEvent(new("Start dispatching", tags: new() { { "message-count", batchDispatchContext.Operations.Count } })); + await this.Fork(batchDispatchContext).ConfigureAwait(false); + activity.AddEvent(new("Finished dispatching")); + } + static void ConvertToPendingOperations(OutboxMessage deduplicationEntry, PendingTransportOperations pendingTransportOperations) { foreach (var operation in deduplicationEntry.TransportOperations) { var message = new OutgoingMessage(operation.MessageId, operation.Headers, operation.Body); - pendingTransportOperations.Add( - new Transport.TransportOperation( - message, - DeserializeRoutingStrategy(operation.Options), - operation.Options, - DispatchConsistency.Isolated - )); + pendingTransportOperations.Add(new(message, DeserializeRoutingStrategy(operation.Options), operation.Options, DispatchConsistency.Isolated)); } } static TransportOperation[] ConvertToOutboxOperations(Transport.TransportOperation[] operations) { var transportOperations = new TransportOperation[operations.Length]; - var index = 0; - foreach (var operation in operations) + for (int index = 0; index < operations.Length; index++) { + var operation = operations[index]; SerializeRoutingStrategy(operation.AddressTag, operation.Properties); - transportOperations[index] = new TransportOperation(operation.Message.MessageId, operation.Properties, operation.Message.Body, operation.Message.Headers); - index++; + transportOperations[index] = new(operation.Message.MessageId, operation.Properties, operation.Message.Body, operation.Message.Headers); } + return transportOperations; } - static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static void SerializeRoutingStrategy(AddressTag addressTag, DispatchProperties options) { switch (addressTag) { @@ -137,20 +189,24 @@ static void SerializeRoutingStrategy(AddressTag addressTag, Dictionary options) + static AddressTag DeserializeRoutingStrategy(DispatchProperties? options) { - if (options.Remove("Destination", out var destination)) + if (options is not null) { - return new UnicastAddressTag(destination); - } + if (options.Remove("Destination", out var destination)) + { + return new UnicastAddressTag(destination); + } - if (options.Remove("EventType", out var eventType)) - { - return new MulticastAddressTag(Type.GetType(eventType, true)); + if (options.Remove("EventType", out var eventType)) + { + return new MulticastAddressTag(Type.GetType(eventType, true)); + } } throw new Exception("Could not find routing strategy to deserialize"); } - static readonly ILog Log = LogManager.GetLogger(); + [LoggerMessage(LogLevel.Information, "Outbox duplicate detected for message '{MessageId}'. Skipping handler execution")] + partial void LogOutboxDuplicateDetectedForMessageMessageIdSkippingHandlerExecution(string messageId); } diff --git a/src/NServiceBus.Core/Pipeline/InvokerNode.cs b/src/NServiceBus.Core/Pipeline/InvokerNode.cs index 0094f5d5578..21d1116e99f 100644 --- a/src/NServiceBus.Core/Pipeline/InvokerNode.cs +++ b/src/NServiceBus.Core/Pipeline/InvokerNode.cs @@ -30,6 +30,7 @@ sealed class InvokerNode(IBehavior(ref obj), next); } } diff --git a/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs b/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs index 89f2426c57b..a43a9ff3072 100644 --- a/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs +++ b/src/NServiceBus.Core/Pipeline/MainPipelineExecutor.cs @@ -60,7 +60,7 @@ public async Task Invoke(MessageContext messageContext, CancellationToken cancel catch (Exception ex) #pragma warning restore PS0019 // Do not catch Exception without considering OperationCanceledException { - activityFactory.RecordError(activity, ex, transportReceiveContext.Extensions); + activityFactory.RecordError(activity, ex, transportReceiveContext.Builder); ex.Data["Message ID"] = message.MessageId; if (message.NativeMessageId != message.MessageId) diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs index abe07c83e89..b39e249a71e 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs @@ -2,7 +2,11 @@ namespace NServiceBus.Pipeline; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; using Routing; /// @@ -23,5 +27,34 @@ public interface IOutgoingLogicalMessageContext : IOutgoingContext /// /// Updates the message instance. /// + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessage(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] void UpdateMessage(object newInstance); + + /// + /// Updates the message instance while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) => UpdateMessage(newInstance!, typeof(T)); + + /// + /// Updates the message instance with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + UpdateMessage(newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs index 1a5c401f0c3..03dc8bed05b 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingContext.cs @@ -4,6 +4,8 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Pipeline; @@ -22,22 +24,46 @@ protected OutgoingContext(string messageId, Dictionary headers, public Dictionary Headers { get; } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions options) { return MessageOperations.Send(this, message, options); } - public Task Send(Action messageConstructor, SendOptions options) + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { + return MessageOperations.Send(this, message, options); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + return MessageOperations.Send(this, message, messageType, options); + } + + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions options) { return MessageOperations.Send(this, messageConstructor, options); } + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions options) { return MessageOperations.Publish(this, message, options); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions) + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { + return MessageOperations.Publish(this, message, options); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + return MessageOperations.Publish(this, message, messageType, options); + } + + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions) { return MessageOperations.Publish(this, messageConstructor, publishOptions); } diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs index 77baff82904..642166c4e76 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessage.cs @@ -3,6 +3,7 @@ namespace NServiceBus.Pipeline; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a logical message about to be push out to the transport. @@ -13,7 +14,7 @@ public class OutgoingLogicalMessage /// Initializes the message with a explicit message type and instance. Use this constructor if the message type is /// different from the instance type. /// - public OutgoingLogicalMessage(Type messageType, object message) + public OutgoingLogicalMessage([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message) { ArgumentNullException.ThrowIfNull(messageType); ArgumentNullException.ThrowIfNull(message); @@ -23,8 +24,9 @@ public OutgoingLogicalMessage(Type messageType, object message) } /// - /// The of the message instance. + /// The declared logical message type used for routing and the enclosed-message-types header. It may differ from the runtime type of . /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } /// diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs index dde65f516d6..29a07562852 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/OutgoingLogicalMessageContext.cs @@ -4,6 +4,9 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; using Pipeline; using Routing; @@ -21,6 +24,10 @@ public OutgoingLogicalMessageContext(string messageId, Dictionary RoutingStrategies { get; } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessage(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public void UpdateMessage(object newInstance) { ArgumentNullException.ThrowIfNull(newInstance); @@ -30,4 +37,32 @@ public void UpdateMessage(object newInstance) Message = new OutgoingLogicalMessage(newInstance.GetType(), newInstance); } } + + /// + /// Replaces the current message with the provided typed message instance. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The new message instance. + [OverloadResolutionPriority(-1)] + public void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + ArgumentNullException.ThrowIfNull(newInstance); + + if (Message.Instance != (object)newInstance || Message.MessageType != typeof(T)) + { + Message = new OutgoingLogicalMessage(typeof(T), newInstance); + } + } + + public void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(newInstance); + ArgumentNullException.ThrowIfNull(messageType); + MessageTypeValidator.Validate(newInstance, messageType); + + if (Message.Instance != newInstance || Message.MessageType != messageType) + { + Message = new OutgoingLogicalMessage(messageType, newInstance); + } + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/RoutingToDispatchConnector.cs b/src/NServiceBus.Core/Pipeline/Outgoing/RoutingToDispatchConnector.cs index 633dc2bd25c..e5858a1c6ee 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/RoutingToDispatchConnector.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/RoutingToDispatchConnector.cs @@ -59,7 +59,7 @@ public override Task Invoke(IRoutingContext context, FuncThe new to use. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, Type newBehavior, string? description = null) + public void Replace(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type newBehavior, string? description = null) { BehaviorTypeChecker.ThrowIfInvalid(newBehavior, nameof(newBehavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -43,7 +44,7 @@ public void Replace(string stepId, Type newBehavior, string? description = null) /// The new to use. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, T newBehavior, string? description = null) + public void Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T newBehavior, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(newBehavior)); @@ -62,7 +63,7 @@ public void Replace(string stepId, T newBehavior, string? description = null) /// The factory method to create new instances of the behavior. /// The description of the new behavior. /// Throws an exception when the stepId cannot be found in the pipeline. - public void Replace(string stepId, Func factoryMethod, string? description = null) + public void Replace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "newBehavior"); @@ -80,7 +81,7 @@ public void Replace(string stepId, Func factoryMethod, s /// The identifier of the step to replace its implementation. /// The new to use. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, Type behavior, string? description = null) + public void RegisterOrReplace(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description = null) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -98,7 +99,7 @@ public void RegisterOrReplace(string stepId, Type behavior, string? description /// The identifier of the step to replace its implementation. /// The new to use. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, T behavior, string? description = null) + public void RegisterOrReplace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); @@ -117,7 +118,7 @@ public void RegisterOrReplace(string stepId, T behavior, string? description /// The identifier of the step to replace its implementation. /// The factory method to create new instances of the behavior. /// The description of the new behavior. - public void RegisterOrReplace(string stepId, Func factoryMethod, string? description = null) + public void RegisterOrReplace<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string? description = null) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -135,7 +136,7 @@ public void RegisterOrReplace(string stepId, Func factor /// /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string description) => Register(typeof(TBehavior), description); + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] TBehavior>(string description) => Register(typeof(TBehavior), description); /// /// Register a new step into the pipeline. @@ -143,7 +144,7 @@ public void RegisterOrReplace(string stepId, Func factor /// The to execute. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(Type behavior, string description) + public void Register([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string description) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); @@ -157,7 +158,7 @@ public void Register(Type behavior, string description) /// The to execute. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, Type behavior, string description) + public void Register(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string description) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); EnsureWriteEnabled(stepId, nameof(Register)); @@ -176,7 +177,7 @@ public void Register(string stepId, Type behavior, string description) /// A callback that creates the behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(Func factoryMethod, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(Func factoryMethod, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -191,7 +192,7 @@ public void Register(Func factoryMethod, string descript /// A callback that creates the behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, Func factoryMethod, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, Func factoryMethod, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), "behavior"); @@ -211,7 +212,7 @@ public void Register(string stepId, Func factoryMethod, /// The behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(T behavior, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(T behavior, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); @@ -226,7 +227,7 @@ public void Register(T behavior, string description) /// The behavior instance. /// The description of the behavior. /// Throws an exception when this behavior is already present in the pipeline. - public void Register(string stepId, T behavior, string description) + public void Register<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] T>(string stepId, T behavior, string description) where T : IBehavior { BehaviorTypeChecker.ThrowIfInvalid(typeof(T), nameof(behavior)); diff --git a/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs b/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs index bc8feb88fde..6dd97be1ca1 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterOrReplaceStep.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Pipeline; sealed class RegisterOrReplaceStep @@ -18,7 +19,7 @@ sealed class RegisterOrReplaceStep public ReplaceStep ReplaceStep { get; } public string StepId { get; } - public static RegisterOrReplaceStep Create(string stepId, Type behaviorType, string? description = null, Func? factoryMethod = null) + public static RegisterOrReplaceStep Create(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behaviorType, string? description = null, Func? factoryMethod = null) { var register = RegisterStep.Create(stepId, behaviorType, description, factoryMethod); var replace = new ReplaceStep(stepId, behaviorType, description, factoryMethod); diff --git a/src/NServiceBus.Core/Pipeline/RegisterStep.cs b/src/NServiceBus.Core/Pipeline/RegisterStep.cs index 2dba3729dfc..6ddd6137d22 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterStep.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterStep.cs @@ -21,7 +21,7 @@ public abstract class RegisterStep /// The type of to register. /// A brief description of what this step does. /// A factory method for creating the behavior. - protected RegisterStep(string stepId, Type behavior, string? description, Func? factoryMethod = null) + protected RegisterStep(string stepId, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description, Func? factoryMethod = null) { BehaviorTypeChecker.ThrowIfInvalid(behavior, nameof(behavior)); ArgumentException.ThrowIfNullOrWhiteSpace(stepId); @@ -53,6 +53,7 @@ protected RegisterStep(string stepId, Type behavior, string? description, Func /// Gets the type of that is being registered. /// + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] public Type BehaviorType { get; private set; } internal Type BehaviorInterfaceType { get; private set; } @@ -132,11 +133,11 @@ internal void Replace(ReplaceStep replacement) internal IBehavior CreateBehavior(IServiceProvider defaultBuilder) => factoryMethod(defaultBuilder); - internal static RegisterStep Create(string pipelineStep, Type behavior, string? description, Func? factoryMethod = null) + internal static RegisterStep Create(string pipelineStep, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description, Func? factoryMethod = null) => new DefaultRegisterStep(behavior, pipelineStep, description, factoryMethod); [MemberNotNull(nameof(BehaviorInterfaceType), nameof(InputContextType), nameof(OutputContextType))] - void UpdateBehaviorMetadata(Type behaviorType) + void UpdateBehaviorMetadata([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); @@ -152,7 +153,7 @@ void UpdateBehaviorMetadata(Type behaviorType) Func DefaultFactoryMethod => provider => (IBehavior)ActivatorUtilities.CreateInstance(provider, BehaviorType); class DefaultRegisterStep( - Type behavior, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string stepId, string? description, Func? factoryMethod) diff --git a/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs b/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs index 4cfa6510978..7555fdffab4 100644 --- a/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs +++ b/src/NServiceBus.Core/Pipeline/RegisterStepExtensions.cs @@ -3,26 +3,27 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Pipeline; static class RegisterStepExtensions { - public static bool IsBehavior(this Type behaviorType) => + public static bool IsBehavior([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) => behaviorType.GetInterfaces() .Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == BehaviorInterfaceType); - public static Type GetBehaviorInterface(this Type behaviorType) => + public static Type GetBehaviorInterface([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) => behaviorType.GetInterfaces() .First(x => x.IsGenericType && x.GetGenericTypeDefinition() == BehaviorInterfaceType); - public static Type GetOutputContext(this Type behaviorType) + public static Type GetOutputContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); return behaviorInterface.GetGenericArguments()[1]; } - public static Type GetInputContext(this Type behaviorType) + public static Type GetInputContext([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] this Type behaviorType) { var behaviorInterface = behaviorType.GetBehaviorInterface(); return behaviorInterface.GetGenericArguments()[0]; diff --git a/src/NServiceBus.Core/Pipeline/ReplaceStep.cs b/src/NServiceBus.Core/Pipeline/ReplaceStep.cs index 06b9d613c8c..c8e8b05d910 100644 --- a/src/NServiceBus.Core/Pipeline/ReplaceStep.cs +++ b/src/NServiceBus.Core/Pipeline/ReplaceStep.cs @@ -3,16 +3,18 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using Pipeline; sealed class ReplaceStep( string idToReplace, - Type behavior, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] Type behavior, string? description = null, Func? factoryMethod = null) { public string ReplaceId { get; } = idToReplace; public string? Description { get; } = description; + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.Interfaces)] public Type BehaviorType { get; } = behavior; public Func? FactoryMethod { get; } = factoryMethod; public int RegistrationOrder { get; set; } diff --git a/src/NServiceBus.Core/PipelineContextExtensions.cs b/src/NServiceBus.Core/PipelineContextExtensions.cs index d7d54aac15b..df3008f7ec8 100644 --- a/src/NServiceBus.Core/PipelineContextExtensions.cs +++ b/src/NServiceBus.Core/PipelineContextExtensions.cs @@ -3,7 +3,10 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; +using Particular.Obsoletes; /// /// Syntactic sugar for . @@ -15,6 +18,10 @@ public static class PipelineContextExtensions /// /// The instance of to use for the action. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -23,16 +30,48 @@ public static Task Send(this IPipelineContext context, object message) return context.Send(message, new SendOptions()); } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Send(message, new SendOptions()); + } + + /// + /// Sends the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Send(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Send(message, messageType, new SendOptions()); + } + /// /// Instantiates a message of and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. /// /// The message will be sent to the destination configured for . /// - public static Task Send(this IPipelineContext context, Action messageConstructor) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -46,6 +85,10 @@ public static Task Send(this IPipelineContext context, Action messageConst /// The instance of to use for the action. /// The address of the destination to which the message will be sent. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(this IPipelineContext, string, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Send(this IPipelineContext context, string destination, object message) { ArgumentNullException.ThrowIfNull(context); @@ -59,14 +102,58 @@ public static Task Send(this IPipelineContext context, string destination, objec return context.Send(message, options); } + /// + /// Sends the typed message to the given destination. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, string destination, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return context.Send(message, options); + } + + /// + /// Sends the message with the specified message type to the given destination. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The destination to which the message will be sent. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Send(this IPipelineContext context, string destination, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentException.ThrowIfNullOrWhiteSpace(destination); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.SetDestination(destination); + + return context.Send(message, messageType, options); + } + /// /// Instantiates a message of type T and sends it to the given destination. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// The destination to which the message will be sent. /// An action which initializes properties of the message. - public static Task Send(this IPipelineContext context, string destination, Action messageConstructor) + public static Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, string destination, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentException.ThrowIfNullOrWhiteSpace(destination); @@ -84,6 +171,10 @@ public static Task Send(this IPipelineContext context, string destination, Ac /// /// Object being extended. /// The message to send. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "SendLocal(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task SendLocal(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -96,13 +187,53 @@ public static Task SendLocal(this IPipelineContext context, object message) return context.Send(message, options); } + /// + /// Sends the typed message back to the current endpoint. + /// + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// Object being extended. + /// The message to send. + [OverloadResolutionPriority(-1)] + public static Task SendLocal<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return context.Send(message, options); + } + + /// + /// Sends the message with the specified message type back to the current endpoint. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// Object being extended. + /// The message to send. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task SendLocal(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + var options = new SendOptions(); + + options.RouteToThisEndpoint(); + + return context.Send(message, messageType, options); + } + /// /// Instantiates a message of type T and sends it back to the current endpoint. /// - /// The type of message, usually an interface. + /// The type used to send the message locally. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// Object being extended. /// An action which initializes properties of the message. - public static Task SendLocal(this IPipelineContext context, Action messageConstructor) + public static Task SendLocal<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); @@ -119,6 +250,10 @@ public static Task SendLocal(this IPipelineContext context, Action message /// /// The instance of to use for the action. /// The message to publish. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(this IPipelineContext, T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead, or the overload accepting an explicit messageType when the static type is unavailable.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public static Task Publish(this IPipelineContext context, object message) { ArgumentNullException.ThrowIfNull(context); @@ -127,12 +262,44 @@ public static Task Publish(this IPipelineContext context, object message) return context.Publish(message, new PublishOptions()); } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The instance of to use for the action. + /// The message to publish. + [OverloadResolutionPriority(-1)] + public static Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(this IPipelineContext context, T message) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + + return context.Publish(message, new PublishOptions()); + } + + /// + /// Publishes the provided message with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The instance of to use for the action. + /// The message to publish. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public static Task Publish(this IPipelineContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + + return context.Publish(message, messageType, new PublishOptions()); + } + /// /// Publish the message to subscribers. /// /// The instance of to use for the action. /// The message type. - public static Task Publish(this IPipelineContext context) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context) { ArgumentNullException.ThrowIfNull(context); @@ -142,10 +309,10 @@ public static Task Publish(this IPipelineContext context) /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The instance of to use for the action. /// An action which initializes properties of the message. - public static Task Publish(this IPipelineContext context, Action messageConstructor) + public static Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(this IPipelineContext context, Action messageConstructor) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(messageConstructor); diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs index 4927f658821..53e5c19dca4 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.Configuration.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using NServiceBus.Settings; using Transport; @@ -11,6 +12,7 @@ namespace NServiceBus; partial class ReceiveComponent { + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "The startup diagnostics message schema is best-effort and reflects over application message properties that may be removed when trimming.")] public void AddManifest(HostingComponent.Configuration hostingConfiguration, SettingsHolder settings) { var messageTypes = configuration.MessageHandlerRegistry.GetMessageTypes(); @@ -24,7 +26,7 @@ public void AddManifest(HostingComponent.Configuration hostingConfiguration, Set IsEvent = conventions.IsEventType(type), }); - hostingConfiguration.AddStartupDiagnosticsSection("Manifest-MessageTypes", + hostingConfiguration.AddStartupDiagnosticsSectionFactory("Manifest-MessageTypes", () => handledMessages.Select(handledMessage => new ReceiveComponentManifestMessageType { Name = handledMessage.MessageType.Name, @@ -37,25 +39,28 @@ public void AddManifest(HostingComponent.Configuration hostingConfiguration, Set Name = prop.Name, Type = prop.PropertyType.Name, }).ToArray() - }).ToArray()); + }).ToArray(), StartupDiagnosticsJsonContext.Default.ReceiveComponentManifestMessageTypeArray); } +} - record ReceiveComponentManifestMessageType +sealed record ReceiveComponentManifestMessageType +{ + public sealed record SchemaProperty { - public record SchemaProperty - { - public required string Name { get; init; } - public required string Type { get; init; } - } - public required string Name { get; init; } - public required string FullName { get; init; } - public bool IsMessage { get; init; } - public bool IsEvent { get; init; } - public bool IsCommand { get; init; } - public required SchemaProperty[] Schema { get; init; } + public required string Type { get; init; } } + public required string Name { get; init; } + public required string FullName { get; init; } + public bool IsMessage { get; init; } + public bool IsEvent { get; init; } + public bool IsCommand { get; init; } + public required SchemaProperty[] Schema { get; init; } +} + +partial class ReceiveComponent +{ public static Configuration PrepareConfiguration(Settings settings, TransportSeam transportSeam) { var isSendOnlyEndpoint = settings.IsSendOnlyEndpoint; diff --git a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs index 85213d1e5aa..f25328126dc 100644 --- a/src/NServiceBus.Core/Receiving/ReceiveComponent.cs +++ b/src/NServiceBus.Core/Receiving/ReceiveComponent.cs @@ -9,15 +9,17 @@ namespace NServiceBus; using System.Threading.Tasks; using Logging; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using MicrosoftLoggerFactory = Microsoft.Extensions.Logging.ILoggerFactory; using Outbox; using Pipeline; using Transport; using Unicast; +using ILoggerFactory = Logging.ILoggerFactory; partial class ReceiveComponent { - ReceiveComponent(Configuration configuration, IActivityFactory activityFactory, EndpointLogSlot endpointLogSlot) + internal ReceiveComponent(Configuration configuration, IActivityFactory activityFactory, EndpointLogSlot endpointLogSlot) { this.configuration = configuration; this.activityFactory = activityFactory; @@ -67,12 +69,17 @@ public static ReceiveComponent Configure( pipelineSettings.Register("TransportReceiveToPhysicalMessageProcessingConnector", b => { var storage = b.GetService() ?? new NoOpOutboxStorage(); - return new TransportReceiveToPhysicalMessageConnector(storage, b.GetRequiredService(), hostingConfiguration.ActivityFactory.Options); + return new TransportReceiveToPhysicalMessageConnector( + storage, + b.GetRequiredService(), + hostingConfiguration.ActivityFactory.Options, + b.GetRequiredService>() + ); }, "Allows to abort processing the message"); - pipelineSettings.Register("LoadHandlersConnector", b => new LoadHandlersConnector(b.GetRequiredService(), hostingConfiguration.ActivityFactory, b.GetRequiredService()), "Gets all the handlers to invoke from the MessageHandler registry based on the message type."); + pipelineSettings.Register("LoadHandlersConnector", sp => new LoadHandlersConnector(sp.GetRequiredService(), hostingConfiguration.ActivityFactory, sp.GetRequiredService()), "Gets all the handlers to invoke from the MessageHandler registry based on the message type."); - pipelineSettings.Register("InvokeHandlers", sp => new InvokeHandlerTerminator(sp.GetRequiredService()), "Calls the IHandleMessages.Handle(T)"); + pipelineSettings.Register("InvokeHandlers", static sp => new InvokeHandlerTerminator(sp.GetRequiredService()), "Calls the IHandleMessages.Handle(T)"); var handlerDiagnostics = new Dictionary>(); @@ -113,25 +120,36 @@ public static ReceiveComponent Configure( configuration.TransportSeam.Configure([.. receiveSettings]); - hostingConfiguration.AddStartupDiagnosticsSection("Receiving", new + hostingConfiguration.AddStartupDiagnosticsSection("Receiving", new ReceivingDiagnostics { - configuration.LocalQueueAddress, - configuration.InstanceSpecificQueueAddress, - configuration.PurgeOnStartup, + LocalQueueAddress = ToQueueAddressDiagnostics(configuration.LocalQueueAddress), + InstanceSpecificQueueAddress = configuration.InstanceSpecificQueueAddress != null + ? ToQueueAddressDiagnostics(configuration.InstanceSpecificQueueAddress) + : null, + PurgeOnStartup = configuration.PurgeOnStartup, TransactionMode = configuration.TransportSeam.TransportDefinition.TransportTransactionMode.ToString("G"), - configuration.PushRuntimeSettings.MaxConcurrency, - Satellites = configuration.SatelliteDefinitions.Select(s => new + MaxConcurrency = configuration.PushRuntimeSettings.MaxConcurrency, + Satellites = configuration.SatelliteDefinitions.Select(s => new SatelliteDiagnostics { - s.Name, - s.ReceiveAddress, - s.RuntimeSettings.MaxConcurrency + Name = s.Name, + ReceiveAddress = ToQueueAddressDiagnostics(s.ReceiveAddress), + MaxConcurrency = s.RuntimeSettings.MaxConcurrency }).ToArray(), MessageHandlers = handlerDiagnostics - }); + }, StartupDiagnosticsJsonContext.Default.ReceivingDiagnostics); return receiveComponent; } + static QueueAddressDiagnostics ToQueueAddressDiagnostics(QueueAddress address) => + new QueueAddressDiagnostics + { + BaseAddress = address.BaseAddress, + Discriminator = address.Discriminator, + Properties = new Dictionary(address.Properties), + Qualifier = address.Qualifier + }; + public async Task Initialize( IServiceProvider builder, RecoverabilityComponent recoverabilityComponent, @@ -182,7 +200,7 @@ public async Task Initialize( pipelineComponent, messageOperations, activityFactory - ); + ); await mainPump.Initialize( configuration.PushRuntimeSettings, diff --git a/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs b/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs index 68e39d69794..9bdc540fe58 100644 --- a/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs +++ b/src/NServiceBus.Core/Recoverability/Faults/FailedMessage.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Faults; using System; diff --git a/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs b/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs index 0490fb8202a..91123a17cc0 100644 --- a/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs +++ b/src/NServiceBus.Core/Recoverability/RecoverabilityComponent.cs @@ -60,14 +60,14 @@ public void Initialize( pipelineSettings.Register(sp => new RecoverabilityRoutingConnector(sp.GetRequiredService(), messageRetryNotification, messageFaultedNotification), "Executes the configured retry policy"); - hostingConfiguration.AddStartupDiagnosticsSection("Recoverability", new + hostingConfiguration.AddStartupDiagnosticsSection("Recoverability", new RecoverabilityDiagnostics { ImmediateRetries = recoverabilityConfig.Immediate.MaxNumberOfRetries, DelayedRetries = recoverabilityConfig.Delayed.MaxNumberOfRetries, DelayedRetriesTimeIncrease = recoverabilityConfig.Delayed.TimeIncrease.ToString("g"), - recoverabilityConfig.Failed.ErrorQueue, - UnrecoverableExceptions = recoverabilityConfig.Failed.UnrecoverableExceptionTypes.Select(t => t.FullName).ToArray() - }); + ErrorQueue = recoverabilityConfig.Failed.ErrorQueue, + UnrecoverableExceptions = recoverabilityConfig.Failed.UnrecoverableExceptionTypes.Select(t => t.FullName!).ToArray() + }, StartupDiagnosticsJsonContext.Default.RecoverabilityDiagnostics); } public IRecoverabilityPipelineExecutor CreateRecoverabilityPipelineExecutor( diff --git a/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs b/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs index 436bdcbb4f1..80c29096a49 100644 --- a/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs +++ b/src/NServiceBus.Core/Recoverability/RecoverabilityContext.cs @@ -42,6 +42,7 @@ public RecoverabilityContext( RecoverabilityAction = recoverabilityAction; Extensions.Set(errorContext.TransportTransaction); + Extensions.Set(new IncomingMessage(NativeMessageId, Headers, Body, ReceiveProperties)); } [ObsoleteMetadata(Message = "For access to the message body, headers, native message ID, or the receive properties use the corresponding properties directly exposed on the context", TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] diff --git a/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs b/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs index 195e34d3212..1127f9d6a8b 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/ForceBatchDispatchToBeIsolatedBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs index f1b2ba3bcbe..31f8137568e 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/IOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System.Threading; using System.Threading.Tasks; @@ -9,6 +11,9 @@ /// public interface IOutboxStorage { + //TODO(nullable-major-release): Consider renaming this to TryGet in the next major to better reflect that it might not get a message + // and instead returns null. + /// /// Tries to find the given message in the outbox. /// @@ -16,7 +21,7 @@ public interface IOutboxStorage /// If there is no present for the given then null is /// returned. /// - Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default); + Task Get(string messageId, ContextBag context, CancellationToken cancellationToken = default); /// /// Stores the outbox message to enable deduplication an re-dispatching of related transport operations. @@ -35,4 +40,4 @@ public interface IOutboxStorage /// A to observe. /// The created outbox transaction. Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default); -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs index 66dfe2014cd..ed5c6288979 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxStorage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System.Threading; using System.Threading.Tasks; @@ -7,7 +9,7 @@ class NoOpOutboxStorage : IOutboxStorage { - public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) => NoOutboxMessageTask; + public Task Get(string messageId, ContextBag options, CancellationToken cancellationToken = default) => NoOutboxMessageTask; public Task Store(OutboxMessage message, IOutboxTransaction transaction, ContextBag options, CancellationToken cancellationToken = default) => Task.CompletedTask; @@ -15,6 +17,6 @@ class NoOpOutboxStorage : IOutboxStorage public Task BeginTransaction(ContextBag context, CancellationToken cancellationToken = default) => NoOutboxTransactionTask; - static readonly Task NoOutboxMessageTask = Task.FromResult(null); - static readonly Task NoOutboxTransactionTask = Task.FromResult(new NoOpOutboxTransaction()); + static readonly Task NoOutboxMessageTask = Task.FromResult(null); + static readonly Task NoOutboxTransactionTask = Task.FromResult(NoOpOutboxTransaction.Instance); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs index 28837b238a8..d88103e3fa1 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/NoOpOutboxTransaction.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System.Threading; using System.Threading.Tasks; @@ -6,6 +8,9 @@ sealed class NoOpOutboxTransaction : IOutboxTransaction { + // Stateless, so a single instance can be shared by every message. + public static readonly NoOpOutboxTransaction Instance = new(); + public void Dispose() { } diff --git a/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs b/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs index e5e1f3ef5a0..11188f6db96 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/Outbox.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; using System; using ConsistencyGuarantees; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs index 09e27ce505a..f018bc330f4 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxConfigExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Outbox; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs index 1422ca19eb3..d3fbab82636 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxMessage.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs index 5577b0da43d..a349e3bcc23 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxSettings.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using Configuration.AdvancedExtensibility; using Settings; diff --git a/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs b/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs index 20e8e1a4d6a..7190099d6fb 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/OutboxTransaction.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Outbox; using System; diff --git a/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs b/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs index 2b9b2305a56..f6ee7018fd4 100644 --- a/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs +++ b/src/NServiceBus.Core/Reliability/Outbox/TransportOperation.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Outbox; +#nullable enable + +namespace NServiceBus.Outbox; using System; using System.Collections.Generic; @@ -12,7 +14,7 @@ public class TransportOperation /// /// Creates a new instance of a . /// - public TransportOperation(string messageId, DispatchProperties properties, ReadOnlyMemory body, Dictionary headers) + public TransportOperation(string messageId, DispatchProperties? properties, ReadOnlyMemory body, Dictionary? headers) { ArgumentException.ThrowIfNullOrWhiteSpace(messageId); @@ -30,7 +32,7 @@ public TransportOperation(string messageId, DispatchProperties properties, ReadO /// /// Transport specific dispatch operation properties. /// - public DispatchProperties Options { get; } + public DispatchProperties? Options { get; } /// /// Gets a byte array to the body content of the outgoing message. @@ -40,5 +42,5 @@ public TransportOperation(string messageId, DispatchProperties properties, ReadO /// /// Gets outgoing message headers. /// - public Dictionary Headers { get; } + public Dictionary? Headers { get; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs index 081f9794790..954717d6957 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/CompletableSynchronizedStorageSessionExtensions.cs @@ -1,5 +1,8 @@ +#nullable enable + namespace NServiceBus.Persistence; +using NServiceBus; using System.Threading; using System.Threading.Tasks; using Extensibility; @@ -7,6 +10,8 @@ namespace NServiceBus.Persistence; using Pipeline; using Transport; +// TODO(nullable-major-release): Investigate if these should even be public. If they should, then they need to have argument validtion added. + /// /// Extension methods for . /// @@ -19,7 +24,14 @@ public static class CompletableSynchronizedStorageSessionExtensions /// The context information. public static ValueTask Open(this ICompletableSynchronizedStorageSession session, IIncomingLogicalMessageContext context) { - var outboxTransaction = context.Extensions.Get(); + // An outbox transaction is only in the context when an outbox is configured. Substituting the no-op + // transaction keeps this contract non-nullable without the receive pipeline having to park a + // placeholder in the context on every message. + if (!context.Extensions.TryGet(out var outboxTransaction)) + { + outboxTransaction = NoOpOutboxTransaction.Instance; + } + var transportTransaction = context.Extensions.Get(); return session.Open(outboxTransaction, transportTransaction, context.Extensions, context.CancellationToken); } diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs index a80be5ba8a3..777c90a8b89 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ICompletableSynchronizedStorageSession.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Persistence; +#nullable enable + +namespace NServiceBus.Persistence; using System; using System.Threading; diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs index 05b27b367ce..72a937fb1d4 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/ISynchronizedStorageSession.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Persistence; +#nullable enable + +namespace NServiceBus.Persistence; /// /// Represents a storage session. diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs index 31e7d8e4fa4..0be833c2976 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/NoOpCompletableSynchronizedStorageSession.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System.Threading; diff --git a/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs b/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs index 1cd46f78021..1fa9a2f3e64 100644 --- a/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs +++ b/src/NServiceBus.Core/Reliability/SynchronizedStorage/SynchronizedStorage.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Features; using Microsoft.Extensions.DependencyInjection; @@ -11,5 +13,5 @@ public sealed class SynchronizedStorage : Feature /// /// See . /// - protected override void Setup(FeatureConfigurationContext context) => context.Services.AddScoped(provider => provider.GetService()); + protected override void Setup(FeatureConfigurationContext context) => context.Services.AddScoped(provider => provider.GetRequiredService()); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs index ebdc49ed7bb..04c347fbe58 100644 --- a/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs +++ b/src/NServiceBus.Core/Routing/AssemblyRouteSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing; @@ -11,15 +12,19 @@ class AssemblyRouteSource : IRouteSource readonly Assembly messageAssembly; readonly UnicastRoute route; + [RequiresUnreferencedCode(TrimmingMessage)] public AssemblyRouteSource(Assembly messageAssembly, UnicastRoute route) { this.messageAssembly = messageAssembly; this.route = route; } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateRoutes(Conventions conventions) { - var routes = messageAssembly.GetTypes() + var routes = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t)) .Select(t => new RouteTableEntry(t, route)) .ToArray(); @@ -33,4 +38,6 @@ public IEnumerable GenerateRoutes(Conventions conventions) } public RouteSourcePriority Priority => RouteSourcePriority.Assembly; + + internal const string TrimmingMessage = "Routing messages by assembly or namespace requires assembly scanning and is not supported in trimming scenarios. Register routes by message type instead."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs index ffd556cf15f..0756bc9ad87 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/AssemblyPublisherSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing.MessageDrivenSubscriptions; @@ -11,15 +12,19 @@ class AssemblyPublisherSource : IPublisherSource readonly Assembly messageAssembly; readonly PublisherAddress address; + [RequiresUnreferencedCode(TrimmingMessage)] public AssemblyPublisherSource(Assembly messageAssembly, PublisherAddress address) { this.messageAssembly = messageAssembly; this.address = address; } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(conventions.IsEventType) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); @@ -34,7 +39,7 @@ public IEnumerable GenerateWithBestPracticeEnforcement(Conv public IEnumerable GenerateWithoutBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(type => conventions.IsMessageType(type) && !conventions.IsCommandType(type)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); @@ -48,4 +53,6 @@ public IEnumerable GenerateWithoutBestPracticeEnforcement(C } public RouteSourcePriority Priority => RouteSourcePriority.Assembly; + + internal const string TrimmingMessage = "Registering publishers by assembly or namespace requires assembly scanning and is not supported in trimming scenarios. Register publishers by message type instead."; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs index 5d4210b0601..73ec000e642 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/MessageDrivenSubscriptionsConfigExtensions.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Features; using Pipeline; @@ -62,6 +63,7 @@ public static void RegisterPublisher(this RoutingSettings routingSettings, /// The to extend. /// The assembly containing the event types. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public static void RegisterPublisher(this RoutingSettings routingSettings, Assembly assembly, string publisherEndpoint) where T : TransportDefinition, IMessageDrivenSubscriptionTransport { ArgumentNullException.ThrowIfNull(assembly); @@ -79,6 +81,7 @@ public static void RegisterPublisher(this RoutingSettings routingSettings, /// The assembly containing the event types. /// The namespace containing the event types. The given value must exactly match the target namespace. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public static void RegisterPublisher(this RoutingSettings routingSettings, Assembly assembly, string @namespace, string publisherEndpoint) where T : TransportDefinition, IMessageDrivenSubscriptionTransport { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs index 632ed60f36d..cf970825583 100644 --- a/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs +++ b/src/NServiceBus.Core/Routing/MessageDrivenSubscriptions/NamespacePublisherSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing.MessageDrivenSubscriptions; @@ -12,6 +13,7 @@ class NamespacePublisherSource : IPublisherSource readonly string messageNamespace; readonly PublisherAddress address; + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public NamespacePublisherSource(Assembly messageAssembly, string messageNamespace, PublisherAddress address) { this.messageAssembly = messageAssembly; @@ -19,9 +21,12 @@ public NamespacePublisherSource(Assembly messageAssembly, string messageNamespac this.messageNamespace = messageNamespace; } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateWithBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsEventType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); @@ -36,7 +41,7 @@ public IEnumerable GenerateWithBestPracticeEnforcement(Conv public IEnumerable GenerateWithoutBestPracticeEnforcement(Conventions conventions) { - var entries = messageAssembly.GetTypes() + var entries = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t) && !conventions.IsCommandType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new PublisherTableEntry(t, address)) .ToArray(); diff --git a/src/NServiceBus.Core/Routing/MulticastAddressTag.cs b/src/NServiceBus.Core/Routing/MulticastAddressTag.cs index cc4a509816f..55470047adf 100644 --- a/src/NServiceBus.Core/Routing/MulticastAddressTag.cs +++ b/src/NServiceBus.Core/Routing/MulticastAddressTag.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Routing; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a route that should deliver the message to all interested subscribers. @@ -11,7 +12,7 @@ public class MulticastAddressTag : AddressTag /// Initializes a new instance of . /// /// The event being published. - public MulticastAddressTag(Type messageType) + public MulticastAddressTag([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { MessageType = messageType; } @@ -19,5 +20,6 @@ public MulticastAddressTag(Type messageType) /// /// The event being published. /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs b/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs index 1ca8e9f1983..12e67dc8092 100644 --- a/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs +++ b/src/NServiceBus.Core/Routing/MulticastRoutingStrategy.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; /// /// A routing strategy for multicast routing. @@ -11,7 +12,7 @@ public class MulticastRoutingStrategy : RoutingStrategy /// /// Creates new routing strategy. /// - public MulticastRoutingStrategy(Type messageType) + public MulticastRoutingStrategy([DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { this.messageType = messageType; } @@ -26,5 +27,6 @@ public override AddressTag Apply(Dictionary headers) return new MulticastAddressTag(messageType); } + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] readonly Type messageType; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs index d27837ec285..aae83c6f2c1 100644 --- a/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs +++ b/src/NServiceBus.Core/Routing/NamespaceRouteSource.cs @@ -2,6 +2,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using Routing; @@ -12,6 +13,7 @@ class NamespaceRouteSource : IRouteSource readonly string messageNamespace; readonly UnicastRoute route; + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, UnicastRoute route) { this.messageAssembly = messageAssembly; @@ -19,9 +21,12 @@ public NamespaceRouteSource(Assembly messageAssembly, string messageNamespace, U this.messageNamespace = messageNamespace; } + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Scanning the configured assembly is intentional. Construction is gated by the constructor's RequiresUnreferencedCode annotation, and the scanning members cannot be annotated because they implement an unannotated interface (IL2046).")] + static Type[] ScanAssemblyTypes(Assembly assembly) => assembly.GetTypes(); + public IEnumerable GenerateRoutes(Conventions conventions) { - var routes = messageAssembly.GetTypes() + var routes = ScanAssemblyTypes(messageAssembly) .Where(t => conventions.IsMessageType(t) && string.Equals(t.Namespace, messageNamespace, StringComparison.OrdinalIgnoreCase)) .Select(t => new RouteTableEntry(t, route)) .ToArray(); diff --git a/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs b/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs index 1931281364b..c95bf1e6295 100644 --- a/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/MulticastPublishConnector.cs @@ -9,7 +9,7 @@ class MulticastPublishConnector : StageConnector stage) { - context.Headers[Headers.MessageIntent] = MessageIntent.Publish.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Publish); var logicalMessageContext = this.CreateOutgoingLogicalMessageContext( context.Message, diff --git a/src/NServiceBus.Core/Routing/Routers/SendConnector.cs b/src/NServiceBus.Core/Routing/Routers/SendConnector.cs index fc913a85fa2..415755ee615 100644 --- a/src/NServiceBus.Core/Routing/Routers/SendConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/SendConnector.cs @@ -15,7 +15,7 @@ public SendConnector(UnicastSendRouter unicastSendRouter) public override async Task Invoke(IOutgoingSendContext context, Func stage) { var routingStrategy = unicastSendRouter.Route(context); - context.Headers[Headers.MessageIntent] = MessageIntent.Send.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Send); var logicalMessageContext = this.CreateOutgoingLogicalMessageContext(context.Message, new[] { routingStrategy }, context); try diff --git a/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs b/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs index 9b69eb49e73..9e854b3beee 100644 --- a/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs +++ b/src/NServiceBus.Core/Routing/Routers/UnicastPublishConnector.cs @@ -26,7 +26,7 @@ public override async Task Invoke(IOutgoingPublishContext context, Func /// The assembly whose messages should be routed. /// Destination endpoint. + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public void RouteToEndpoint(Assembly assembly, string destination) { ArgumentNullException.ThrowIfNull(assembly); @@ -55,6 +57,7 @@ public void RouteToEndpoint(Assembly assembly, string destination) /// The assembly whose messages should be routed. /// The namespace of the messages which should be routed. The given value must exactly match the target namespace. /// Destination endpoint. + [RequiresUnreferencedCode(AssemblyRouteSource.TrimmingMessage)] public void RouteToEndpoint(Assembly assembly, string @namespace, string destination) { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationModePublishConnector.cs b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationModePublishConnector.cs index 15faddd2c91..43ae3b4d77b 100644 --- a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationModePublishConnector.cs +++ b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/MigrationModePublishConnector.cs @@ -18,7 +18,7 @@ public MigrationModePublishConnector(DistributionPolicy distributionPolicy, IUni public override async Task Invoke(IOutgoingPublishContext context, Func stage) { - context.Headers[Headers.MessageIntent] = MessageIntent.Publish.ToString(); + context.Headers[Headers.MessageIntent] = nameof(MessageIntent.Publish); var eventType = context.Message.MessageType; var addressLabels = await GetRoutingStrategies(context, eventType).ConfigureAwait(false); diff --git a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs index f72ee85070a..6bb0de704ce 100644 --- a/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs +++ b/src/NServiceBus.Core/Routing/SubscriptionMigrationMode/SubscriptionMigrationModeSettings.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using Configuration.AdvancedExtensibility; using Pipeline; @@ -49,6 +50,7 @@ public void RegisterPublisher(Type eventType, string publisherEndpoint) /// /// The assembly containing the event types. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public void RegisterPublisher(Assembly assembly, string publisherEndpoint) { ArgumentNullException.ThrowIfNull(assembly); @@ -65,6 +67,7 @@ public void RegisterPublisher(Assembly assembly, string publisherEndpoint) /// The assembly containing the event types. /// The namespace containing the event types. The given value must exactly match the target namespace. /// The publisher endpoint. + [RequiresUnreferencedCode(AssemblyPublisherSource.TrimmingMessage)] public void RegisterPublisher(Assembly assembly, string @namespace, string publisherEndpoint) { ArgumentNullException.ThrowIfNull(assembly); diff --git a/src/NServiceBus.Core/Sagas/Saga.cs b/src/NServiceBus.Core/Sagas/Saga.cs index 87e9804a28a..457463da1c1 100644 --- a/src/NServiceBus.Core/Sagas/Saga.cs +++ b/src/NServiceBus.Core/Sagas/Saga.cs @@ -4,9 +4,12 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Extensibility; +using Particular.Obsoletes; /// /// This class is used to define sagas containing data and handling a message. @@ -38,7 +41,7 @@ public abstract class Saga /// /// The context which is used to send the timeout. /// to send timeout . - protected Task RequestTimeout(IMessageHandlerContext context, DateTimeOffset at) where TTimeoutMessageType : new() + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, DateTimeOffset at) where TTimeoutMessageType : new() { return RequestTimeout(context, at, new TTimeoutMessageType()); } @@ -49,7 +52,7 @@ public abstract class Saga /// The context which is used to send the timeout. /// to send timeout . /// The message to send after is reached. - protected Task RequestTimeout(IMessageHandlerContext context, DateTimeOffset at, TTimeoutMessageType timeoutMessage) + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, DateTimeOffset at, TTimeoutMessageType timeoutMessage) { VerifySagaCanHandleTimeout(timeoutMessage); @@ -60,7 +63,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex SetTimeoutHeaders(options); - return context.Send(timeoutMessage, options); + return context.Send(timeoutMessage, options); } /// @@ -68,7 +71,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex /// /// The context which is used to send the timeout. /// Given to delay timeout message by. - protected Task RequestTimeout(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new() + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within) where TTimeoutMessageType : new() { return RequestTimeout(context, within, new TTimeoutMessageType()); } @@ -79,7 +82,7 @@ protected Task RequestTimeout(IMessageHandlerContext contex /// The context which is used to send the timeout. /// Given to delay timeout message by. /// The message to send after expires. - protected Task RequestTimeout(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage) + protected Task RequestTimeout<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TTimeoutMessageType>(IMessageHandlerContext context, TimeSpan within, TTimeoutMessageType timeoutMessage) { VerifySagaCanHandleTimeout(timeoutMessage); @@ -90,13 +93,38 @@ protected Task RequestTimeout(IMessageHandlerContext contex SetTimeoutHeaders(sendOptions); - return context.Send(timeoutMessage, sendOptions); + return context.Send(timeoutMessage, sendOptions); } /// /// Sends the using the bus to the endpoint that caused this saga to start. /// + [ObsoleteMetadata(ReplacementTypeOrMember = "ReplyToOriginator", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'ReplyToOriginator' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] protected Task ReplyToOriginator(IMessageHandlerContext context, object message, IReadOnlyDictionary? outgoingHeaders = null) + { + var options = BuildReplyToOriginatorOptions(outgoingHeaders); + return context.Reply(message, options); + } + + /// + /// Sends the typed using the bus to the endpoint that caused this saga to start. + /// + /// The type used to reply. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The context of the currently handled message. + /// The message to send. + /// The headers to attach to the outgoing message. + [OverloadResolutionPriority(-1)] + protected Task ReplyToOriginator<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IMessageHandlerContext context, T message, IReadOnlyDictionary? outgoingHeaders = null) + { + var options = BuildReplyToOriginatorOptions(outgoingHeaders); + return context.Reply(message, options); + } + + ReplyOptions BuildReplyToOriginatorOptions(IReadOnlyDictionary? outgoingHeaders) { if (string.IsNullOrEmpty(Entity.Originator)) { @@ -122,7 +150,7 @@ protected Task ReplyToOriginator(IMessageHandlerContext context, object message, SagaIdToUse = null }); - return context.Reply(message, options); + return options; } /// diff --git a/src/NServiceBus.Core/Sagas/SagaComponent.cs b/src/NServiceBus.Core/Sagas/SagaComponent.cs index 48655bfe305..b66a10a1868 100644 --- a/src/NServiceBus.Core/Sagas/SagaComponent.cs +++ b/src/NServiceBus.Core/Sagas/SagaComponent.cs @@ -54,6 +54,7 @@ public Settings(SettingsHolder settings) settings.SetDefault(new SagaMetadataCollection()); } + [RequiresDynamicCode("Saga discovery using assembly scanning might rely on dynamic code generation which is not available with Ahead of Time compilation.")] [RequiresUnreferencedCode("Saga discovery using assembly scanning might require access to unreferenced code")] public void AddDiscoveredSagas(IEnumerable availableTypes) { diff --git a/src/NServiceBus.Core/Sagas/SagaMetadata.cs b/src/NServiceBus.Core/Sagas/SagaMetadata.cs index 2fdacc9db6c..7b348cc5234 100644 --- a/src/NServiceBus.Core/Sagas/SagaMetadata.cs +++ b/src/NServiceBus.Core/Sagas/SagaMetadata.cs @@ -86,6 +86,7 @@ public bool TryGetFinder(string messageType, [NotNullWhen(true)] out SagaFinderD /// /// Potential saga types. /// Saga metadata for all the found saga types. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public static IEnumerable CreateMany(IEnumerable sagaTypes) { @@ -108,6 +109,7 @@ public static IEnumerable CreateMany(IEnumerable sagaTypes) /// /// A type representing a Saga. Must be a non-generic type inheriting from . /// An instance of describing the Saga. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public static SagaMetadata Create<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Saga)] TSaga>() where TSaga : Saga { @@ -143,7 +145,7 @@ public static IEnumerable CreateMany(IEnumerable sagaTypes) var saga = (Saga)RuntimeHelpers.GetUninitializedObject(sagaType); - var mapper = new SagaMapper(sagaType, associatedMessages, propertyAccessors ?? []); + var mapper = new SagaMapper(sagaType, associatedMessages, propertyAccessors ?? [], correlationPropertyAccessor); saga.ConfigureHowToFindSaga(mapper); @@ -256,7 +258,8 @@ static Type GetBaseSagaType(Type t) static readonly MethodInfo CreateSagaOfTSagaTEntityMethod = typeof(SagaMetadata) .GetMethod(nameof(Create), 2, BindingFlags.Public | BindingFlags.Static, [typeof(IReadOnlyCollection), typeof(CorrelationPropertyAccessor), typeof(IReadOnlyCollection)]) ?? throw new MissingMethodException(nameof(Create)); - internal const string TrimmingMessage = "Saga discovery using assembly scanning might require access to unreferenced code"; + internal const string TrimmingMessage = "Saga discovery using assembly scanning might require access to unreferenced code."; + internal const string DynamicCodeMessage = "Saga discovery using assembly scanning might rely on dynamic code generation which is not available with Ahead of Time compilation."; /// /// Details about a saga data property used to correlate messages hitting the saga. diff --git a/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs b/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs index 8a501b3c659..ae32ca15664 100644 --- a/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs +++ b/src/NServiceBus.Core/Sagas/SagaPersistenceBehavior.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Logging; @@ -12,6 +13,7 @@ class SagaPersistenceBehavior(ISagaPersister persister, ISagaIdGenerator sagaIdGenerator, SagaMetadataCollection sagaMetadataCollection, IServiceProvider serviceProvider, PipelineMetrics pipelineMetrics) : IBehavior { + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Best-effort saga type resolution from saga headers; when trimming removes the type, processing falls back to querying the persister for the current saga type.")] public async Task Invoke(IInvokeHandlerContext context, Func next) { var isTimeoutMessage = IsTimeoutMessage(context.Headers); diff --git a/src/NServiceBus.Core/Serialization/IMessageSerializer.cs b/src/NServiceBus.Core/Serialization/IMessageSerializer.cs index 680807eea95..b2e52fa5a1f 100644 --- a/src/NServiceBus.Core/Serialization/IMessageSerializer.cs +++ b/src/NServiceBus.Core/Serialization/IMessageSerializer.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus.Serialization; using System; @@ -30,5 +32,5 @@ public interface IMessageSerializer /// serialized data. /// /// Deserialized messages. - object[] Deserialize(ReadOnlyMemory body, IList messageTypes = null); + object[] Deserialize(ReadOnlyMemory body, IList? messageTypes = null); } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs index f9a6cf6f80e..77cf076ef1d 100644 --- a/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationConfigExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Configuration.AdvancedExtensibility; diff --git a/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs index bc4e657c085..e37423243be 100644 --- a/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationContextExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serialization/SerializationDefinition.cs b/src/NServiceBus.Core/Serialization/SerializationDefinition.cs index 0106f5c06d1..06e93d5183c 100644 --- a/src/NServiceBus.Core/Serialization/SerializationDefinition.cs +++ b/src/NServiceBus.Core/Serialization/SerializationDefinition.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Serialization; +#nullable enable + +namespace NServiceBus.Serialization; using System; using MessageInterfaces; diff --git a/src/NServiceBus.Core/Serialization/SerializationExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationExtensions.cs index 4c720ecb7be..2c0d46a06d7 100644 --- a/src/NServiceBus.Core/Serialization/SerializationExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationExtensions.cs @@ -1,5 +1,8 @@ -namespace NServiceBus.Serialization; +#nullable enable +namespace NServiceBus.Serialization; + +using System; using Configuration.AdvancedExtensibility; using Settings; @@ -13,7 +16,10 @@ public class SerializationExtensions : ExposeSettings where T : Serialization /// Initializes a new instance of . /// public SerializationExtensions(SettingsHolder serializerSettings, SettingsHolder endpointConfigurationSettings) : base(serializerSettings) - => EndpointConfigurationSettings = endpointConfigurationSettings; + { + ArgumentNullException.ThrowIfNull(endpointConfigurationSettings); + EndpointConfigurationSettings = endpointConfigurationSettings; + } // provides access to the settings backing EndpointConfiguration. The settings provided by the 'Settings' property are isolated settings for the serializer. internal readonly SettingsHolder EndpointConfigurationSettings; diff --git a/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs index efcc9eebeec..99a4602bc40 100644 --- a/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationExtensionsExtensions.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using Serialization; diff --git a/src/NServiceBus.Core/Serialization/SerializationFeature.cs b/src/NServiceBus.Core/Serialization/SerializationFeature.cs index ca9ee82a60b..a07229d8c1d 100644 --- a/src/NServiceBus.Core/Serialization/SerializationFeature.cs +++ b/src/NServiceBus.Core/Serialization/SerializationFeature.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; @@ -29,7 +31,7 @@ protected override void Setup(FeatureConfigurationContext context) var additionalDeserializerDefinitions = context.Settings.GetAdditionalSerializers(); var additionalDeserializers = new List(); - var additionalDeserializerDiagnostics = new List(); + var additionalDeserializerDiagnostics = new List(); foreach (var definitionAndSettings in additionalDeserializerDefinitions) { var deserializer = CreateMessageSerializer(definitionAndSettings, mapper, settings); @@ -37,11 +39,11 @@ protected override void Setup(FeatureConfigurationContext context) var deserializerType = definitionAndSettings.Item1.GetType(); - additionalDeserializerDiagnostics.Add(new + additionalDeserializerDiagnostics.Add(new AdditionalDeserializerDiagnostics { - Type = deserializerType.FullName, + Type = deserializerType.FullName!, Version = FileVersionRetriever.GetFileVersion(deserializerType), - deserializer.ContentType + ContentType = deserializer.ContentType }); } @@ -58,17 +60,17 @@ protected override void Setup(FeatureConfigurationContext context) LogFoundMessages(messageMetadataRegistry.GetAllMessages()); - context.Settings.AddStartupDiagnosticsSection("Serialization", new + context.Settings.AddStartupDiagnosticsSection("Serialization", new SerializationDiagnostics { - MainSerializer = new + MainSerializer = new MainSerializerDiagnostics { - Type = mainSerializerAndDefinition.Item1.GetType().FullName, + Type = mainSerializerAndDefinition.Item1.GetType().FullName!, Version = FileVersionRetriever.GetFileVersion(mainSerializerAndDefinition.Item1.GetType()), - mainSerializer.ContentType + ContentType = mainSerializer.ContentType }, AdditionalDeserializers = additionalDeserializerDiagnostics, AllowMessageTypeInference = allowMessageTypeInference - }); + }, StartupDiagnosticsJsonContext.Default.SerializationDiagnostics); } static IMessageSerializer CreateMessageSerializer(Tuple definitionAndSettings, IMessageMapper mapper, IReadOnlySettings mainSettings) diff --git a/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs b/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs index 4c8147f8439..89d274c9fa4 100644 --- a/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs +++ b/src/NServiceBus.Core/Serialization/SerializationSettingsExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs b/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs new file mode 100644 index 00000000000..9df21b619ee --- /dev/null +++ b/src/NServiceBus.Core/Serializers/JsonSerializerOptionsExtensions.cs @@ -0,0 +1,24 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +static class JsonSerializerOptionsExtensions +{ + extension(JsonSerializerOptions? options) + { + public JsonTypeInfo? ResolveTypeInfo(Type runtimeType) + { + var typeInfo = options?.TypeInfoResolver?.GetTypeInfo(runtimeType, options); + if (typeInfo is not null) + { + return typeInfo; + } + + return JsonSerializer.IsReflectionEnabledByDefault ? null : throw new InvalidOperationException($"No JSON metadata was found for '{runtimeType.FullName}'."); + } + } +} diff --git a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs index b4c998de5ce..60847618f43 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/JsonMessageSerializer.cs @@ -1,8 +1,10 @@ #nullable enable + namespace NServiceBus.Serializers.SystemJson; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text.Json; @@ -27,7 +29,18 @@ public JsonMessageSerializer(JsonSerializerOptions? serializerOptions, string co public string ContentType { get; } public void Serialize(object message, Stream stream) - => JsonSerializer.Serialize(stream, message, serializerOptions); + { + var messageType = message.GetType(); + var typeInfo = serializerOptions.ResolveTypeInfo(messageType); + if (typeInfo is not null) + { + JsonSerializer.Serialize(stream, message, typeInfo); + } + else + { + SerializeWithReflection(message, stream, messageType, serializerOptions); + } + } public object[] Deserialize(ReadOnlyMemory body, IList? messageTypes = null) { @@ -49,10 +62,41 @@ public object[] Deserialize(ReadOnlyMemory body, IList? messageTypes object Deserialize(ReadOnlyMemory body, Type type) { var actualType = GetMappedType(type); - using var stream = new ReadOnlyStream(body); - return JsonSerializer.Deserialize(stream, actualType, serializerOptions)!; + var typeInfo = serializerOptions.ResolveTypeInfo(actualType); + if (typeInfo is not null) + { + using var stream = new ReadOnlyStream(body); + return JsonSerializer.Deserialize(stream, typeInfo)!; + } + else + { + using var stream = new ReadOnlyStream(body); + return DeserializeWithReflection(stream, actualType, serializerOptions)!; + } } + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static void SerializeWithReflection(object message, Stream stream, Type messageType, JsonSerializerOptions? options) + => JsonSerializer.Serialize(stream, message, messageType, options); + + [UnconditionalSuppressMessage( + "Trimming", + "IL2026", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + [UnconditionalSuppressMessage( + "AOT", + "IL3050", + Justification = "Only called when System.Text.Json reflection serialization is enabled.")] + static object DeserializeWithReflection(Stream stream, Type type, JsonSerializerOptions? options) + => JsonSerializer.Deserialize(stream, type, options)!; + static IEnumerable FindRootTypes(IEnumerable messageTypesToDeserialize) { Type? currentRoot = null; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs index e4e73fb88e8..f83208317cf 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonConfigurationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs index a2fcae51aa8..651384b9805 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializer.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs index dd0b2720acd..7fa26fbba7f 100644 --- a/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs +++ b/src/NServiceBus.Core/Serializers/SystemJson/SystemJsonSerializerSettings.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus.Serializers.SystemJson; using System.Text.Json; diff --git a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs b/src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs similarity index 84% rename from src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs rename to src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs index 2d088464b7e..95c2f206237 100644 --- a/src/NServiceBus.Core/Utils/Reflection/DelegateFactory.cs +++ b/src/NServiceBus.Core/Serializers/XML/DelegateFactory.cs @@ -1,17 +1,23 @@ +#nullable enable + namespace NServiceBus; using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] static class DelegateFactory { public static Func CreateGet(PropertyInfo property) { if (!PropertyInfoToLateBoundProperty.TryGetValue(property, out var lateBoundPropertyGet)) { + ArgumentNullException.ThrowIfNull(property.DeclaringType, nameof(property)); + var instanceParameter = Expression.Parameter(typeof(object), "target"); var member = Expression.Property(Expression.Convert(instanceParameter, property.DeclaringType), property); @@ -32,6 +38,8 @@ public static Func CreateGet(FieldInfo field) { if (!FieldInfoToLateBoundField.TryGetValue(field, out var lateBoundFieldGet)) { + ArgumentNullException.ThrowIfNull(field.DeclaringType, nameof(field)); + var instanceParameter = Expression.Parameter(typeof(object), "target"); var member = Expression.Field(Expression.Convert(instanceParameter, field.DeclaringType), field); @@ -52,12 +60,14 @@ public static Action CreateSet(FieldInfo field) { if (!FieldInfoToLateBoundFieldSet.TryGetValue(field, out var callback)) { + ArgumentNullException.ThrowIfNull(field.DeclaringType, nameof(field)); + var sourceType = field.DeclaringType; - var method = new DynamicMethod("Set" + field.Name, null, new[] - { + var method = new DynamicMethod("Set" + field.Name, null, + [ typeof(object), typeof(object) - }, true); + ], true); var gen = method.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); // Load input to stack @@ -87,15 +97,18 @@ public static Action CreateSet(PropertyInfo property) { if (!PropertyInfoToLateBoundPropertySet.TryGetValue(property, out var result)) { - var method = new DynamicMethod("Set" + property.Name, null, new[] - { + ArgumentNullException.ThrowIfNull(property.DeclaringType, nameof(property)); + + var method = new DynamicMethod("Set" + property.Name, null, + [ typeof(object), typeof(object) - }, true); + ], true); var gen = method.GetILGenerator(); var sourceType = property.DeclaringType; var setter = property.GetSetMethod(true); + ArgumentNullException.ThrowIfNull(setter, nameof(property)); gen.Emit(OpCodes.Ldarg_0); // Load input to stack @@ -129,8 +142,8 @@ public static Action CreateSet(PropertyInfo property) return result; } - static readonly ConcurrentDictionary> PropertyInfoToLateBoundProperty = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> FieldInfoToLateBoundField = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> PropertyInfoToLateBoundPropertySet = new ConcurrentDictionary>(); - static readonly ConcurrentDictionary> FieldInfoToLateBoundFieldSet = new ConcurrentDictionary>(); + static readonly ConcurrentDictionary> PropertyInfoToLateBoundProperty = new(); + static readonly ConcurrentDictionary> FieldInfoToLateBoundField = new(); + static readonly ConcurrentDictionary> PropertyInfoToLateBoundPropertySet = new(); + static readonly ConcurrentDictionary> FieldInfoToLateBoundFieldSet = new(); } diff --git a/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs b/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs index d903bfb0b13..e44533ef83a 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlDeserialization.cs @@ -13,6 +13,7 @@ using Logging; using MessageInterfaces; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlDeserialization(IMessageMapper mapper, XmlSerializerCache cache, bool skipWrappingRawXml, bool sanitizeInput) { diff --git a/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs b/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs index 479675970ab..f0da9758bf8 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlMessageSerializer.cs @@ -8,6 +8,7 @@ namespace NServiceBus; using MessageInterfaces; using Serialization; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlMessageSerializer : IMessageSerializer { diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs b/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs index aaa66d58b1f..82405d1022e 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSanitizingStream.cs @@ -7,13 +7,8 @@ namespace NServiceBus; // A StreamReader that excludes XML-illegal characters while reading. [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] -class XmlSanitizingStream : StreamReader +class XmlSanitizingStream(Stream streamToSanitize) : StreamReader(streamToSanitize, true) { - public XmlSanitizingStream(Stream streamToSanitize) - : base(streamToSanitize, true) - { - } - public static bool IsLegalXmlChar(string xmlVersion, int character) { switch (xmlVersion) diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs index ff5d1fd7952..91f914e0b75 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerialization.cs @@ -11,23 +11,11 @@ using System.Xml; using System.Xml.Linq; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] -sealed class XmlSerialization : IDisposable +sealed class XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = XmlSerialization.DefaultNamespace) + : IDisposable { - public XmlSerialization(Type messageType, Stream stream, object message, Conventions conventions, XmlSerializerCache cache, bool skipWrappingRawXml, string @namespace = DefaultNamespace) - { - this.messageType = messageType; - this.message = message; - this.conventions = conventions; - this.cache = cache; - this.skipWrappingRawXml = skipWrappingRawXml; - this.@namespace = @namespace; - writer = new RawXmlTextWriter(stream, new XmlWriterSettings - { - CloseOutput = false - }); - } - public void Serialize() { var doc = new XDocument(new XDeclaration("1.0", null, null)); @@ -329,14 +317,10 @@ public void Dispose() bool disposed; - readonly XmlSerializerCache cache; - readonly Conventions conventions; - readonly object message; - - readonly Type messageType; - readonly string @namespace; - readonly bool skipWrappingRawXml; - readonly RawXmlTextWriter writer; + readonly RawXmlTextWriter writer = new(stream, new XmlWriterSettings + { + CloseOutput = false + }); const string BaseType = "baseType"; const string DefaultNamespace = "http://tempuri.net"; diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs index 218b15c5edf..57489a494be 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerializer.cs @@ -11,10 +11,10 @@ /// /// Defines the capabilities of the XML serializer. /// +[RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public class XmlSerializer : SerializationDefinition { - internal const string TrimmingMessage = "XmlSerializer is not supported in trimming scenarios."; /// /// Provides a factory method for building a message serializer. /// @@ -52,4 +52,7 @@ public override Func Configure(IReadOnlySett internal const string CustomNamespaceConfigurationKey = "XmlSerializer.CustomNamespace"; internal const string SkipWrappingRawXml = "XmlSerializer.SkipWrappingRawXml"; internal const string SanitizeInput = "XmlSerializer.SanitizeInput"; + + internal const string TrimmingMessage = "XmlSerializer is not supported in trimming scenarios and members from deserialized types may be trimmed if not referenced directly."; + internal const string DynamicCodeMessage = "XmlSerializer relies on dynamic code generation which is not available with Ahead of Time compilation"; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs b/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs index d59e913cda5..a652d08b25c 100644 --- a/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs +++ b/src/NServiceBus.Core/Serializers/XML/XmlSerializerCache.cs @@ -11,6 +11,7 @@ namespace NServiceBus; using System.Xml.Serialization; using Logging; +[RequiresDynamicCode(XmlSerializer.DynamicCodeMessage)] [RequiresUnreferencedCode(XmlSerializer.TrimmingMessage)] class XmlSerializerCache { diff --git a/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs b/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs index c8d217111d6..08ad0788b06 100644 --- a/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs +++ b/src/NServiceBus.Core/ServicePlatform/Retries/PlatformRetryNotifications.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; /// /// Provides notifications to ServiceControl about successfully retried messages. diff --git a/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs b/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs index c21b376e22a..b373af331b0 100644 --- a/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs +++ b/src/NServiceBus.Core/ServicePlatform/Retries/RetryAcknowledgementBehavior.cs @@ -1,8 +1,11 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Pipeline; using Routing; @@ -15,23 +18,13 @@ class RetryAcknowledgementBehavior : IForkConnector next) { - var useRetryAcknowledgement = IsRetriedMessage(context, out var id, out var acknowledgementQueue); + RoutingContext? routingContext = null; - if (useRetryAcknowledgement) + if (IsRetriedMessage(context, out var id, out var acknowledgementQueue)) { // notify the ServiceControl audit instance that the retry has already been acknowledged by the endpoint context.Extensions.Set(MarkAsAcknowledgedBehavior.State.Instance); - } - - await next(context).ConfigureAwait(false); - - if (useRetryAcknowledgement) - { - await ConfirmSuccessfulRetry().ConfigureAwait(false); - } - async Task ConfirmSuccessfulRetry() - { var messageToDispatch = new OutgoingMessage( CombGuid.Generate().ToString(), new Dictionary @@ -46,12 +39,18 @@ async Task ConfirmSuccessfulRetry() // context explicitly so it stays correlated to the processing of the retried message. ContextPropagation.PropagateContextToHeaders(Activity.Current, messageToDispatch.Headers); - var routingContext = new RoutingContext(messageToDispatch, new UnicastRoutingStrategy(acknowledgementQueue), context); + routingContext = new RoutingContext(messageToDispatch, new UnicastRoutingStrategy(acknowledgementQueue), context); + } + + await next(context).ConfigureAwait(false); + + if (routingContext is not null) + { await this.Fork(routingContext).ConfigureAwait(false); } } - static bool IsRetriedMessage(ITransportReceiveContext context, out string retryUniqueMessageId, out string retryAcknowledgementQueue) + static bool IsRetriedMessage(ITransportReceiveContext context, [NotNullWhen(true)] out string? retryUniqueMessageId, [NotNullWhen(true)] out string? retryAcknowledgementQueue) { // check if the message is coming from a manual retry attempt if (context.Message.Headers.TryGetValue(RetryUniqueMessageIdHeaderKey, out var uniqueMessageId) && diff --git a/src/NServiceBus.Core/StartableEndpoint.cs b/src/NServiceBus.Core/StartableEndpoint.cs index 0090b0c51de..4e7063ab97d 100644 --- a/src/NServiceBus.Core/StartableEndpoint.cs +++ b/src/NServiceBus.Core/StartableEndpoint.cs @@ -1,6 +1,7 @@ namespace NServiceBus; using System; +using System.Linq; using System.Runtime.InteropServices; using System.Security.Principal; using System.Threading; @@ -51,8 +52,8 @@ public async Task Setup(CancellationToken cancellationToken = default) void AddSendingQueueManifest() { - hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-SendingQueues", transportSeam.QueueBindings.SendingAddresses); - hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-ErrorQueue", settings.ErrorQueueAddress()); + hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-SendingQueues", transportSeam.QueueBindings.SendingAddresses.ToList(), StartupDiagnosticsJsonContext.Default.ListString); + hostingComponent.Config.AddStartupDiagnosticsSection("Manifest-ErrorQueue", settings.ErrorQueueAddress(), StartupDiagnosticsJsonContext.Default.String); } public async Task Start(CancellationToken cancellationToken = default) diff --git a/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs b/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs index e4bae3b6d0c..24efd759922 100644 --- a/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs +++ b/src/NServiceBus.Core/StaticHeaders/ApplyStaticHeadersBehavior.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs b/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs index 013afb13eb8..b706539b228 100644 --- a/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs +++ b/src/NServiceBus.Core/StaticHeaders/CurrentStaticHeaders.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System.Collections.Generic; diff --git a/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs b/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs index b85c3e258fd..535254c2a92 100644 --- a/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs +++ b/src/NServiceBus.Core/StaticHeaders/StaticHeaders.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using Features; diff --git a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs index ecfd80e3a90..bad8ad5921c 100644 --- a/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs +++ b/src/NServiceBus.Core/StaticHeaders/StaticHeadersConfigExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; @@ -20,6 +22,9 @@ public static void AddHeaderToAllOutgoingMessages(this EndpointConfiguration con { ArgumentException.ThrowIfNullOrWhiteSpace(key); + // TODO(nullable-major-release): Uncomment this in the next major version so that we actually enforce non-null values here + //ArgumentException.ThrowIfNullOrWhiteSpace(value); + if (!config.Settings.TryGet(out CurrentStaticHeaders headers)) { headers = []; @@ -30,4 +35,4 @@ public static void AddHeaderToAllOutgoingMessages(this EndpointConfiguration con headers[key] = value; } -} \ No newline at end of file +} diff --git a/src/NServiceBus.Core/Support/RuntimeEnvironment.cs b/src/NServiceBus.Core/Support/RuntimeEnvironment.cs index 138455659a9..6292790dce0 100644 --- a/src/NServiceBus.Core/Support/RuntimeEnvironment.cs +++ b/src/NServiceBus.Core/Support/RuntimeEnvironment.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Support; +#nullable enable + +namespace NServiceBus.Support; using System; diff --git a/src/NServiceBus.Core/Transports/DispatchProperties.cs b/src/NServiceBus.Core/Transports/DispatchProperties.cs index 92683892374..6a54d86b465 100644 --- a/src/NServiceBus.Core/Transports/DispatchProperties.cs +++ b/src/NServiceBus.Core/Transports/DispatchProperties.cs @@ -1,19 +1,30 @@ -namespace NServiceBus.Transport; +#nullable enable + +namespace NServiceBus.Transport; using System; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using DelayedDelivery; using Performance.TimeToBeReceived; /// /// Describes additional properties for an outgoing message. /// -public class DispatchProperties : Dictionary +[SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] +public class DispatchProperties : IDictionary, IReadOnlyDictionary { //These can't be changed to be backwards compatible with previous versions of the core - static readonly string DoNotDeliverBeforeKeyName = "DeliverAt"; - static readonly string DelayDeliveryWithKeyName = "DelayDeliveryFor"; - static readonly string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + const string DoNotDeliverBeforeKeyName = "DeliverAt"; + const string DelayDeliveryWithKeyName = "DelayDeliveryFor"; + const string DiscardIfNotReceivedBeforeKeyName = "TimeToBeReceived"; + + // Dedicated fields for the three well-known properties, avoiding string-key lookups + string? deliverAt; + string? delayDeliveryFor; + string? timeToBeReceived; /// /// Creates a new instance of . @@ -23,45 +34,626 @@ public DispatchProperties() } /// - /// Creates a new instance of an copies the values from the provided dictionary. + /// Creates a new instance of and copies the values from the provided dictionary. + /// + public DispatchProperties(Dictionary properties) + : this((IDictionary)properties) + { + } + + /// + /// Creates a new instance of and copies the values from the provided dictionary. /// - public DispatchProperties(Dictionary properties) : base(properties ?? []) + public DispatchProperties(IDictionary properties) { + if (properties is null) + { + return; + } + + if (properties is DispatchProperties source) + { + // Fast path: direct field copy when source is a DispatchProperties + deliverAt = source.deliverAt; + delayDeliveryFor = source.delayDeliveryFor; + timeToBeReceived = source.timeToBeReceived; + + count = source.count; + for (int i = 0; i < count; i++) + { + slots[i] = source.slots[i]; + } + + if (source.stash is { Count: > 0 }) + { + stash = new Dictionary(source.stash); + } + + return; + } + + foreach (var kvp in properties) + { + AddInternal(kvp.Key, kvp.Value); + } } /// /// Delay message delivery to a specific . /// - public DoNotDeliverBefore DoNotDeliverBefore + public DoNotDeliverBefore? DoNotDeliverBefore { - get => ContainsKey(DoNotDeliverBeforeKeyName) - ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(this[DoNotDeliverBeforeKeyName])) + get => deliverAt is not null + ? new DoNotDeliverBefore(DateTimeOffsetHelper.ToDateTimeOffset(deliverAt)) : null; - set => this[DoNotDeliverBeforeKeyName] = DateTimeOffsetHelper.ToWireFormattedString(value.At); + set => deliverAt = value is not null ? DateTimeOffsetHelper.ToWireFormattedString(value.At) : null; } /// /// Delay message delivery by a certain . /// - public DelayDeliveryWith DelayDeliveryWith + public DelayDeliveryWith? DelayDeliveryWith { - get => ContainsKey(DelayDeliveryWithKeyName) - ? new DelayDeliveryWith(TimeSpan.Parse(this[DelayDeliveryWithKeyName])) + get => delayDeliveryFor is not null + ? new DelayDeliveryWith(TimeSpan.Parse(delayDeliveryFor)) : null; - set => this[DelayDeliveryWithKeyName] = value.Delay.ToString(); + set => delayDeliveryFor = value?.Delay.ToString(); } /// /// Discard the message after a certain period of time. /// - public DiscardIfNotReceivedBefore DiscardIfNotReceivedBefore + public DiscardIfNotReceivedBefore? DiscardIfNotReceivedBefore { - get => ContainsKey(DiscardIfNotReceivedBeforeKeyName) - ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(this[DiscardIfNotReceivedBeforeKeyName])) + get => timeToBeReceived is not null + ? new DiscardIfNotReceivedBefore(TimeSpan.Parse(timeToBeReceived)) : null; - set => this[DiscardIfNotReceivedBeforeKeyName] = value.MaxTime.ToString(); + set => timeToBeReceived = value?.MaxTime.ToString(); + } + + /// + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + return fieldValue; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + set + { + ArgumentNullException.ThrowIfNull(key); + if (TrySetField(key, value)) + { + return; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + slot.Value = value; + return; + } + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + } + + /// + public ICollection Keys + { + get + { + var keys = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + keys[index++] = DoNotDeliverBeforeKeyName; + } + + if (delayDeliveryFor is not null) + { + keys[index++] = DelayDeliveryWithKeyName; + } + + if (timeToBeReceived is not null) + { + keys[index++] = DiscardIfNotReceivedBeforeKeyName; + } + + for (int i = 0; i < count; i++) + { + keys[index++] = slots[i].Key; + } + + if (stash is null) + { + return keys; + } + + foreach (var key in stash.Keys) + { + keys[index++] = key; + } + + return keys; + } + } + + /// + public ICollection Values + { + get + { + var values = new string[Count]; + int index = 0; + + if (deliverAt is not null) + { + values[index++] = deliverAt; + } + + if (delayDeliveryFor is not null) + { + values[index++] = delayDeliveryFor; + } + + if (timeToBeReceived is not null) + { + values[index++] = timeToBeReceived; + } + + for (int i = 0; i < count; i++) + { + values[index++] = slots[i].Value; + } + + if (stash is null) + { + return values; + } + + foreach (var value in stash.Values) + { + values[index++] = value; + } + + return values; + } + } + + /// + public int Count + { + get + { + int fieldCount = 0; + + if (deliverAt is not null) + { + fieldCount++; + } + + if (delayDeliveryFor is not null) + { + fieldCount++; + } + + if (timeToBeReceived is not null) + { + fieldCount++; + } + + return fieldCount + count + (stash?.Count ?? 0); + } + } + + /// + public bool IsReadOnly => false; + + /// + public void Add(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + throw new ArgumentException($"An item with the same key '{key}' has already been added."); + } + + AddInternal(key, value); + } + + /// + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } + + /// + public bool Remove(string key) + { + ArgumentNullException.ThrowIfNull(key); + if (RemoveFromFields(key)) + { + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + count--; + if (i != count) + { + slots[i] = slots[count]; + } + + slots[count] = new Slot(); + return true; + } + + return stash?.Remove(key) ?? false; + } + + /// + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + if (TryGetValueFromFields(key, out var fieldValue)) + { + value = fieldValue; + return true; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null!; + return false; + } + + /// + /// Attempts to add the specified key and value to the dictionary. + /// + /// true if the key/value pair was added; false if the key already exists. + public bool TryAdd(string key, string value) + { + ArgumentNullException.ThrowIfNull(key); + if (ContainsKeyInFields(key)) + { + return false; + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return false; + } + } + + if (stash is not null && stash.ContainsKey(key)) + { + return false; + } + + AddInternal(key, value); + return true; + } + + /// + public void Clear() + { + deliverAt = null; + delayDeliveryFor = null; + timeToBeReceived = null; + + for (int i = 0; i < count; i++) + { + slots[i] = new Slot(); + } + + count = 0; + stash?.Clear(); + } + + /// + public IEnumerator> GetEnumerator() + { + if (deliverAt is not null) + { + yield return new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + yield return new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + yield return new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + bool ContainsKeyInFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + return true; + } + + return timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName; + } + + bool TryGetValueFromFields(string key, out string value) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + value = deliverAt; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + value = delayDeliveryFor; + return true; + } + + if (timeToBeReceived is not null && key == DiscardIfNotReceivedBeforeKeyName) + { + value = timeToBeReceived; + return true; + } + + value = null!; + return false; + } + + bool TrySetField(string key, string value) + { + switch (key) + { + case DoNotDeliverBeforeKeyName: + deliverAt = value; + return true; + case DelayDeliveryWithKeyName: + delayDeliveryFor = value; + return true; + case DiscardIfNotReceivedBeforeKeyName: + timeToBeReceived = value; + return true; + default: + return false; + } + } + + bool RemoveFromFields(string key) + { + if (deliverAt is not null && key == DoNotDeliverBeforeKeyName) + { + deliverAt = null; + return true; + } + + if (delayDeliveryFor is not null && key == DelayDeliveryWithKeyName) + { + delayDeliveryFor = null; + return true; + } + + if (timeToBeReceived is null || key != DiscardIfNotReceivedBeforeKeyName) + { + return false; + } + + timeToBeReceived = null; + return true; + + } + + void AddInternal(string key, string value) + { + if (TrySetField(key, value)) + { + return; + } + + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + IEnumerable IReadOnlyDictionary.Keys => Keys; + IEnumerable IReadOnlyDictionary.Values => Values; + + void ICollection>.Add(KeyValuePair item) => Add(item.Key, item.Value); + + bool ICollection>.Contains(KeyValuePair item) => TryGetValue(item.Key, out var value) && StringComparer.Ordinal.Equals(value, item.Value); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ArgumentNullException.ThrowIfNull(array); + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException("Destination array is not long enough to copy all items."); + } + + int index = arrayIndex; + + if (deliverAt is not null) + { + array[index++] = new KeyValuePair(DoNotDeliverBeforeKeyName, deliverAt); + } + + if (delayDeliveryFor is not null) + { + array[index++] = new KeyValuePair(DelayDeliveryWithKeyName, delayDeliveryFor); + } + + if (timeToBeReceived is not null) + { + array[index++] = new KeyValuePair(DiscardIfNotReceivedBeforeKeyName, timeToBeReceived); + } + + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + array[index++] = new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + return; + } + + foreach (var kvp in stash) + { + array[index++] = kvp; + } + } + + bool ICollection>.Remove(KeyValuePair item) => ((ICollection>)this).Contains(item) && Remove(item.Key); + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + // Only needed for custom properties added by transports; the three well-known + // properties are stored as dedicated fields to avoid string-key lookups. + const int InlineArrayLength = 2; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs b/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs index efa24037ee1..e529ae8d7b1 100644 --- a/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs +++ b/src/NServiceBus.Core/Transports/Learning/LearningTransportDispatcher.cs @@ -2,12 +2,14 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using Transport; +using static NServiceBusMarkerInterfaceConvention; class LearningTransportDispatcher : IMessageDispatcher { @@ -35,7 +37,7 @@ async Task DispatchMulticast(IEnumerable transportO foreach (var transportOperation in transportOperations) { - var subscribers = await GetSubscribersFor(transportOperation.MessageType, cancellationToken) + var subscribers = await GetSubscribersFor(transportOperation, cancellationToken) .ConfigureAwait(false); foreach (var subscriber in subscribers) @@ -139,15 +141,13 @@ await AsyncFile.WriteTextAtomic(messagePath, headerPayload, creationTime, cancel } } - async Task> GetSubscribersFor(Type messageType, CancellationToken cancellationToken) + async Task> GetSubscribersFor(MulticastTransportOperation transportOperation, CancellationToken cancellationToken) { var subscribers = new HashSet(); - var allEventTypes = GetPotentialEventTypes(messageType); - - foreach (var eventType in allEventTypes) + foreach (var eventTypeName in GetPotentialEventTypeNames(transportOperation)) { - var eventDir = Path.Combine(basePath, ".events", eventType.FullName); + var eventDir = Path.Combine(basePath, ".events", eventTypeName); if (!Directory.Exists(eventDir)) { @@ -166,35 +166,57 @@ async Task> GetSubscribersFor(Type messageType, Cancellation return subscribers; } - static HashSet GetPotentialEventTypes(Type messageType) + static HashSet GetPotentialEventTypeNames(MulticastTransportOperation transportOperation) { - var allEventTypes = new HashSet(); + if (!transportOperation.Message.Headers.TryGetValue(Headers.EnclosedMessageTypes, out var messageHierarchy) || string.IsNullOrWhiteSpace(messageHierarchy)) + { + return InferEventTypeNames(transportOperation.MessageType); + } + + var eventTypeNames = new HashSet(StringComparer.Ordinal); + foreach (var typeName in messageHierarchy.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var messageTypeName = AssemblyQualifiedNameParser.GetMessageTypeNameWithoutAssembly(typeName).ToString(); + if (!IsMarkerType(messageTypeName)) + { + eventTypeNames.Add(messageTypeName); + } + } + + return eventTypeNames; + } + + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Used only for legacy or custom multicast operations without the enclosed message hierarchy header. Normal pipeline and outbox operations carry message metadata.")] + static HashSet InferEventTypeNames(Type messageType) + { + var allEventTypeNames = new HashSet(StringComparer.Ordinal); var currentType = messageType; while (currentType != null) { //do not include the marker interfaces - if (IsCoreMarkerInterface(currentType)) + if (IsMarkerType(currentType)) { break; } - allEventTypes.Add(currentType); + allEventTypeNames.Add(currentType.FullName!); currentType = currentType.BaseType; } - foreach (var type in messageType.GetInterfaces().Where(i => !IsCoreMarkerInterface(i))) + foreach (var type in messageType.GetInterfaces()) { - allEventTypes.Add(type); + if (!IsMarkerType(type)) + { + allEventTypeNames.Add(type.FullName!); + } } - return allEventTypes; + return allEventTypeNames; } - static bool IsCoreMarkerInterface(Type type) => type == typeof(IMessage) || type == typeof(IEvent) || type == typeof(ICommand); - readonly int maxMessageSizeKB; readonly string basePath; -} +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs b/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs index 0458d2073ca..a8523e896ad 100644 --- a/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs +++ b/src/NServiceBus.Core/Transports/MulticastTransportOperation.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Transport; using System; +using System.Diagnostics.CodeAnalysis; /// /// Represents a transport operation which should be delivered to multiple receivers. @@ -10,7 +11,7 @@ public class MulticastTransportOperation : IOutgoingTransportOperation /// /// Creates a new instance. /// - public MulticastTransportOperation(OutgoingMessage message, Type messageType, DispatchProperties properties, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default) + public MulticastTransportOperation(OutgoingMessage message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, DispatchProperties properties, DispatchConsistency requiredDispatchConsistency = DispatchConsistency.Default) { Message = message; MessageType = messageType; @@ -21,6 +22,7 @@ public MulticastTransportOperation(OutgoingMessage message, Type messageType, Di /// /// Defines the message type which needs to be multicasted. /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] public Type MessageType { get; } /// diff --git a/src/NServiceBus.Core/Transports/ReceiveProperties.cs b/src/NServiceBus.Core/Transports/ReceiveProperties.cs index f4eecbab4c0..4b7466e7ac9 100644 --- a/src/NServiceBus.Core/Transports/ReceiveProperties.cs +++ b/src/NServiceBus.Core/Transports/ReceiveProperties.cs @@ -2,9 +2,11 @@ namespace NServiceBus.Transport; +using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; /// /// Properties received from the transport that can be propagated to outgoing dispatch operations. @@ -13,8 +15,6 @@ namespace NServiceBus.Transport; [SuppressMessage("Naming", "CA1710:Identifiers should have correct suffix", Justification = "Name reflects domain semantics, not collection implementation.")] public sealed class ReceiveProperties : IReadOnlyDictionary { - readonly Dictionary properties; - /// /// An empty instance. /// @@ -23,34 +23,188 @@ public sealed class ReceiveProperties : IReadOnlyDictionary /// /// Creates an empty instance of . /// - public ReceiveProperties() => properties = []; + public ReceiveProperties() + { + } /// /// Creates a from the provided dictionary. - /// The dictionary is stored by reference — do not mutate it after passing to this constructor. /// - public ReceiveProperties(Dictionary dictionary) => properties = dictionary; + public ReceiveProperties(Dictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + foreach (var kvp in dictionary) + { + AddInternal(kvp.Key, kvp.Value); + } + } /// - public string this[string key] => properties[key]; + public string this[string key] + { + get + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return slot.Value; + } + } + + if (stash is not null && stash.TryGetValue(key, out var value)) + { + return value; + } + + ThrowKeyNotFoundException(key); + return null; + } + } /// - public IEnumerable Keys => properties.Keys; + public IEnumerable Keys + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Key; + } + + if (stash is not null) + { + foreach (var key in stash.Keys) + { + yield return key; + } + } + } + } /// - public IEnumerable Values => properties.Values; + public IEnumerable Values + { + get + { + for (int i = 0; i < count; i++) + { + yield return slots[i].Value; + } + + if (stash is not null) + { + foreach (var value in stash.Values) + { + yield return value; + } + } + } + } /// - public int Count => properties.Count; + public int Count => count + (stash?.Count ?? 0); /// - public bool ContainsKey(string key) => properties.ContainsKey(key); + public bool ContainsKey(string key) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (StringComparer.Ordinal.Equals(key, slot.Key)) + { + return true; + } + } + + return stash is not null && stash.ContainsKey(key); + } /// - public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) => properties.TryGetValue(key, out value); + public bool TryGetValue(string key, [MaybeNullWhen(false)] out string value) + { + ArgumentNullException.ThrowIfNull(key); + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + + if (!StringComparer.Ordinal.Equals(key, slot.Key)) + { + continue; + } + + value = slot.Value; + return true; + } + + if (stash is not null && stash.TryGetValue(key, out var stashedValue)) + { + value = stashedValue; + return true; + } + + value = null; + return false; + } /// - public IEnumerator> GetEnumerator() => properties.GetEnumerator(); + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < count; i++) + { + ref var slot = ref slots[i]; + yield return new KeyValuePair(slot.Key, slot.Value); + } + + if (stash is null) + { + yield break; + } + + foreach (var kvp in stash) + { + yield return kvp; + } + } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void AddInternal(string key, string value) + { + if (count < InlineArrayLength) + { + slots[count] = new Slot { Key = key, Value = value }; + count++; + return; + } + + (stash ??= [])[key] = value; + } + + [DoesNotReturn] + static void ThrowKeyNotFoundException(string key) => throw new KeyNotFoundException($"The given key '{key}' was not present in the dictionary."); + + SlotArray slots; + int count; + Dictionary? stash; + + const int InlineArrayLength = 4; + + struct Slot + { + public string Key; + public string Value; + } + + [InlineArray(InlineArrayLength)] + struct SlotArray + { + Slot _element0; + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs b/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs index 817f9043442..9a9915b6561 100644 --- a/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs +++ b/src/NServiceBus.Core/Transports/ReceiveSettingsExtensions.cs @@ -18,7 +18,7 @@ public static void MakeInstanceUniquelyAddressable(this EndpointConfiguration co ArgumentException.ThrowIfNullOrWhiteSpace(discriminator); config.Settings.Set("EndpointInstanceDiscriminator", discriminator); - config.Settings.AddStartupDiagnosticsSection("Manifest-UniqueAddressDiscriminator", discriminator); + config.Settings.AddStartupDiagnosticsSection("Manifest-UniqueAddressDiscriminator", discriminator, StartupDiagnosticsJsonContext.Default.String); } /// diff --git a/src/NServiceBus.Core/Transports/TransportSeam.cs b/src/NServiceBus.Core/Transports/TransportSeam.cs index 649e8520a16..370662b6cd3 100644 --- a/src/NServiceBus.Core/Transports/TransportSeam.cs +++ b/src/NServiceBus.Core/Transports/TransportSeam.cs @@ -1,6 +1,8 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using System.Transactions; @@ -19,7 +21,7 @@ public async Task CreateTransportInfrastructure(IServic { if (OperatingSystem.IsWindows() && TransportDefinition.TransportTransactionMode == TransportTransactionMode.TransactionScope) { - TransactionManager.ImplicitDistributedTransactions = true; + EnableImplicitDistributedTransactions(); } hostSettings.ServiceProvider = serviceProvider; @@ -29,6 +31,10 @@ public async Task CreateTransportInfrastructure(IServic return transportInfrastructure; } + [SupportedOSPlatform("windows")] + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The trimming limitation is conditional on TransactionScope mode and cannot be expressed at the shared transport configuration boundary; the setter is called only for that mode on Windows.")] + static void EnableImplicitDistributedTransactions() => TransactionManager.ImplicitDistributedTransactions = true; + public static TransportSeam Create(Settings transportSeamSettings, HostingComponent.Configuration hostingConfiguration) { var transportDefinition = transportSeamSettings.TransportDefinition; diff --git a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs index 5949ed20ca5..3f6d573c540 100644 --- a/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs +++ b/src/NServiceBus.Core/Unicast/Config/MessageHandlerRegistrationExtensions.cs @@ -1,4 +1,5 @@ #nullable enable + namespace NServiceBus; using System; @@ -13,6 +14,7 @@ public static class MessageHandlerRegistrationExtensions /// /// Registers a message handler. /// + [RequiresDynamicCode(MessageHandlerRegistry.DynamicCodeMessage)] [RequiresUnreferencedCode(MessageHandlerRegistry.TrimmingMessage)] public static void AddHandler<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Handler)] THandler>(this EndpointConfiguration config) { diff --git a/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs new file mode 100644 index 00000000000..6736ce59e0c --- /dev/null +++ b/src/NServiceBus.Core/Unicast/Config/MessageTypeRegistrationExtensions.cs @@ -0,0 +1,36 @@ +#nullable enable + +namespace NServiceBus; + +using System; +using System.Diagnostics.CodeAnalysis; +using Unicast.Messages; + +/// +/// Provides extensions to manually register message types. +/// +public static class MessageTypeRegistrationExtensions +{ + internal const string TrimmingMessage = "AddMessageType relies on an NServiceBus source-generated interceptor to register the statically known message hierarchy without reflection when trimming is enabled. If this warning is reported, the interceptor was not used for this call; enable or restore the NServiceBus analyzer/source-generator tooling and use a statically known message type."; + + /// + /// Registers the message type including its hierarchy of base types and implemented interfaces. + /// + /// + /// The type is checked against the endpoint's message conventions when the message metadata registry is initialized. + /// This method registers metadata for a type that the conventions already identify as a message; it does not itself + /// classify an arbitrary type as a message. Types that do not implement , , + /// or (unobtrusive mode) need matching conventions defined with + /// . + /// The hierarchy is inferred at runtime using reflection. Under trimming or NativeAOT the call is replaced by a + /// source-generated, reflection-free registration that registers the statically known hierarchy instead. + /// + [RequiresUnreferencedCode(TrimmingMessage)] + public static void AddMessageType<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] TMessage>(this EndpointConfiguration config) + { + ArgumentNullException.ThrowIfNull(config); + + var messageMetadataRegistry = config.Settings.GetOrCreate(); + messageMetadataRegistry.RegisterMessageTypes([typeof(TMessage)]); + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs b/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs index ece4f59a0cb..de3374ca489 100644 --- a/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs +++ b/src/NServiceBus.Core/Unicast/Config/SagaRegistrationExtensions.cs @@ -14,6 +14,7 @@ public static class SagaRegistrationExtensions /// /// Registers a saga. /// + [RequiresDynamicCode(SagaMetadata.DynamicCodeMessage)] [RequiresUnreferencedCode(SagaMetadata.TrimmingMessage)] public static void AddSaga<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Saga)] TSaga>(this EndpointConfiguration config) where TSaga : Saga, IHandleMessages { diff --git a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs index 4fb04136814..3e1d3200a04 100644 --- a/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs +++ b/src/NServiceBus.Core/Unicast/MessageHandlerRegistry.cs @@ -44,13 +44,11 @@ public List GetHandlersFor(Type messageType) /// Lists all message type for which we have handlers. /// /// This method should not be called on a hot path. - public IEnumerable GetMessageTypes() - { - return (from messagesBeingHandled in messageHandlerFactories.Values - from typeHandled in messagesBeingHandled - let messageType = typeHandled.MessageType - select messageType).Distinct(); - } + public IEnumerable GetMessageTypes() => + (from messagesBeingHandled in messageHandlerFactories.Values + from typeHandled in messagesBeingHandled + let messageType = typeHandled.MessageType + select messageType).Distinct(); /// /// Registers the given potential handler type. @@ -59,12 +57,14 @@ from typeHandled in messagesBeingHandled TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] [Obsolete("Use 'AddHandler()' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void RegisterHandler(Type handlerType) => AddHandlerWithReflection(handlerType); /// /// Registers the handler type. /// + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void AddHandler() { @@ -159,6 +159,7 @@ List GetOrCreate() /// Add handlers from types scanned at runtime. /// /// Scanned types, with "load handlers first" types ordered first. + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public void AddScannedHandlers(IEnumerable orderedTypes) { @@ -190,6 +191,7 @@ public void Clear() deduplicationSet.Clear(); } + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] void AddHandlerWithReflection(Type handlerType) => AddHandlerWithReflectionMethod.InvokeGeneric(this, [handlerType]); @@ -200,13 +202,11 @@ void AddHandlerWithReflection(Type handlerType) => .GetMethod(nameof(AddHandler), BindingFlags.Public | BindingFlags.Instance, []) ?? throw new MissingMethodException(nameof(AddHandler)); static readonly MethodInfo AddMessageHandlerForMessageMethod = typeof(MessageHandlerRegistry) - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .SingleOrDefault(m => m.Name == nameof(AddMessageHandlerForMessage) && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2) + .GetMethod(nameof(AddMessageHandlerForMessage), 2, BindingFlags.Public | BindingFlags.Instance, null, [], null) ?? throw new MissingMethodException(nameof(AddMessageHandlerForMessage)); static readonly MethodInfo AddTimeoutHandlerForMessageMethod = typeof(MessageHandlerRegistry) - .GetMethods(BindingFlags.Public | BindingFlags.Instance) - .SingleOrDefault(m => m.Name == nameof(AddTimeoutHandlerForMessage) && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2) + .GetMethod(nameof(AddTimeoutHandlerForMessage), 2, BindingFlags.Public | BindingFlags.Instance, null, [], null) ?? throw new MissingMethodException(nameof(AddTimeoutHandlerForMessage)); readonly Dictionary> messageHandlerFactories = []; @@ -214,6 +214,7 @@ void AddHandlerWithReflection(Type handlerType) => static readonly Type IHandleMessagesType = typeof(IHandleMessages<>); internal const string TrimmingMessage = "Registering handlers using assembly scanning is not supported in trimming scenarios."; + internal const string DynamicCodeMessage = "Registering handlers using assembly scanning relies on dynamic code generation which is not available with Ahead of Time compilation."; readonly record struct HandlerAndMessage(Type HandlerType, Type MessageType, bool IsTimeoutHandler) { @@ -250,6 +251,7 @@ public MessageHandler Create() => static readonly ObjectFactory factory = ActivatorUtilities.CreateFactory([]); static readonly ObjectFactory> handlerFactory = + // SAFETY: Registration validated THandler implements IHandleTimeouts; retype skips the interface cast. static (sp, args) => Unsafe.As>(factory(sp, args)); } @@ -276,6 +278,7 @@ public MessageHandler Create() => static readonly ObjectFactory factory = ActivatorUtilities.CreateFactory([]); static readonly ObjectFactory> handlerFactory = + // SAFETY: Registration validated THandler implements IHandleMessages; retype skips the interface cast. static (sp, args) => Unsafe.As>(factory(sp, args)); } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Unicast/MessageOperations.cs b/src/NServiceBus.Core/Unicast/MessageOperations.cs index dfe850c0c3c..c3a8e40d784 100644 --- a/src/NServiceBus.Core/Unicast/MessageOperations.cs +++ b/src/NServiceBus.Core/Unicast/MessageOperations.cs @@ -3,6 +3,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using Extensibility; @@ -27,19 +28,31 @@ class MessageOperations( readonly bool UseMessageTypeNamesInSpanNames = activityFactory.Options.UseMessageTypeNamesInSpanNames; - public Task Publish(IBehaviorContext context, Action messageConstructor, PublishOptions options) + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, PublishOptions options) { - return Publish(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); + return PublishMessage(context, typeof(T), message!, options); } + public Task Publish(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return PublishMessage(context, messageType, message, options); + } + + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, PublishOptions options) + { + return PublishMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); + } + + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Publish(IBehaviorContext context, object message, PublishOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); - return Publish(context, messageType, message, options); + return PublishMessage(context, messageType, message, options); } - async Task Publish(IBehaviorContext context, Type messageType, object message, PublishOptions options) + async Task PublishMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, PublishOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -71,7 +84,7 @@ await publishPipeline.Invoke(publishContext) } catch (Exception ex) { - activityFactory.RecordError(activity, ex, context.Extensions); + activityFactory.RecordError(activity, ex, context.Builder); throw; } } @@ -104,7 +117,7 @@ await subscribePipeline.Invoke(subscribeContext) } catch (Exception ex) { - activityFactory.RecordError(activity, ex, context.Extensions); + activityFactory.RecordError(activity, ex, context.Builder); throw; } } @@ -134,16 +147,28 @@ await unsubscribePipeline.Invoke(unsubscribeContext) } catch (Exception ex) { - activityFactory.RecordError(activity, ex, context.Extensions); + activityFactory.RecordError(activity, ex, context.Builder); throw; } } - public Task Send(IBehaviorContext context, Action messageConstructor, SendOptions options) + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, SendOptions options) + { + return SendMessage(context, typeof(T), message!, options); + } + + public Task Send(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return SendMessage(context, messageType, message, options); + } + + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, SendOptions options) { return SendMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Send(IBehaviorContext context, object message, SendOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); @@ -151,7 +176,7 @@ public Task Send(IBehaviorContext context, object message, SendOptions options) return SendMessage(context, messageType, message, options); } - async Task SendMessage(IBehaviorContext context, Type messageType, object message, SendOptions options) + async Task SendMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, SendOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -182,11 +207,23 @@ await sendPipeline.Invoke(outgoingContext) } catch (Exception ex) { - activityFactory.RecordError(activity, ex, context.Extensions); + activityFactory.RecordError(activity, ex, context.Builder); throw; } } + public Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(IBehaviorContext context, T message, ReplyOptions options) + { + return ReplyMessage(context, typeof(T), message!, options); + } + + public Task Reply(IBehaviorContext context, object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return ReplyMessage(context, messageType, message, options); + } + + [RequiresUnreferencedCode(RuntimeTypeRoutingTrimmingMessage)] public Task Reply(IBehaviorContext context, object message, ReplyOptions options) { var messageType = messageMapper.GetMappedTypeFor(message.GetType()); @@ -194,12 +231,12 @@ public Task Reply(IBehaviorContext context, object message, ReplyOptions options return ReplyMessage(context, messageType, message, options); } - public Task Reply(IBehaviorContext context, Action messageConstructor, ReplyOptions options) + public Task Reply<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(IBehaviorContext context, Action messageConstructor, ReplyOptions options) { return ReplyMessage(context, typeof(T), messageMapper.CreateInstance(messageConstructor), options); } - async Task ReplyMessage(IBehaviorContext context, Type messageType, object message, ReplyOptions options) + async Task ReplyMessage(IBehaviorContext context, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, object message, ReplyOptions options) { var messageId = options.UserDefinedMessageId ?? CombGuid.Generate().ToString(); var headers = new Dictionary(options.OutgoingHeaders) @@ -230,11 +267,14 @@ await replyPipeline.Invoke(outgoingContext) } catch (Exception ex) { - activityFactory.RecordError(activity, ex, context.Extensions); + activityFactory.RecordError(activity, ex, context.Builder); throw; } } + internal const string RuntimeTypeRoutingTrimmingMessage = "When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type."; + internal const string DefaultInterfaceTrimmingSuppressionJustification = "The default interface implementation preserves compatibility with third-party implementations. Built-in implementations override this method and preserve the declared message type."; + static void MergeDispatchProperties(ContextBag context, DispatchProperties dispatchProperties) { // we can't add the constraints directly to the SendOptions ContextBag as the options can be reused diff --git a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs new file mode 100644 index 00000000000..26726d51916 --- /dev/null +++ b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.Legacy.cs @@ -0,0 +1,66 @@ +#nullable enable + +namespace NServiceBus.Unicast.Messages; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +public partial class MessageMetadataRegistry +{ + [UnconditionalSuppressMessage("Trimming", "IL2057", Justification = "Dynamic type loading is best-effort; when trimming removes a type, resolution falls back to known registered message metadata.")] + Type? GetType(string messageTypeIdentifier) + { + if (allowDynamicTypeLoading) + { + try + { + return Type.GetType(messageTypeIdentifier); + } + catch (Exception ex) + { + Logger.Warn($"Message type identifier '{messageTypeIdentifier}' could not be loaded", ex); + } + } + else + { + Logger.Warn($"Unknown message type identifier '{messageTypeIdentifier}'. Dynamic type loading is disabled. Make sure the type is loaded before starting the endpoint or enable dynamic type loading."); + } + + return null; + } + + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Runtime hierarchy inference is used for message types not pre-registered with source-generated hierarchy metadata, including scanned, dynamically loaded, published-only, and legacy message types.")] + Type[] GetRuntimeMessageHierarchy(Type messageType) + { + var parentTypes = new List(messageType.GetInterfaces()); + + var currentBaseType = messageType.BaseType; + var objectType = typeof(object); + while (currentBaseType != null && currentBaseType != objectType) + { + parentTypes.Add(currentBaseType); + currentBaseType = currentBaseType.BaseType; + } + + return [.. parentTypes + .Where(isMessageType) + .OrderByDescending(static type => + { + if (type.IsInterface) + { + return type.GetInterfaces().Length; + } + + var result = 0; + while (type.BaseType != null) + { + result++; + type = type.BaseType; + } + + return result; + })]; + } +} diff --git a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs index 00d00491b26..54db9e87268 100644 --- a/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs +++ b/src/NServiceBus.Core/Unicast/Messages/MessageMetadataRegistry.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Logging; using NServiceBus; @@ -95,7 +96,10 @@ public MessageMetadata GetMessageMetadata(string messageTypeIdentifier) if (!cacheHit) { - messageType = GetType(messageTypeIdentifier); + if (!StrictRegisteredOnlyMode) + { + messageType = GetType(messageTypeIdentifier); + } if (messageType == null) { @@ -136,6 +140,12 @@ public MessageMetadata GetMessageMetadata(string messageTypeIdentifier) if (isMessageType(messageType)) { + if (StrictRegisteredOnlyMode) + { + Logger.WarnFormat("Message header '{0}' was mapped to type '{1}' but that type was not found in the message registry. Register the message type explicitly using 'AddMessageType()' when running with assembly scanning disabled in a trimmed application. ", messageTypeIdentifier, messageType.FullName); + return null; + } + return RegisterMessageTypeCore(messageType); } @@ -209,25 +219,44 @@ void RegisterMessageType(Type messageType) } } - Type GetType(string messageTypeIdentifier) + /// + /// Attempts to retrieve the for the specified type from the already registered metadata cache without + /// performing any runtime resolution or registration. + /// + /// The message type to retrieve metadata for. + /// The when the type is registered; otherwise null. + /// true when the type is already registered, otherwise false. + internal bool TryGetMessageMetadata(Type messageType, out MessageMetadata metadata) { - if (allowDynamicTypeLoading) - { - try - { - return Type.GetType(messageTypeIdentifier); - } - catch (Exception ex) - { - Logger.Warn($"Message type identifier '{messageTypeIdentifier}' could not be loaded", ex); - } - } - else + ArgumentNullException.ThrowIfNull(messageType); + AssertIsInitialized(); + + return messages.TryGetValue(messageType.TypeHandle, out metadata); + } + + /// + /// Attempts to retrieve the for the message identifier from the already registered metadata cache without + /// performing any runtime type resolution, registration, or logging. + /// + /// The message identifier to retrieve metadata for. + /// The when the identifier is registered; otherwise null. + /// true when the identifier is already registered, otherwise false. + internal bool TryGetMessageMetadata(string messageTypeIdentifier, out MessageMetadata metadata) + { + ArgumentException.ThrowIfNullOrWhiteSpace(messageTypeIdentifier); + AssertIsInitialized(); + + // cachedTypes can hold a null entry for identifiers that failed to resolve (see GetMessageMetadata(string)); + // treat those negative entries as a cache miss instead of dereferencing them. + if (cachedTypes.TryGetValue(messageTypeIdentifier, out var messageType) && + messageType is not null && + messages.TryGetValue(messageType.TypeHandle, out metadata)) { - Logger.Warn($"Unknown message type identifier '{messageTypeIdentifier}'. Dynamic type loading is disabled. Make sure the type is loaded before starting the endpoint or enable dynamic type loading."); + return true; } - return null; + metadata = null!; + return false; } void RegisterMessageTypeWithHierarchyCore(Type messageType, IEnumerable parentMessages) @@ -252,12 +281,16 @@ MessageMetadata RegisterMessageTypeCore(Type messageType) return metadata; } + // Strict mode forbids runtime hierarchy inference and registration on cache misses. The guard lives here so + // pre-initialization bare registrations are also rejected during Initialize. + if (StrictRegisteredOnlyMode) + { + ThrowStrictMissingMetadataException(messageType); + } + LogGenericMessageTypeWarning(messageType); - //get the parent types - var parentMessages = GetParentTypes(messageType) - .Where(isMessageType) - .OrderByDescending(PlaceInMessageHierarchy); + var parentMessages = GetRuntimeMessageHierarchy(messageType); metadata = new MessageMetadata(messageType, [messageType, .. parentMessages]); @@ -267,6 +300,10 @@ MessageMetadata RegisterMessageTypeCore(Type messageType) return metadata; } + [DoesNotReturn] + static void ThrowStrictMissingMetadataException(Type messageType) => + throw new Exception($"Could not find metadata for '{messageType.FullName}' because the endpoint runs in strict registered-only message metadata mode.{Environment.NewLine}Ensure one of the following registration paths is used:{Environment.NewLine}1. Register the message type before the endpoint starts using 'AddMessageType()' or 'RegisterMessageTypeWithHierarchy'.{Environment.NewLine}2. If '{messageType.FullName}' is handled by a handler or saga, register the handler or saga with 'AddHandler()' or 'AddSaga()' and the message type with 'AddMessageType()'.{Environment.NewLine}In either case, ensure '{messageType.FullName}' implements either 'IMessage', 'IEvent' or 'ICommand' or alternatively, if you don't want to implement an interface, you can use 'Unobtrusive Mode'."); + static void LogGenericMessageTypeWarning(Type messageType) { if (messageType.IsGenericType) @@ -284,43 +321,15 @@ void AssertIsInitialized() } } - static int PlaceInMessageHierarchy(Type type) - { - if (type.IsInterface) - { - return type.GetInterfaces().Length; - } - - var result = 0; - - while (type.BaseType != null) - { - result++; - - type = type.BaseType; - } - - return result; - } - - static IEnumerable GetParentTypes(Type type) - { - foreach (var i in type.GetInterfaces()) - { - yield return i; - } - - // return all inherited types - var currentBaseType = type.BaseType; - var objectType = typeof(object); - while (currentBaseType != null && currentBaseType != objectType) - { - yield return currentBaseType; - currentBaseType = currentBaseType.BaseType; - } - } - bool initialized; + + /// + /// When enabled, message metadata is only resolved for types registered up front (e.g. via the source-generated + /// AddMessageType<T> registration) and no runtime registration, hierarchy inference, or dynamic type + /// loading is performed. Must be set before so pre-initialization registrations are + /// enforced against the strict policy. + /// + internal bool StrictRegisteredOnlyMode { get; set; } readonly List<(Type MessageType, IEnumerable Hierarchy)> preRegisteredMessagesWithHierarchy = []; readonly List preRegisteredMessageTypes = []; readonly ConcurrentDictionary messages = new(); diff --git a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs index cdf8fc65777..90893513f95 100644 --- a/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs +++ b/src/NServiceBus.Core/Unicast/RunningEndpointInstance.cs @@ -3,10 +3,13 @@ namespace NServiceBus; using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Features; using Logging; +using Particular.Obsoletes; using Settings; using Transport; @@ -99,13 +102,34 @@ public async ValueTask DisposeAsync() return; } - await StopCore().ConfigureAwait(false); + // In case Stop was not called, we need to trigger shutdown before cleaning up resources. + // Since we're already disposing, we want to bypass any waits and just trigger shutdown with a canceled token. + // We are effectively indicating the graceful shutdown period has already elapsed and any ongoing operations should + // be aborted immediately if they participate in the cooperative cancellation. + var cancellationToken = new CancellationToken(true); - settings.Clear(); - stoppingTokenSource.Dispose(); - await serviceProviderLease.DisposeAsync().ConfigureAwait(false); + try + { + await StopCore(cancellationToken).ConfigureAwait(false); + } +#pragma warning disable PS0019 + catch (Exception) +#pragma warning restore PS0019 + { + // ignored because we're already disposing and we don't want to throw from DisposeAsync. Any exceptions from StopCore are already logged, so we can safely ignore them here. + } + finally + { + settings.Clear(); + stoppingTokenSource.Dispose(); + await serviceProviderLease.DisposeAsync().ConfigureAwait(false); + } } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Send(T, SendOptions, CancellationToken) or Send(object, Type, SendOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Send(object message, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -115,7 +139,27 @@ public Task Send(object message, SendOptions sendOptions, CancellationToken canc return messageSession.Send(message, sendOptions, cancellationToken); } - public Task Send(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) + [OverloadResolutionPriority(-1)] + public Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(sendOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Send(message, sendOptions, cancellationToken); + } + + public Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions sendOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(sendOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Send(message, messageType, sendOptions, cancellationToken); + } + + public Task Send<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, SendOptions sendOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(sendOptions); @@ -124,6 +168,10 @@ public Task Send(Action messageConstructor, SendOptions sendOptions, Cance return messageSession.Send(messageConstructor, sendOptions, cancellationToken); } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "Publish(T, PublishOptions, CancellationToken) or Publish(object, Type, PublishOptions, CancellationToken)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic or explicit Type overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public Task Publish(object message, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(message); @@ -133,7 +181,27 @@ public Task Publish(object message, PublishOptions publishOptions, CancellationT return messageSession.Publish(message, publishOptions, cancellationToken); } - public Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) + [OverloadResolutionPriority(-1)] + public Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(publishOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Publish(message, publishOptions, cancellationToken); + } + + public Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions publishOptions, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + ArgumentNullException.ThrowIfNull(publishOptions); + + GuardAgainstUseWhenNotStarted(); + return messageSession.Publish(message, messageType, publishOptions, cancellationToken); + } + + public Task Publish<[DynamicallyAccessedMembers(IMessageCreator.CreatorMembersRequired)] T>(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(messageConstructor); ArgumentNullException.ThrowIfNull(publishOptions); diff --git a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs index d9f18fee0a7..8f95125eba3 100644 --- a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs +++ b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWork.cs @@ -1,4 +1,6 @@ -namespace NServiceBus.Features; +#nullable enable + +namespace NServiceBus.Features; using System; using System.Transactions; diff --git a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs index da412437846..8271b13dcc4 100644 --- a/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs +++ b/src/NServiceBus.Core/UnitOfWork/TransactionScopes/TransactionScopeUnitOfWorkBehavior.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Threading.Tasks; diff --git a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs index fd66c6958e2..56320293b8d 100644 --- a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs +++ b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettings.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Transactions; diff --git a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs index a775f38d594..498a7ce5d7f 100644 --- a/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs +++ b/src/NServiceBus.Core/UnitOfWork/UnitOfWorkSettingsExtensions.cs @@ -1,3 +1,5 @@ +#nullable enable + namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs b/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs index 3f83dfc16e2..8cc86e157c7 100644 --- a/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs +++ b/src/NServiceBus.Core/Utils/ArrayPoolBufferWriter.cs @@ -5,7 +5,6 @@ namespace NServiceBus; using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; sealed class ArrayPoolBufferWriter( ArrayPool pool, @@ -126,7 +125,20 @@ public Span GetSpan(int sizeHint = 0) } - Memory IMemoryOwner.Memory => MemoryMarshal.AsMemory(WrittenMemory); + Memory IMemoryOwner.Memory + { + get + { + T[]? array = buffer; + + if (array is null) + { + ThrowObjectDisposedException(); + } + + return array.AsMemory(0, WrittenCount); + } + } public void Dispose() { diff --git a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs index 581bba0cf34..cd71d3e8843 100644 --- a/src/NServiceBus.Core/Utils/FileVersionRetriever.cs +++ b/src/NServiceBus.Core/Utils/FileVersionRetriever.cs @@ -1,13 +1,17 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; static class FileVersionRetriever { public static string GetFileVersion(Type type) => GetFileVersion(type.Assembly); + [UnconditionalSuppressMessage("SingleFile", "IL3000", Justification = "Location is checked for empty string before use; falls back to AssemblyFileVersionAttribute or assembly name version when running as a single-file app.")] public static string GetFileVersion(Assembly assembly) { if (!string.IsNullOrEmpty(assembly.Location)) @@ -19,11 +23,11 @@ public static string GetFileVersion(Assembly assembly) var fileVersionAttribute = assembly.GetCustomAttribute(); - if (Version.TryParse(fileVersionAttribute.Version, out var version)) + if (Version.TryParse(fileVersionAttribute?.Version, out var version)) { return version.ToString(3); } - return assembly.GetName().Version.ToString(3); + return assembly.GetName().Version?.ToString(3) ?? "0.0.0"; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs index 8cd0715741f..9d021c8449c 100644 --- a/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs +++ b/src/NServiceBus.Core/Utils/LazyArrayPoolBufferWriter.cs @@ -1,10 +1,10 @@ #nullable enable + namespace NServiceBus; using System; using System.Buffers; using System.Diagnostics.CodeAnalysis; -using System.Runtime.InteropServices; sealed class LazyArrayPoolBufferWriter(int? capacity = null) : IBufferWriter, IMemoryOwner { @@ -55,7 +55,7 @@ public Span GetSpan(int sizeHint = 0) public void Clear() => innerWriter?.Clear(); public void Dispose() => innerWriter?.Dispose(); - Memory IMemoryOwner.Memory => MemoryMarshal.AsMemory(WrittenMemory); + Memory IMemoryOwner.Memory => innerWriter is null ? Memory.Empty : ((IMemoryOwner)innerWriter).Memory; [MemberNotNull(nameof(innerWriter))] void EnsureInnerWriter() => innerWriter ??= capacity is null ? new ArrayPoolBufferWriter() : new ArrayPoolBufferWriter(capacity.Value); diff --git a/src/NServiceBus.Core/Utils/PathUtilities.cs b/src/NServiceBus.Core/Utils/PathUtilities.cs index 43fd3cb3dac..4aa37249096 100644 --- a/src/NServiceBus.Core/Utils/PathUtilities.cs +++ b/src/NServiceBus.Core/Utils/PathUtilities.cs @@ -1,4 +1,6 @@ -namespace NServiceBus; +#nullable enable + +namespace NServiceBus; using System; diff --git a/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs b/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs index 7c7b5ef6c40..94d87c4905b 100644 --- a/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs +++ b/src/NServiceBus.Core/Utils/Reflection/MethodInfoExtensions.cs @@ -12,18 +12,23 @@ static class MethodInfoExtensions { extension(MethodInfo method) { + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(object? target, object?[]? args, Type[] genericTypes) => (T?)method.InvokeGeneric(target, args, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(object?[]? args, Type[] genericTypes) => (T?)method.InvokeGeneric(null, args, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public T? InvokeGeneric(Type genericType) => (T?)method.InvokeGeneric(null, null, [genericType]); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public object? InvokeGeneric(object? target, Type[] genericTypes) => method.InvokeGeneric(target, null, genericTypes); + [RequiresDynamicCode(DynamicCodeMessage)] [RequiresUnreferencedCode(TrimmingMessage)] public object? InvokeGeneric(object? target, object?[]? args, Type[] genericTypes) { @@ -45,4 +50,5 @@ static class MethodInfoExtensions } const string TrimmingMessage = "Generic invocations might require access to unreferenced code"; + const string DynamicCodeMessage = "Generic invocation relies on dynamic code generation which is not available with Ahead of Time compilation"; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs index 89920d270e6..5c26300742c 100644 --- a/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs +++ b/src/NServiceBus.Core/Utils/Reflection/TypeExtensionMethods.cs @@ -1,9 +1,10 @@ +#nullable enable + namespace NServiceBus; using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; static class TypeExtensionMethods { @@ -46,16 +47,13 @@ public bool IsSystemType() } public bool IsFromParticularAssembly() => type.Assembly.IsParticularAssembly(); - } - extension([NotNull] Type t) - { /// /// Takes the name of the given type and makes it friendly for serialization /// by removing problematic characters. /// public string SerializationFriendlyName() => - TypeToNameLookup.GetOrAdd(t.TypeHandle, static (typeHandle, t) => + TypeToNameLookup.GetOrAdd(type.TypeHandle, static (typeHandle, t) => { var index = t.Name.IndexOf('`'); if (index >= 0) @@ -71,25 +69,21 @@ public string SerializationFriendlyName() => } } - if (args.Length == 2) + if (args.Length == 2 && t.IsGenericType && t.GetGenericTypeDefinition() == typeof(KeyValuePair<,>)) { - if (typeof(KeyValuePair<,>).MakeGenericType(args[0], args[1]) == t) - { - result = "NServiceBus." + result; - } + result = "NServiceBus." + result; } return result; } - return Type.GetTypeFromHandle(typeHandle)?.Name; - }, t); + return t.Name; + }, type); } static bool IsClrType(ReadOnlySpan publicKeyToken) => publicKeyToken.SequenceEqual(MsPublicKeyToken); - static readonly byte[] MsPublicKeyToken = typeof(string).Assembly.GetName().GetPublicKeyToken(); - + static readonly byte[] MsPublicKeyToken = typeof(string).Assembly.GetName().GetPublicKeyToken() ?? []; static readonly ConcurrentDictionary IsSystemTypeCache = new(); static readonly ConcurrentDictionary TypeToNameLookup = new(); } \ No newline at end of file diff --git a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj index 585c481b424..7879fbc1f04 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj +++ b/src/NServiceBus.Learning.AcceptanceTests/NServiceBus.Learning.AcceptanceTests.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs b/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs index 998485d6070..f88183c4502 100644 --- a/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs +++ b/src/NServiceBus.Learning.AcceptanceTests/When_message_sent_with_LearningTransport.cs @@ -22,8 +22,8 @@ public async Task Should_preserve_file_created_time_as_receive_property() using (Assert.EnterMultipleScope()) { Assert.That(context.MessageReceived, Is.True, "Message was not received"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); - Assert.That(DateTime.TryParse(context.FileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); } } @@ -35,7 +35,29 @@ public async Task Should_preserve_file_created_time_property_on_dispatched_copie .WithEndpoint() .Run(); - Assert.That(context.MessageAudited, Is.True, "Message was not audited"); + using (Assert.EnterMultipleScope()) + { + Assert.That(context.MessageAudited, Is.True, "Message was not audited"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + } + } + + [Test] + public async Task Should_preserve_file_created_time_property_on_delayed_retry() + { + var context = await Scenario.Define() + .WithEndpoint(b => b + .When(session => session.SendLocal(new TestMessage())) + .DoNotFailOnErrorMessages()) + .Run(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.NumberOfRetries, Is.EqualTo(2), "Message was not retried"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + } } [Test] @@ -45,7 +67,12 @@ public async Task Should_not_preserve_receive_properties_on_outgoing_messages() .WithEndpoint(b => b.When(session => session.SendLocal(new TestMessage()))) .Run(); - Assert.That(context.MessageReceived, Is.True, "Message was received"); + using (Assert.EnterMultipleScope()) + { + Assert.That(context.MessageReceived, Is.True, "Message was received"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(DateTime.TryParse(context.SendFileCreatedAt, out _), Is.True, "SendFileCreatedAt should be a valid datetime"); + } } [Test] @@ -73,8 +100,8 @@ public async Task Should_preserve_file_created_time_property_when_moved_to_error using (Assert.EnterMultipleScope()) { Assert.That(context.MessageMovedToErrorQueue, Is.True, "Message was not moved to error queue"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present on message in error queue"); - Assert.That(DateTime.TryParse(context.FileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be present on message in error queue"); + Assert.That(DateTime.TryParse(context.ReceiveFileCreatedAt, out _), Is.True, "FileCreatedAt should be a valid datetime"); } } @@ -92,21 +119,24 @@ public async Task Should_not_override_error_queue_dispatch_properties_with_recei using (Assert.EnterMultipleScope()) { Assert.That(context.MessageMovedToErrorQueue, Is.True, "Message was not moved to error queue"); - Assert.That(context.FileCreatedAt, Is.Not.Null, "FileCreatedAt property should be captured from original message"); + Assert.That(context.ReceiveFileCreatedAt, Is.Not.Null, "FileCreatedAt property should be captured from original message"); Assert.That(context.ErrorQueueFileCreatedAtDiffersFromOriginal, Is.True, "Error queue message should have FileCreatedAt from dispatch properties, not receive properties"); } } - class Context : ScenarioContext + public class Context : ScenarioContext { public bool MessageReceived { get; set; } - public string FileCreatedAt { get; set; } public bool MessageAudited { get; set; } public bool MessageMovedToErrorQueue { get; set; } public bool ErrorQueueFileCreatedAtDiffersFromOriginal { get; set; } + public int NumberOfRetries { get; set; } + public string ReceiveFileCreatedAt { get; set; } + public string RetryFileCreatedAt { get; set; } + public string SendFileCreatedAt { get; set; } } - class EndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder + public class EndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder { public EndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup(endpointConfiguration => { @@ -114,7 +144,8 @@ public EndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup + [Handler] + public class OutgoingTestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) { @@ -122,7 +153,7 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } else { @@ -144,11 +175,12 @@ public override Task Invoke(IRoutingContext context, Func next) } } - class AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder + public class AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint : EndpointConfigurationBuilder { public AuditSpyForEndPointThatReceivesFromAnotherAndAuditsEndpoint() => EndpointSetup(); + [Handler] public class AuditMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) @@ -157,12 +189,12 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is propagated to audit messages.")); } - testContext.MarkAsCompleted(testContext.MessageAudited, testContext.FileCreatedAt != fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageAudited, testContext.ReceiveFileCreatedAt != fileCreatedAt); } else { @@ -174,18 +206,19 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) } } - class SendingEndpoint : EndpointConfigurationBuilder + public class SendingEndpoint : EndpointConfigurationBuilder { public SendingEndpoint() => EndpointSetup(); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public async Task Handle(TestMessage message, IMessageHandlerContext context) { testContext.MessageReceived = true; if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; await context.SendLocal(new OutgoingTestMessage()); } @@ -197,19 +230,21 @@ public async Task Handle(TestMessage message, IMessageHandlerContext context) } //handler for the outgoing message to verify that receive properties are not propagated to outgoing messages - class OutgoingTestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class OutgoingTestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) { testContext.MessageReceived = true; if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + testContext.SendFileCreatedAt = fileCreatedAt; + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is propagated to outgoing messages.")); } - testContext.MarkAsCompleted(testContext.MessageReceived, testContext.FileCreatedAt != fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageReceived, testContext.ReceiveFileCreatedAt != fileCreatedAt); } else { @@ -220,14 +255,15 @@ public Task Handle(OutgoingTestMessage message, IMessageHandlerContext context) } } - class Endpoint : EndpointConfigurationBuilder + public class Endpoint : EndpointConfigurationBuilder { public Endpoint() => EndpointSetup(endpointConfiguration => { endpointConfiguration.AuditProcessedMessagesTo(Conventions.EndpointNamingConvention(typeof(AuditSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -235,28 +271,64 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } else { testContext.MarkAsFailed(new Exception("Failed to retrieve receive properties from the message context.")); } - testContext.MarkAsCompleted(testContext.MessageReceived, testContext.FileCreatedAt != null); + testContext.MarkAsCompleted(testContext.MessageReceived, testContext.ReceiveFileCreatedAt != null); return Task.CompletedTask; } } } - class EndpointWithAuditOn : EndpointConfigurationBuilder + public class FailingEndpoint : EndpointConfigurationBuilder + { + public FailingEndpoint() => EndpointSetup(endpointConfiguration => + { + endpointConfiguration.Recoverability().Delayed(settings => settings.NumberOfRetries(1)); + endpointConfiguration.Recoverability().Immediate(settings => settings.NumberOfRetries(0)); + }); + + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages + { + public Task Handle(TestMessage message, IMessageHandlerContext context) + { + if (testContext.NumberOfRetries == 0) + { + if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) + { + testContext.ReceiveFileCreatedAt = fileCreatedAt; + } + } + else + { + if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) + { + testContext.RetryFileCreatedAt = fileCreatedAt; + testContext.MarkAsCompleted(testContext.ReceiveFileCreatedAt == testContext.RetryFileCreatedAt); + } + } + testContext.NumberOfRetries++; + + throw new SimulatedException("Simulating an exception to see if it preserves receive properties on retries"); + } + } + } + + public class EndpointWithAuditOn : EndpointConfigurationBuilder { public EndpointWithAuditOn() => EndpointSetup(endpointConfiguration => { endpointConfiguration.AuditProcessedMessagesTo(Conventions.EndpointNamingConvention(typeof(AuditSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -264,7 +336,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } return Task.CompletedTask; @@ -272,11 +344,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class AuditSpy : EndpointConfigurationBuilder + public class AuditSpy : EndpointConfigurationBuilder { public AuditSpy() => EndpointSetup(); + [Handler] public class AuditMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -285,12 +358,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt != testContext.FileCreatedAt) + if (fileCreatedAt != testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message is not propagated to audit messages.")); } - testContext.MarkAsCompleted(testContext.MessageAudited, testContext.FileCreatedAt == fileCreatedAt); + testContext.MarkAsCompleted(testContext.MessageAudited, testContext.ReceiveFileCreatedAt == fileCreatedAt); } else { @@ -302,7 +375,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class EndpointWithFailingHandler : EndpointConfigurationBuilder + public class EndpointWithFailingHandler : EndpointConfigurationBuilder { public EndpointWithFailingHandler() => EndpointSetup(endpointConfiguration => { @@ -310,7 +383,8 @@ public EndpointWithFailingHandler() => EndpointSetup(endpointConf endpointConfiguration.SendFailedMessagesTo(Conventions.EndpointNamingConvention(typeof(ErrorSpy))); }); - class TestMessageHandler(Context testContext) : IHandleMessages + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -318,7 +392,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } throw new SimulatedException("Message should be moved to error queue"); @@ -326,7 +400,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class EndpointWithFailingHandlerAndDispatchOverride : EndpointConfigurationBuilder + public class EndpointWithFailingHandlerAndDispatchOverride : EndpointConfigurationBuilder { public EndpointWithFailingHandlerAndDispatchOverride() => EndpointSetup(endpointConfiguration => { @@ -335,7 +409,8 @@ public EndpointWithFailingHandlerAndDispatchOverride() => EndpointSetup + [Handler] + public class TestMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) { @@ -343,7 +418,7 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; + testContext.ReceiveFileCreatedAt = fileCreatedAt; } throw new SimulatedException("Message should be moved to error queue"); @@ -379,10 +454,11 @@ public override IReadOnlyCollection GetRoutingContexts(IRecover } } - class ErrorSpy : EndpointConfigurationBuilder + public class ErrorSpy : EndpointConfigurationBuilder { public ErrorSpy() => EndpointSetup(); + [Handler] public class ErrorMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -391,8 +467,8 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - testContext.FileCreatedAt = fileCreatedAt; - testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.FileCreatedAt != null); + testContext.ReceiveFileCreatedAt = fileCreatedAt; + testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.ReceiveFileCreatedAt != null); } else { @@ -404,10 +480,11 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) } } - class ErrorSpyWithDispatchPropertyVerification : EndpointConfigurationBuilder + public class ErrorSpyWithDispatchPropertyVerification : EndpointConfigurationBuilder { public ErrorSpyWithDispatchPropertyVerification() => EndpointSetup(); + [Handler] public class ErrorMessageHandler(Context testContext) : IHandleMessages { public Task Handle(TestMessage message, IMessageHandlerContext context) @@ -416,12 +493,12 @@ public Task Handle(TestMessage message, IMessageHandlerContext context) if (context.Extensions.TryGet(out var incomingMessage) && incomingMessage.ReceiveProperties.TryGetValue("LearningTransport.FileCreatedAt", out var fileCreatedAt)) { - if (fileCreatedAt == testContext.FileCreatedAt) + if (fileCreatedAt == testContext.ReceiveFileCreatedAt) { testContext.MarkAsFailed(new Exception("Receive properties from the original message were propagated to error queue message instead of dispatch properties.")); } - testContext.ErrorQueueFileCreatedAtDiffersFromOriginal = testContext.FileCreatedAt != fileCreatedAt; + testContext.ErrorQueueFileCreatedAtDiffersFromOriginal = testContext.ReceiveFileCreatedAt != fileCreatedAt; testContext.MarkAsCompleted(testContext.MessageMovedToErrorQueue, testContext.ErrorQueueFileCreatedAtDiffersFromOriginal); } else diff --git a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj index 7e091be3153..3084b595cd6 100644 --- a/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj +++ b/src/NServiceBus.PersistenceTests/NServiceBus.PersistenceTests.csproj @@ -11,11 +11,11 @@ - - + + - - + + diff --git a/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs b/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs new file mode 100644 index 00000000000..2686c9b749c --- /dev/null +++ b/src/NServiceBus.Testing.Fakes/DynamicMemberTypeAccess.cs @@ -0,0 +1,14 @@ +namespace NServiceBus.Testing; + +using System.Diagnostics.CodeAnalysis; + +static class DynamicMemberTypeAccess +{ + public const DynamicallyAccessedMemberTypes Message = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; + + public const string RuntimeTypeRoutingTrimmingMessage = "When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type."; + +} diff --git a/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj b/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj index 38497d82768..64ad33a4a79 100644 --- a/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj +++ b/src/NServiceBus.Testing.Fakes/NServiceBus.Testing.Fakes.csproj @@ -11,4 +11,8 @@ + + + + diff --git a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs index 04716b85aeb..cd9fe1f780b 100644 --- a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs @@ -1,6 +1,9 @@ namespace NServiceBus.Testing; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Pipeline; using Unicast.Messages; @@ -35,8 +38,33 @@ public TestableIncomingLogicalMessageContext(IMessageCreator messageCreator = nu /// Updates the message instance contained in . /// /// The new instance. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual void UpdateMessageInstance(object newInstance) { Message = new LogicalMessage(new MessageMetadata(newInstance.GetType()), newInstance); } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public virtual void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public virtual void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + Message = new LogicalMessage(new MessageMetadata(messageType), newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs b/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs index 376b17fb671..b7b1e4f3674 100644 --- a/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableMessageProcessingContext.cs @@ -4,6 +4,8 @@ namespace NServiceBus.Testing; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading.Tasks; /// @@ -38,17 +40,41 @@ public TestableMessageProcessingContext(IMessageCreator messageCreator = null) : /// /// The message to send. /// Options for this reply. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Reply(object message, ReplyOptions options) { repliedMessages.Enqueue(new RepliedMessage(message, options)); return Task.CompletedTask; } + /// + /// Sends the typed message to the endpoint which sent the message currently being handled. + /// + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// Options for this reply. + [OverloadResolutionPriority(-1)] + public virtual Task Reply<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, ReplyOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Reply((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Sends the message with the specified message type to the endpoint which sent the message currently being handled. + /// + public virtual Task Reply(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, ReplyOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Reply(message, options); + } + /// /// Instantiates a message of type T and performs a regular /// . /// - /// The type of message, usually an interface. + /// The type used to reply with the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Options for this reply. public virtual Task Reply(Action messageConstructor, ReplyOptions options) diff --git a/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs b/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs index 6a92fa88501..3f9ccf74f3a 100644 --- a/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs +++ b/src/NServiceBus.Testing.Fakes/TestableMessageSession.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using NServiceBus.Extensibility; @@ -45,6 +47,7 @@ public TestableMessageSession(IMessageCreator messageCreator = null) /// /// The message to send. /// The options for the send. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Send(object message, SendOptions options, CancellationToken cancellationToken = default) { var headers = options.GetHeaders(); @@ -58,10 +61,33 @@ public virtual Task Send(object message, SendOptions options, CancellationToken return Task.CompletedTask; } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + public virtual Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options, CancellationToken cancellationToken = default) + { +#pragma warning disable IDE0004 // Cast is redundant + return Send((object)message!, options, cancellationToken); +#pragma warning restore IDE0004 + } + + /// + /// Sends the provided message with the specified message type. + /// + public virtual Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options, cancellationToken); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. public virtual Task Send(Action messageConstructor, SendOptions options, CancellationToken cancellationToken = default) @@ -74,16 +100,40 @@ public virtual Task Send(Action messageConstructor, SendOptions options, C /// /// The message to publish. /// The options for the publish. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Publish(object message, PublishOptions options, CancellationToken cancellationToken = default) { publishedMessages.Enqueue(new PublishedMessage(message, options)); return Task.CompletedTask; } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + public virtual Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options, CancellationToken cancellationToken = default) + { +#pragma warning disable IDE0004 // Cast is redundant + return Publish((object)message!, options, cancellationToken); +#pragma warning restore IDE0004 + } + + /// + /// Publishes the provided message with the specified message type. + /// + public virtual Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options, CancellationToken cancellationToken = default) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options, cancellationToken); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. public virtual Task Publish(Action messageConstructor, PublishOptions publishOptions, CancellationToken cancellationToken = default) diff --git a/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs b/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs index 01277fb6018..170b8bec21b 100644 --- a/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableOutgoingLogicalMessageContext.cs @@ -1,6 +1,9 @@ namespace NServiceBus.Testing; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Pipeline; using Routing; @@ -12,11 +15,30 @@ public partial class TestableOutgoingLogicalMessageContext : TestableOutgoingCon /// /// Updates the message instance. /// + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual void UpdateMessage(object newInstance) { Message = new OutgoingLogicalMessage(newInstance.GetType(), newInstance); } + /// + /// Updates the message instance while preserving the specified message type. + /// + [OverloadResolutionPriority(-1)] + public virtual void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessage(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance with the specified message type. + /// + public virtual void UpdateMessage(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + Message = new OutgoingLogicalMessage(messageType, newInstance); + } + /// /// The outgoing message. /// diff --git a/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs b/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs index 4090f548108..3eb4a9d1107 100644 --- a/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestablePipelineContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Extensibility; @@ -43,6 +44,7 @@ public partial class TestablePipelineContext : IPipelineContext /// /// The message to send. /// The options for the send. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Send(object message, SendOptions options) { var headers = options.GetHeaders(); @@ -56,10 +58,33 @@ public virtual Task Send(object message, SendOptions options) return Task.CompletedTask; } + /// + /// Sends the provided typed message. + /// + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to send. + /// The options for the send. + [OverloadResolutionPriority(-1)] + public virtual Task Send<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, SendOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Send((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Sends the provided message with the specified message type. + /// + public virtual Task Send(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, SendOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Send(message, options); + } + /// /// Instantiates a message of type T and sends it. /// - /// The type of message, usually an interface. + /// The type used to send the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// The options for the send. public virtual Task Send(Action messageConstructor, SendOptions options) @@ -72,16 +97,40 @@ public virtual Task Send(Action messageConstructor, SendOptions options) /// /// The message to publish. /// The options for the publish. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual Task Publish(object message, PublishOptions options) { publishedMessages.Enqueue(new PublishedMessage(message, options)); return Task.CompletedTask; } + /// + /// Publishes the provided typed message. + /// + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The message to publish. + /// The options for the publish. + [OverloadResolutionPriority(-1)] + public virtual Task Publish<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T message, PublishOptions options) + { +#pragma warning disable IDE0004 // Cast is redundant + return Publish((object)message!, options); +#pragma warning restore IDE0004 + } + + /// + /// Publishes the provided message with the specified message type. + /// + public virtual Task Publish(object message, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType, PublishOptions options) + { + MessageTypeValidator.Validate(message, messageType); + return Publish(message, options); + } + /// /// Instantiates a message of type T and publishes it. /// - /// The type of message, usually an interface. + /// The type used to publish the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// An action which initializes properties of the message. /// Specific options for this event. public virtual Task Publish(Action messageConstructor, PublishOptions publishOptions) diff --git a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj index 99bd27b1320..a729570decd 100644 --- a/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj +++ b/src/NServiceBus.TransportTests/NServiceBus.TransportTests.csproj @@ -9,11 +9,11 @@ - - + + - - + + diff --git a/src/NServiceBus.TransportTests/When_message_is_available.cs b/src/NServiceBus.TransportTests/When_message_is_available.cs index 25188a0bed9..f7298fa98d7 100644 --- a/src/NServiceBus.TransportTests/When_message_is_available.cs +++ b/src/NServiceBus.TransportTests/When_message_is_available.cs @@ -1,5 +1,6 @@ namespace NServiceBus.TransportTests; +using System; using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; @@ -15,18 +16,18 @@ public async Task Should_invoke_on_message(TransportTransactionMode transactionM { var onMessageInvoked = CreateTaskCompletionSource(); - byte[] messageBody = null; - await StartPump( - (context, _) => - { - messageBody = context.Body.ToArray(); - return onMessageInvoked.SetCompleted(context); - }, + (context, _) => onMessageInvoked.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)), (_, __) => Task.FromResult(ErrorHandleResult.Handled), transactionMode); - await SendMessage(InputQueueName, new Dictionary { { "MyHeader", "MyValue" } }, body: new byte[] { 1, 2, 3 }); + await SendMessage(InputQueueName, new Dictionary { { "MyHeader", "MyValue" } }, body: [1, 2, 3]); var messageContext = await onMessageInvoked.Task; @@ -34,7 +35,7 @@ await StartPump( { Assert.That(string.IsNullOrEmpty(messageContext.NativeMessageId), Is.False, "Should pass the native message id"); Assert.That(messageContext.Headers["MyHeader"], Is.EqualTo("MyValue"), "Should pass the message headers"); - Assert.That(messageBody, Is.EqualTo(new byte[] { 1, 2, 3 }), "Should pass the body"); + Assert.That(messageContext.Body.Span.SequenceEqual(new byte[] { 1, 2, 3 }), Is.True, "Should pass the body"); } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs b/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs index c6dc9e2f512..084aa296d39 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_before_on_error.cs @@ -24,7 +24,15 @@ await StartPump( }, (context, __) => { - errorHandled.SetResult(context); + errorHandled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -35,4 +43,4 @@ await StartPump( Assert.That(errorContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs b/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs index d861b612f64..4b9d7eabf4d 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_before_retry.cs @@ -22,7 +22,13 @@ await StartPump( { if (retrying) { - return retried.SetCompleted(context); + return retried.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)); } context.Headers["test-header"] = "modified"; @@ -41,4 +47,4 @@ await StartPump( Assert.That(retryMessageContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs b/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs index c5df1706a3f..0751e7c2b5a 100644 --- a/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs +++ b/src/NServiceBus.TransportTests/When_modifying_headers_in_on_error.cs @@ -18,7 +18,13 @@ public async Task Should_roll_back(TransportTransactionMode transactionMode) var retrying = false; await StartPump( - (context, _) => retrying ? retried.SetCompleted(context) : throw new Exception(), + (context, _) => retrying ? retried.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)) : throw new Exception(), (context, _) => { retrying = true; @@ -33,4 +39,4 @@ await StartPump( Assert.That(retryMessageContext.Headers["test-header"], Is.EqualTo("original")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws.cs b/src/NServiceBus.TransportTests/When_on_message_throws.cs index 2dc25b8513c..5dcac8caa74 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws.cs @@ -20,7 +20,15 @@ await StartPump( (_, __) => throw new Exception("Simulated exception"), (context, _) => { - onErrorCalled.SetResult(context); + onErrorCalled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -36,4 +44,4 @@ await StartPump( Assert.That(errorContext.Headers["MyHeader"], Is.EqualTo("MyValue"), "Should pass the message headers"); } } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs b/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs index 079dda0ea84..02a13646848 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws_after_delayed_retry.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -27,7 +28,15 @@ await StartPump( } else { - sentDelayedMessage.SetResult(context); + sentDelayedMessage.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); } return ErrorHandleResult.Handled; @@ -40,4 +49,4 @@ await StartPump( Assert.That(errorContext.ImmediateProcessingFailures, Is.EqualTo(1), "Should track delivery attempts between immediate retries"); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs b/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs index 68fd71df2fc..5dece053e0b 100644 --- a/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs +++ b/src/NServiceBus.TransportTests/When_on_message_throws_after_immediate_retry.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( { if (attempts == maxAttempts) { - maxAttemptsReached.SetResult(context); + maxAttemptsReached.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); } @@ -37,4 +46,4 @@ await StartPump( Assert.That(errorContext.ImmediateProcessingFailures, Is.EqualTo(attempts), "Should track delivery attempts between immediate retries"); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_receiving_message.cs b/src/NServiceBus.TransportTests/When_receiving_message.cs index 28a6fdcb503..b99a74ccf70 100644 --- a/src/NServiceBus.TransportTests/When_receiving_message.cs +++ b/src/NServiceBus.TransportTests/When_receiving_message.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( }, (context, _) => { - onError.SetResult(context); + onError.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -33,4 +42,4 @@ await StartPump( var errorContext = await onError.Task; Assert.That(errorContext.ReceiveAddress, Is.EqualTo(receiver.ReceiveAddress)); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_scope_complete_throws.cs b/src/NServiceBus.TransportTests/When_scope_complete_throws.cs index c25340d9ce5..84b5fc3c4ce 100644 --- a/src/NServiceBus.TransportTests/When_scope_complete_throws.cs +++ b/src/NServiceBus.TransportTests/When_scope_complete_throws.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Transactions; using NUnit.Framework; @@ -25,7 +26,15 @@ await StartPump( }, (context, _) => { - onErrorCalled.SetResult(context); + onErrorCalled.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -57,4 +66,4 @@ class EnlistmentWhichFailsDuringPrepare : IEnlistmentNotification public void InDoubt(Enlistment enlistment) => enlistment.Done(); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs b/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs index f6e97d2fa03..7e609f63e21 100644 --- a/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs +++ b/src/NServiceBus.TransportTests/When_scope_throws_after_successful_message_processing.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Transactions; using NUnit.Framework; @@ -30,7 +31,15 @@ await StartPump( return Task.FromResult(ErrorHandleResult.RetryRequired); } - secondFailure.SetResult(context); + secondFailure.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, diff --git a/src/NServiceBus.TransportTests/When_setting_context_items.cs b/src/NServiceBus.TransportTests/When_setting_context_items.cs index 9d038633172..9088f0be4ff 100644 --- a/src/NServiceBus.TransportTests/When_setting_context_items.cs +++ b/src/NServiceBus.TransportTests/When_setting_context_items.cs @@ -1,6 +1,7 @@ namespace NServiceBus.TransportTests; using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Transport; @@ -23,7 +24,15 @@ await StartPump( }, (context, _) => { - onError.SetResult(context); + onError.SetResult(new ErrorContext( + context.Exception, + new Dictionary(context.Headers), + context.NativeMessageId, + context.Body.ToArray(), + context.TransportTransaction, + context.ImmediateProcessingFailures, + context.ReceiveAddress, + context.Extensions)); return Task.FromResult(ErrorHandleResult.Handled); }, transactionMode); @@ -34,4 +43,4 @@ await StartPump( Assert.That(errorContext.Extensions.Get("MyKey"), Is.EqualTo("MyValue")); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs b/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs index 3bdf0f532c9..101648e1b5b 100644 --- a/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs +++ b/src/NServiceBus.TransportTests/When_using_unicode_characters_in_headers.cs @@ -13,7 +13,13 @@ public async Task Should_support_unicode_characters() var messageProcessed = CreateTaskCompletionSource(); await StartPump( - (context, _) => messageProcessed.SetCompleted(context), + (context, _) => messageProcessed.SetCompleted(new MessageContext( + context.NativeMessageId, + new Dictionary(context.Headers), + context.Body.ToArray(), + context.TransportTransaction, + context.ReceiveAddress, + context.Extensions)), (_, __) => Task.FromResult(ErrorHandleResult.Handled), TransportTransactionMode.None); @@ -35,4 +41,4 @@ await StartPump( Assert.That(messageContext.Headers, Is.Not.Empty); Assert.That(messageContext.Headers, Is.SupersetOf(headers)); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.slnx b/src/NServiceBus.slnx index 2921f61f2ae..db4c21f2494 100644 --- a/src/NServiceBus.slnx +++ b/src/NServiceBus.slnx @@ -12,12 +12,12 @@ - + diff --git a/src/Shared/MessageTypeValidator.cs b/src/Shared/MessageTypeValidator.cs new file mode 100644 index 00000000000..dbfdd0d0d63 --- /dev/null +++ b/src/Shared/MessageTypeValidator.cs @@ -0,0 +1,22 @@ +namespace NServiceBus; + +using System; +using System.Diagnostics.CodeAnalysis; + +static class MessageTypeValidator +{ + internal static void Validate(object message, [DynamicallyAccessedMembers(MessageTypeAccess)] Type messageType) + { + ArgumentNullException.ThrowIfNull(message); + ArgumentNullException.ThrowIfNull(messageType); + if (!messageType.IsInstanceOfType(message)) + { + throw new ArgumentException($"The message instance of type '{message.GetType()}' is not assignable to the declared message type '{messageType}'.", nameof(message)); + } + } + + const DynamicallyAccessedMemberTypes MessageTypeAccess = DynamicallyAccessedMemberTypes.PublicConstructors + | DynamicallyAccessedMemberTypes.NonPublicConstructors + | DynamicallyAccessedMemberTypes.PublicProperties + | DynamicallyAccessedMemberTypes.Interfaces; +} diff --git a/src/TrimmedEndpoint/Program.cs b/src/TrimmedEndpoint/Program.cs new file mode 100644 index 00000000000..7c24eb84792 --- /dev/null +++ b/src/TrimmedEndpoint/Program.cs @@ -0,0 +1,354 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using NServiceBus.Features; +using NServiceBus.Installation; +using NServiceBus.MessageMutator; +using NServiceBus.Pipeline; + +#if INCLUDE_SAGA +using NServiceBus.Configuration.AdvancedExtensibility; +#endif + +var configuration = new EndpointConfiguration("TrimmedEndpoint"); +string? startupDiagnostics = null; +configuration.CustomDiagnosticsWriter((diagnostics, _) => +{ + startupDiagnostics = diagnostics; + return Task.CompletedTask; +}); +configuration.AssemblyScanner().Disable = true; +configuration.UseSerialization().Options(new JsonSerializerOptions +{ + TypeInfoResolver = TrimmedEndpointJsonContext.Default +}); +var storageDirectory = Path.Combine(Path.GetTempPath(), "nservicebus-learning-trimmed"); +configuration.UseTransport().StorageDirectory(storageDirectory); +configuration.UsePersistence(); +#if INCLUDE_SAGA +// The learning saga persister serializes saga data with System.Text.Json; provide source-generated metadata for AOT. +configuration.GetSettings().Set("LearningSagaPersistence.SerializerOptions", new JsonSerializerOptions +{ + TypeInfoResolver = SagaDataJsonContext.Default +}); +#endif + +// Only send-only message types need explicit registration: types handled by AddHandler/AddSaga are +// registered by the generated code, and duplicating them here would mask failures in that automatic path. +// Duplicate registration must be harmless (first registration wins). +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.AddMessageType(); +configuration.Conventions().DefiningCommandsAs(type => + type == typeof(UnobtrusiveCommand) || + (type != typeof(ICommand) && typeof(ICommand).IsAssignableFrom(type))); +configuration.EnableFeature(); +configuration.EnableInstallers(); +configuration.AddHandler(); +// Exercise the typed replacement APIs so their trimming-sensitive, annotated paths are covered. +configuration.RegisterMessageMutator(new ReplacesIncomingMessageInstance()); +configuration.RegisterMessageMutator(new ReplacesOutgoingMessage()); +#if INCLUDE_SAGA +configuration.AddSaga(); +#endif + +var endpoint = await Endpoint.Start(configuration); +IMessageSession session = endpoint; + +// Generated handler path. +await session.SendLocal(new MyCommand { SomeValue = "hello" }); +await WaitFor(() => MyHandler.Invoked); + +#if INCLUDE_SAGA +// Saga start + timeout path. +await session.SendLocal(new StartOrderCommand { OrderId = "order-1" }); +await WaitFor(() => OrderSaga.Started && OrderSaga.TimedOut); + +// Saga handle path (correlated via OrderId). +await session.SendLocal(new HandleOrderCommand { OrderId = "order-1" }); +await WaitFor(() => OrderSaga.Handled); +#endif + +// Outgoing-only AddMessageType path: routing to a non-existent queue exercises metadata + serialization only. +await session.Send("NonExistentQueue", new OutgoingCommand { SomeValue = "outgoing" }); + +// Unobtrusive message type: AddMessageType was called before the convention was configured, proving that +// generated registration is evaluated against the finalized conventions in trimmed and AOT deployments. +await session.Send("NonExistentQueue", new UnobtrusiveCommand { SomeValue = "unobtrusive" }); + +// The endpoint runs with assembly scanning disabled in a trimmed/AOT application, so strict registered-only +// message metadata mode must be active: sending an unregistered message type has to fail with an actionable +// error instead of being registered on demand. +var strictModeVerified = false; +try +{ + await session.SendLocal(new UnregisteredMessage()); +} +catch (Exception ex) +{ + strictModeVerified = ContainsStrictModeMessage(ex); +} + +await endpoint.Stop(); + +var startupDiagnosticsVerified = false; +if (startupDiagnostics is not null) +{ + using var diagnosticsDocument = JsonDocument.Parse(startupDiagnostics); + var root = diagnosticsDocument.RootElement; + startupDiagnosticsVerified = root.TryGetProperty("Endpoint", out _) && + root.TryGetProperty("Hosting", out _) && + root.TryGetProperty("Messages", out _) && + root.TryGetProperty(TrimmedScenarioFeature.DiagnosticsSectionName, out var featureDiagnostics) && + featureDiagnostics.TryGetProperty("FeatureConfigured", out var featureConfigured) && + featureConfigured.GetBoolean(); +} + +if (!MyHandler.Invoked) +{ + Console.Error.WriteLine("Handler was not invoked."); + return 1; +} + +#if INCLUDE_SAGA +if (!OrderSaga.Started || !OrderSaga.TimedOut || !OrderSaga.Handled) +{ + Console.Error.WriteLine($"SagaStarted={OrderSaga.Started} SagaTimedOut={OrderSaga.TimedOut} SagaHandled={OrderSaga.Handled}"); + return 2; +} +#endif + +if (!strictModeVerified) +{ + Console.Error.WriteLine("Strict registered-only message metadata mode was not active."); + return 3; +} + +if (!startupDiagnosticsVerified) +{ + Console.Error.WriteLine("Startup diagnostics did not contain the expected Core and custom feature sections."); + return 4; +} + +if (!TrimmedScenarioInstaller.Invoked || !TrimmedScenarioBehavior.Invoked) +{ + Console.Error.WriteLine($"InstallerInvoked={TrimmedScenarioInstaller.Invoked} BehaviorInvoked={TrimmedScenarioBehavior.Invoked}"); + return 5; +} + +if (!ReplacesIncomingMessageInstance.Replaced || !MyHandler.ReceivedReplacedInstance || !ReplacesOutgoingMessage.Replaced) +{ + Console.Error.WriteLine($"IncomingReplaced={ReplacesIncomingMessageInstance.Replaced} HandlerReceivedReplacedInstance={MyHandler.ReceivedReplacedInstance} OutgoingReplaced={ReplacesOutgoingMessage.Replaced}"); + return 6; +} + +Console.WriteLine("TRIM-VALIDATION-SUCCESS"); +return 0; + +static async Task WaitFor(Func condition, int iterations = 150) +{ + for (var i = 0; i < iterations && !condition(); i++) + { + await Task.Delay(100); + } +} + +static bool ContainsStrictModeMessage(Exception exception) +{ + for (var current = exception; current is not null; current = current.InnerException) + { + if (current.Message.Contains("strict registered-only message metadata mode")) + { + return true; + } + } + + return false; +} + +[Handler] +public class MyHandler : IHandleMessages +{ + public static bool Invoked; + public static bool ReceivedReplacedInstance; + + public Task Handle(MyCommand message, IMessageHandlerContext context) + { + Invoked = true; + ReceivedReplacedInstance = message.SomeValue == "replaced"; + return Task.CompletedTask; + } +} + +public sealed class ReplacesIncomingMessageInstance : IMutateIncomingMessages +{ + public static bool Replaced; + + public Task MutateIncoming(MutateIncomingMessageContext context) + { + if (context.Message is MyCommand) + { + context.UpdateMessageInstance(new MyCommand { SomeValue = "replaced" }); + Replaced = true; + } + + return Task.CompletedTask; + } +} + +public sealed class ReplacesOutgoingMessage : IMutateOutgoingMessages +{ + public static bool Replaced; + + public Task MutateOutgoing(MutateOutgoingMessageContext context) + { + if (context.OutgoingMessage is OutgoingCommand) + { + context.UpdateMessage(new OutgoingCommand { SomeValue = "replaced" }); + Replaced = true; + } + + return Task.CompletedTask; + } +} + +#if INCLUDE_SAGA +[Saga] +public class OrderSaga : Saga, + IAmStartedByMessages, + IHandleMessages, + IHandleTimeouts +{ + public static bool Started; + public static bool Handled; + public static bool TimedOut; + + protected override void ConfigureHowToFindSaga(SagaPropertyMapper mapper) + { + mapper.MapSaga(s => s.OrderId) + .ToMessage(m => m.OrderId) + .ToMessage(m => m.OrderId); + } + + public Task Handle(StartOrderCommand message, IMessageHandlerContext context) + { + Started = true; + return RequestTimeout(context, TimeSpan.FromMilliseconds(300)); + } + + public Task Handle(HandleOrderCommand message, IMessageHandlerContext context) + { + Handled = true; + return Task.CompletedTask; + } + + public Task Timeout(OrderTimeout state, IMessageHandlerContext context) + { + TimedOut = true; + return Task.CompletedTask; + } +} + +public class OrderSagaData : ContainSagaData +{ + public string OrderId { get; set; } = string.Empty; +} + +public class StartOrderCommand : ICommand +{ + public string OrderId { get; set; } = string.Empty; +} + +public class HandleOrderCommand : ICommand +{ + public string OrderId { get; set; } = string.Empty; +} + +public class OrderTimeout : IMessage; +#endif + +public class MyCommand : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class OutgoingCommand : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class UnregisteredMessage : ICommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public class UnobtrusiveCommand +{ + public string SomeValue { get; set; } = string.Empty; +} + +public sealed class TrimmedScenarioFeature : Feature +{ + public const string DiagnosticsSectionName = "TrimmedScenario"; + + protected override void Setup(FeatureConfigurationContext context) + { + context.AddInstaller(); + context.Pipeline.Register("TrimmedScenarioBehavior", new TrimmedScenarioBehavior(), "Verifies a user-defined behavior under trimming and NativeAOT"); + context.Settings.AddStartupDiagnosticsSection( + DiagnosticsSectionName, + new TrimmedScenarioDiagnostics { FeatureConfigured = true }, + TrimmedScenarioJsonContext.Default.TrimmedScenarioDiagnostics); + } +} + +public sealed class TrimmedScenarioInstaller : INeedToInstallSomething +{ + public static bool Invoked; + + public Task Install(string identity, CancellationToken cancellationToken = default) + { + Invoked = true; + return Task.CompletedTask; + } +} + +public sealed class TrimmedScenarioBehavior : Behavior +{ + public static bool Invoked; + + public override Task Invoke(IOutgoingLogicalMessageContext context, Func next) + { + Invoked = true; + return next(); + } +} + +public sealed class TrimmedScenarioDiagnostics +{ + public bool FeatureConfigured { get; set; } +} + +[JsonSerializable(typeof(MyCommand))] +[JsonSerializable(typeof(OutgoingCommand))] +[JsonSerializable(typeof(UnobtrusiveCommand))] +[JsonSerializable(typeof(UnregisteredMessage))] +#if INCLUDE_SAGA +[JsonSerializable(typeof(StartOrderCommand))] +[JsonSerializable(typeof(HandleOrderCommand))] +[JsonSerializable(typeof(OrderTimeout))] +#endif +public partial class TrimmedEndpointJsonContext : JsonSerializerContext +{ +} + +[JsonSerializable(typeof(TrimmedScenarioDiagnostics))] +public partial class TrimmedScenarioJsonContext : JsonSerializerContext +{ +} + +#if INCLUDE_SAGA +[JsonSerializable(typeof(OrderSagaData))] +public partial class SagaDataJsonContext : JsonSerializerContext +{ +} +#endif \ No newline at end of file diff --git a/src/TrimmedEndpoint/TrimmedEndpoint.csproj b/src/TrimmedEndpoint/TrimmedEndpoint.csproj new file mode 100644 index 00000000000..f6ceef69bf4 --- /dev/null +++ b/src/TrimmedEndpoint/TrimmedEndpoint.csproj @@ -0,0 +1,35 @@ + + + + Exe + net10.0 + enable + enable + $(InterceptorsNamespaces);NServiceBus + + $(NoWarn);CS0618;CA2007;CA1050;CA2211 + + true + + true + + $(DefineConstants);INCLUDE_SAGA + + + + + + + + + + + + + + + + + +