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
14 changes: 12 additions & 2 deletions Brighter.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<Platform Name="x86" />
</Configurations>
<Folder Name="/samples/" />
<Folder Name="/samples/Analyzer/">
<Project Path="samples/Analyzer/AnalyzerSamples/AnalyzerSamples.csproj" />
</Folder>
<Folder Name="/samples/CommandProcessor/">
<Project Path="samples/CommandProcessor/HelloWorld/HelloWorld.csproj" />
<Project Path="samples/CommandProcessor/HelloWorldAsync/HelloWorldAsync.csproj" />
Expand Down Expand Up @@ -153,6 +156,7 @@
</Folder>
<Folder Name="/tests/">
<File Path="tests/README.md" />
<Project Path="tests/Paramore.Brighter.Analyzer.Tests/Paramore.Brighter.Analyzer.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.AWS.Tests/Paramore.Brighter.AWS.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.AWS.V4.Tests/Paramore.Brighter.AWS.V4.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.AWSScheduler.Tests/Paramore.Brighter.AWSScheduler.Tests.csproj" />
Expand Down Expand Up @@ -180,14 +184,18 @@
<Project Path="tests/Paramore.Brighter.RocketMQ.Tests/Paramore.Brighter.RocketMQ.Tests/Paramore.Brighter.RocketMQ.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.Sqlite.Tests/Paramore.Brighter.Sqlite.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.Test.Generator.Tests/Paramore.Brighter.Test.Generator.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.Testing.Tests/Paramore.Brighter.Testing.Tests.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<Project Path="tests/Paramore.Brighter.TickerQ.Tests/Paramore.Brighter.TickerQ.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.Transforms.Adaptors.Tests/Paramore.Brighter.Transforms.Adaptors.Tests.csproj" />
<Project Path="tests/Paramore.Brighter.Testing.Tests/Paramore.Brighter.Testing.Tests.csproj" />
<Project Path="tests/Paramore.Test.Helpers/Paramore.Test.Helpers.csproj" />
</Folder>
<Folder Name="/tools/">
<Project Path="tools/Paramore.Brighter.Test.Generator/Paramore.Brighter.Test.Generator.csproj" />
</Folder>
<Project Path="src/Paramore.Brighter.Analyzer.Package/Paramore.Brighter.Analyzer.Package.csproj" />
<Project Path="src/Paramore.Brighter.Analyzer/Paramore.Brighter.Analyzer.csproj" />
<Project Path="src/Paramore.Brighter.Archive.Azure/Paramore.Brighter.Archive.Azure.csproj" />
<Project Path="src/Paramore.Brighter.Dapper/Paramore.Brighter.Dapper.csproj" />
<Project Path="src/Paramore.Brighter.DynamoDb.V4/Paramore.Brighter.DynamoDb.V4.csproj" />
Expand Down Expand Up @@ -259,6 +267,9 @@
<Project Path="src/Paramore.Brighter.Spanner/Paramore.Brighter.Spanner.csproj" />
<Project Path="src/Paramore.Brighter.Sqlite.EntityFrameworkCore/Paramore.Brighter.Sqlite.EntityFrameworkCore.csproj" />
<Project Path="src/Paramore.Brighter.Sqlite/Paramore.Brighter.Sqlite.csproj" />
<Project Path="src/Paramore.Brighter.Testing/Paramore.Brighter.Testing.csproj">
<Build Solution="Debug|*" Project="false" />
</Project>
<Project Path="src/Paramore.Brighter.Transformers.AWS.V4/Paramore.Brighter.Transformers.AWS.V4.csproj" />
<Project Path="src/Paramore.Brighter.Transformers.AWS/Paramore.Brighter.Transformers.AWS.csproj" />
<Project Path="src/Paramore.Brighter.Transformers.Azure/Paramore.Brighter.Transformers.Azure.csproj" />
Expand All @@ -267,5 +278,4 @@
<Project Path="src/Paramore.Brighter.Transformers.MassTransit/Paramore.Brighter.Transformers.MassTransit.csproj" />
<Project Path="src/Paramore.Brighter.Transformers.MongoGridFS/Paramore.Brighter.Transformers.MongoGridFS.csproj" />
<Project Path="src/Paramore.Brighter/Paramore.Brighter.csproj" />
<Project Path="src/Paramore.Brighter.Testing/Paramore.Brighter.Testing.csproj" />
</Solution>
4 changes: 4 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
<PackageVersion Include="MassTransit.AmazonSQS" Version="9.0.1" />
<PackageVersion Include="MassTransit.Extensions.DependencyInjection" Version="7.3.1" />
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.2" />

