Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.Sagas.AddSagaGenerator/HandlerRegistrations.Sagas.g.cs =======
// <auto-generated/>

#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();
}

/// <summary>
/// Registers the <see cref="global::FinderOnly.FinderOnlySaga"/> saga with the endpoint configuration.
/// </summary>
public void AddFinderOnlySaga()
{
var sagaMetadataCollection = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration)
.GetOrCreate<NServiceBus.Sagas.SagaMetadataCollection>();
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<global::FinderOnly.FinderOnlySaga, global::FinderOnly.FinderOnlySagaData>(associatedMessages, null, propertyAccessors);
sagaMetadataCollection.Add(metadata);

var settings = NServiceBus.Configuration.AdvancedExtensibility.AdvancedExtensibilityExtensions.GetSettings(_configuration);
var messageHandlerRegistry = settings.GetOrCreate<NServiceBus.Unicast.MessageHandlerRegistry>();
var messageMetadataRegistry = settings.GetOrCreate<NServiceBus.Unicast.Messages.MessageMetadataRegistry>();
messageHandlerRegistry.AddMessageHandlerForMessage<global::FinderOnly.FinderOnlySaga, global::FinderOnly.StartSagaMessage>();
messageMetadataRegistry.RegisterMessageTypeWithHierarchy(typeof(global::FinderOnly.StartSagaMessage), []);
}
}
}
}
}

// == NServiceBus.Core.Analyzer/NServiceBus.Core.Analyzer.AddHandlerAndSagasRegistrationGenerator/HandlerRegistrations.g.cs ==
// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618

namespace NServiceBus
{
/// <summary>
/// Provides access to handler and saga registries discovered in the <i>FinderOnlySagas</i> assembly.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("NService.Core.Analyzer.Tests", "1.0.0")]
public static partial class FinderOnlySagasHandlerRegistryExtensions
{
extension (global::NServiceBus.HandlerRegistry registry)
{
/// <summary>
/// Gets the root registry for handler and saga types in the <i>FinderOnlySagas</i> assembly.
/// </summary>
/// <remarks>
/// Use the returned registry to access namespace-specific registries and add-all methods for this assembly.
/// </remarks>
public FinderOnlySagasRootRegistry FinderOnlySagasAssembly => new(registry.Configuration);
}

/// <summary>
/// Root registry to add handlers and sagas for the entire assembly.
/// </summary>
public sealed partial class FinderOnlySagasRootRegistry(global::NServiceBus.EndpointConfiguration configuration)
{
readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration));

/// <summary>
/// Gets the registry for the <i>FinderOnly</i> namespace segment under this branch.
/// </summary>
public FinderOnlyRegistry FinderOnly => new(_configuration);

/// <summary>
/// Registers all handlers and sagas for this namespace segment and its child namespaces.
/// </summary>
/// <remarks>
/// Includes child registries: <see cref="FinderOnly"/>.
/// </remarks>
public void AddAll()
{
AddAllHandlers();
AddAllSagas();
}

/// <summary>
/// Registers all handlers for this namespace segment and its child namespaces.
/// </summary>
/// <remarks>
/// Includes child registries: <see cref="FinderOnly"/>.
/// </remarks>
public void AddAllHandlers()
{
AddAllHandlersCore();
FinderOnly.AddAllHandlers();
}

/// <summary>
/// Registers all sagas for this namespace segment and its child namespaces.
/// </summary>
/// <remarks>
/// Includes child registries: <see cref="FinderOnly"/>.
/// </remarks>
public void AddAllSagas()
{
AddAllSagasCore();
FinderOnly.AddAllSagas();
}

partial void AddAllHandlersCore();
partial void AddAllSagasCore();

/// <summary>
/// Registry for the <i>FinderOnly</i> namespace segment. Use this registry to add handlers and sagas for this branch.
/// </summary>
public sealed partial class FinderOnlyRegistry(global::NServiceBus.EndpointConfiguration configuration)
{
readonly global::NServiceBus.EndpointConfiguration _configuration = configuration ?? throw new System.ArgumentNullException(nameof(configuration));

/// <summary>
/// Registers all handlers and sagas for this namespace segment and its child namespaces.
/// </summary>
/// <remarks>
/// Includes sagas in this namespace: <see cref="AddFinderOnlySaga"/>.
/// </remarks>
public void AddAll()
{
AddAllHandlers();
AddAllSagas();
}

/// <summary>
/// Registers all handlers for this namespace segment and its child namespaces.
/// </summary>
public void AddAllHandlers()
{
AddAllHandlersCore();
}

/// <summary>
/// Registers all sagas for this namespace segment and its child namespaces.
/// </summary>
/// <remarks>
/// Includes sagas in this namespace: <see cref="AddFinderOnlySaga"/>.
/// </remarks>
public void AddAllSagas()
{
AddAllSagasCore();
}

partial void AddAllHandlersCore();
partial void AddAllSagasCore();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<FinderOnlySagaData>,
IAmStartedByMessages<StartSagaMessage>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<FinderOnlySagaData> mapper)
{
mapper.ConfigureFinderMapping<StartSagaMessage, FinderOnlyFinder>();
}

public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask;
}