<PackageVersion Include="Microsoft.Bcl.TimeProvider" Version="10.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.1.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="10.0.2" />
Expand Down
59 changes: 59 additions & 0 deletions docs/adr/0037-provide-roslyn-analyzers-for-brighter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 37. Provide Roslyn Analyzers for Brighter

Date: 2026-01-25

## Status

Proposed

## Context

Using Brighter correctly often involves adhering to specific patterns and conventions that are not strictly enforced by the C# compiler alone. For example, developers must ensure that:
- Attributes like `[Wrap]` are used correctly to configure the pipeline.
- `Publication` objects are initialized with a valid `RequestType` that implements `IRequest` for proper mapping.
- Subscriptions are configured with valid parameters.

Currently, violations of these rules often result in runtime errors (e.g., `RuntimeBinderException`, `ArgumentException`) or silent failures. identifying these issues requires running the application, which delays the feedback loop and increases the risk of bugs reaching production.

We want to "shift left" on these checks, providing feedback to developers as they write code.

## Decision

We will introduce a new project, `Paramore.Brighter.Analyzer`, containing custom Roslyn analyzers.
This project will be distributed as a NuGet package that developers can include in their projects.

The analyzers will inspect code at compile-time and report diagnostics (warnings or errors) for known invalid usage patterns of Brighter components.

The initial implementation will include the following analyzers:

1. **PublicationRequestTypeAssignmentAnalyzer**:
- Ensures that when a `Publication` object is created, the `RequestType` property is explicitly assigned.
Comment thread
iancooper marked this conversation as resolved.
- Verifies that the assigned type implements `IRequest`.

2. **SubscriptionConstructorAnalyzer**:
- Validates that `Subscription` objects are instantiated with correct arguments and configuration.

3. **WrapAttributeAnalyzer**:
- Ensures that attributes used for wrapping (e.g., encryption, compression) are applied to the correct mapping method (`MapToMessage` or `MapToRequest`).
- Prevents incorrect pipeline configuration by verifying that attributes intended for requests are not applied to message mapping and vice versa.

## Technical Implementation Strategy

The logic for the analyzers will be implemented using the **Roslyn API** and the **Visitor Pattern**.

1. **Roslyn Syntax & Operation Trees**: We leverage Roslyn to inspect the code structure. We specifically focus on the **Operation Tree (IOperation)**, which provides a semantic view of the code (e.g., understanding that a line is an object creation regardless of syntax).
2. **Visitor Pattern**: To efficiently traverse the code, we implement a `Visitor` (e.g., `RequestTypeAssignmentVisitor`).
- The **Visitor** "walks" through specific nodes of the code tree (like `ObjectCreation`).
- We utilize **Double Dispatch** (via the `Accept` method) to ensure the correct `Visit` method is called for each node type. This avoids the need for explicit type casting and massive `if-else` or `switch` statements to determine node types, making the code cleaner and more maintainable.
- It maintains state as it visits nodes (e.g., "I am currently inside a Publication object creation").

## Consequences

### Positive
- **Immediate Feedback**: Developers receive feedback on incorrect usage within the IDE and at build time.
- **Reduced Runtime Errors**: Prevents a class of configuration and usage errors from occurring at runtime.
- **Better Discovery**: Helps new users learn Brighter's constraints and requirements through compiler messages rather than documentation lookup or trial-and-error.

### Negative
- **Maintenance**: The analyzer project must be maintained and updated as Brighter's API and patterns evolve.

17 changes: 17 additions & 0 deletions samples/Analyzer/AnalyzerSamples/AnalyzerSamples.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Paramore.Brighter.Analyzer\Paramore.Brighter.Analyzer.csproj" PrivateAssets="all" ReferenceOutputAssembly="true" OutputItemType="Analyzer" SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\src\Paramore.Brighter\Paramore.Brighter.csproj" />
</ItemGroup>

</Project>
48 changes: 48 additions & 0 deletions samples/Analyzer/AnalyzerSamples/PublicationSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Paramore.Brighter;

namespace AnalyzerSamples
{
public static class PublicationCreator
{
public static Publication GetPublicationNoRequestType()
{
return new PublicationSample() {
Subject="test"
};
}
public static Publication GetPublicationWrongRequestType()
{
return new PublicationSample()
{
Subject = "test",
RequestType = typeof(EventSample2)
};
}
public static Publication GetPublicationWithRequestType()
{
return new PublicationSample()
{
Subject = "test",
RequestType = typeof(EventSample)
};
}
public static AnyClass GetAnyOtherClass()
{
return new AnyClass();
}
}
public class PublicationSample : Publication
{
}
public class EventSample(Id id) : Event(id)
{
}
public class EventSample2
{
}

public class AnyClass
{

}
}
37 changes: 37 additions & 0 deletions samples/Analyzer/AnalyzerSamples/SubscriptionSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@


using Paramore.Brighter;

namespace AnalyzerSamples
{
internal class SubscriptionSample
{
}
public static class SubscriptionCreator
{
public static Subscription GetSubscription_MessagePumpSpecified()
{
return new SubscriptionTest("name", "name", "key", messagePumpType: MessagePumpType.Reactor);
}
public static Subscription GetSubscription_NoMessagePumpSpecified()
{
return new SubscriptionTest("name", "name", "key");
}
public static Subscription GetSubscriptionNested_NoMessagePumpSpecified()
{
return new SubscriptionTestNested("name", "name", "key");
}
public class SubscriptionTest : Subscription
{
public SubscriptionTest(SubscriptionName subscriptionName, ChannelName channelName, RoutingKey routingKey, Type? requestType = null, Func<Message, Type>? getRequestType = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, MessagePumpType messagePumpType = MessagePumpType.Unknown, IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, TimeSpan? emptyChannelDelay = null, TimeSpan? channelFailureDelay = null) : base(subscriptionName, channelName, routingKey, requestType, getRequestType, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay)
{
}
}
public class SubscriptionTestNested : SubscriptionTest
{
public SubscriptionTestNested(SubscriptionName subscriptionName, ChannelName channelName, RoutingKey routingKey, Type? requestType = null, Func<Message, Type>? getRequestType = null, int bufferSize = 1, int noOfPerformers = 1, TimeSpan? timeOut = null, int requeueCount = -1, TimeSpan? requeueDelay = null, int unacceptableMessageLimit = 0, MessagePumpType messagePumpType = MessagePumpType.Unknown, IAmAChannelFactory? channelFactory = null, OnMissingChannel makeChannels = OnMissingChannel.Create, TimeSpan? emptyChannelDelay = null, TimeSpan? channelFailureDelay = null) : base(subscriptionName, channelName, routingKey, requestType, getRequestType, bufferSize, noOfPerformers, timeOut, requeueCount, requeueDelay, unacceptableMessageLimit, messagePumpType, channelFactory, makeChannels, emptyChannelDelay, channelFailureDelay)
{
}
}
}
}
47 changes: 47 additions & 0 deletions samples/Analyzer/AnalyzerSamples/WrapWithMapperSample.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Paramore.Brighter;
using Paramore.Brighter.Transforms.Attributes;