public class FinderOnlySagaData : ContainSagaData
{
public string Property { get; set; }
}

public class FinderOnlyFinder : ISagaFinder<FinderOnlySagaData, StartSagaMessage>
{
public Task<FinderOnlySagaData> FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData));
}

public class StartSagaMessage : IMessage;
}
""";

SourceGeneratorTest.ForIncrementalGenerator<AddSagaGenerator>()
.WithIncrementalGenerator<AddHandlerAndSagasRegistrationGenerator>()
.WithSource(source, "test.cs")
.Run()
.Approve()
.AssertRunsAreEqual();
}

[Test]
public void BasicSagas()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,60 @@ public class SampleCommand : ICommand
Assert.That(diagnostics, Does.Not.Contain("IL2026"));
}

[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<FinderOnlySaga>();
}
}

public class FinderOnlySaga : Saga<FinderOnlySagaData>,
IAmStartedByMessages<StartSagaMessage>
{
protected override void ConfigureHowToFindSaga(SagaPropertyMapper<FinderOnlySagaData> mapper)
{
mapper.ConfigureFinderMapping<StartSagaMessage, FinderOnlyFinder>();
}

public Task Handle(StartSagaMessage message, IMessageHandlerContext context) => Task.CompletedTask;
}

public class FinderOnlySagaData : ContainSagaData
{
public string Property { get; set; }
}

public class FinderOnlyFinder : ISagaFinder<FinderOnlySagaData, StartSagaMessage>
{
public Task<FinderOnlySagaData> FindBy(StartSagaMessage message, ISynchronizedStorageSession storageSession, IReadOnlyContextBag context, CancellationToken cancellationToken = default) => Task.FromResult(default(FinderOnlySagaData));
}

public class StartSagaMessage : IMessage;
""";

var result = SourceGeneratorTest.ForIncrementalGenerator<AddSagaInterceptor>()
.WithSource(source, "test.cs")
.WithAnalyzer<MockTrimmingAnalyzer>()
.WithSuppressor<AddSagaInterceptorSuppressor>()
.Run();

var diagnostics = result.GetCompilationOutput();

Assert.That(diagnostics, Does.Not.Contain("IL2026"));
}

[Test]
public void DoesNotSuppressIL2026ForNonAddSagaCalls()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
namespace NServiceBus.Core.Analyzer.Sagas;

using System.Collections.Immutable;
using NServiceBus.Core.Analyzer.Handlers;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
Expand Down Expand Up @@ -45,10 +46,22 @@ 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))
{
context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic));
continue;
}

// Only suppress when an interceptor can actually be emitted for this call site. A saga that cannot be
// parsed (no Saga<TSagaData> base, abstract, or otherwise unsupported) keeps the RequiresUnreferencedCode
// fallback warning.
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)
{
continue;
}

context.ReportSuppression(Suppression.Create(SuppressRUCDiagnostic, diagnostic));
}
}

Expand Down
14 changes: 10 additions & 4 deletions src/NServiceBus.Core.Analyzer/Sagas/Sagas.Emitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ static void EmitSagaMetadataAdd(SourceWriter sourceWriter, SagaSpec details)
sourceWriter.WriteLine($"{propertyAccessorClassName}.Instance,");
}

var correlationPropertyAccessorClassName = CorrelationPropertyAccessorName(details.SagaDataFullyQualifiedName, 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);");
}
Expand Down Expand Up @@ -137,7 +139,11 @@ static void EmitCorrelationPropertyAccessors(SourceWriter sourceWriter, Immutabl
var uniqueMappings = new Dictionary<(string SagaDataType, string PropertyType, string PropertyName), (CorrelationPropertyMappingSpec Mapping, string SagaDataType)>();
foreach (var saga in sagas)
{
var mapping = saga.CorrelationPropertyMapping;
if (saga.CorrelationPropertyMapping is not { } mapping)
{
continue;
}

var key = (saga.SagaDataFullyQualifiedName, mapping.PropertyType, mapping.PropertyName);
if (!uniqueMappings.ContainsKey(key))
{
Expand Down
9 changes: 5 additions & 4 deletions src/NServiceBus.Core.Analyzer/Sagas/Sagas.Parser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static partial class Sagas

public record SagaSpec : AddHandlerAndSagasRegistrationGenerator.Parser.BaseSpec
{
public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec correlationProperty, ImmutableEquatableArray<PropertyMappingSpec> propertyMappings)
public SagaSpec(HandlerSpec handler, string sagaDataFullyQualifiedName, CorrelationPropertyMappingSpec? correlationProperty, ImmutableEquatableArray<PropertyMappingSpec> propertyMappings)
: base(handler)
{
SagaDataFullyQualifiedName = sagaDataFullyQualifiedName;
Expand All @@ -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<PropertyMappingSpec> PropertyMappings { get; }
public HandlerSpec Handler { get; }
}
Expand Down Expand Up @@ -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)
Expand Down
Loading