namespace AnalyzerSamples
{
public class WrapWithMapperAsyncSample: IAmAMessageMapperAsync<SampleEvent>
{
public IRequestContext? Context { get; set ; }

[Compress(0)]
[ClaimCheck(0)]
public async Task<SampleEvent> MapToRequestAsync(Message message, CancellationToken cancellationToken = default)
{
await Task.Yield();
return new SampleEvent(message.Id);
}

[Decompress(0)]
public async Task<Message> MapToMessageAsync(SampleEvent request, Publication publication, CancellationToken cancellationToken = default)
{
await Task.Yield();
throw new NotImplementedException();
}
}
public class WrapWithMapperSample: IAmAMessageMapper<SampleEvent>
{
public IRequestContext? Context { get; set ; }

[Compress(0)]
[ClaimCheck(0)]
public SampleEvent MapToRequest(Message message)
{
return new SampleEvent(message.Id);
}

[Decompress(0)]
public Message MapToMessage(SampleEvent request, Publication publication)
{
throw new NotImplementedException();
}
}


public class SampleEvent(Id id) : Event(id)
{
}
}
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<Version>10.0.0</Version>

<!-- The .NET Version used in all projects -->
<BrighterNetStandardTargetFrameworks>netstandard2.0;</BrighterNetStandardTargetFrameworks>
<BrighterTargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</BrighterTargetFrameworks>
<BrighterFrameworkAndCoreTargetFrameworks>net462;net8.0;net9.0;net10.0</BrighterFrameworkAndCoreTargetFrameworks>
<BrighterCoreTargetFrameworks>net8.0;net9.0;net10.0</BrighterCoreTargetFrameworks>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(BrighterNetStandardTargetFrameworks)</TargetFrameworks>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup>

<PropertyGroup>
<Authors>AboubakrNasef</Authors>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<Description>Paramore.Brighter.Analyzer</Description>
<PackageReleaseNotes>Analyzer For usage of Brighter Command</PackageReleaseNotes>
<PackageTags>Paramore.Brighter,Paramore.Brighter.Analyzer, analyzers</PackageTags>
<DevelopmentDependency>true</DevelopmentDependency>
<NoPackageAnalysis>true</NoPackageAnalysis>

<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput</TargetsForTfmSpecificContentInPackage>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Paramore.Brighter.Analyzer\Paramore.Brighter.Analyzer.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="tools\*.ps1" CopyToOutputDirectory="PreserveNewest" Pack="true" PackagePath="" />
</ItemGroup>

<Target Name="_AddAnalyzersToOutput">
<ItemGroup>
<TfmSpecificPackageFile Include="$(OutputPath)\Paramore.Brighter.Analyzer.dll" PackagePath="analyzers/dotnet/cs" />
</ItemGroup>
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<GlobalAnalyzerConfigFiles Condition="'$(BrighterAnalysisMode)' == 'None'" Include="$(MSBuildThisFileDirectory)\..\configuration\none.editorconfig" />
<GlobalAnalyzerConfigFiles Condition="'$(BrighterAnalysisMode)' == 'Default'" Include="$(MSBuildThisFileDirectory)\..\configuration\default.editorconfig" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dotnet_diagnostic.BRT001.severity = warning
dotnet_diagnostic.BRT002.severity = warning
dotnet_diagnostic.BRT003.severity = warning
dotnet_diagnostic.BRT004.severity = warning
dotnet_diagnostic.BRT005.severity = warning
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dotnet_diagnostic.BRT001.severity = none
dotnet_diagnostic.BRT002.severity = none
dotnet_diagnostic.BRT003.severity = none
dotnet_diagnostic.BRT004.severity = none
dotnet_diagnostic.BRT005.severity = none
Loading
Loading