Vogen
Vogen is a C# .NET Source Generator and a set of Roslyn analyzers.
It wraps your primitives (integers, decimals, etc.) in a 'Value Object' that represents
domain concepts like CustomerId, AccountBalance, etc.
It adds new compilation errors to help stop the creation of invalid Value Objects.
///
///
///
///
///
///
public static partial class CSharpCodeFixVerifier
where TAnalyzer : DiagnosticAnalyzer, new()
where TCodeFix : CodeFixProvider, new()
{
///
public static DiagnosticResult Diagnostic()
=> CSharpCodeFixVerifier.Diagnostic();
///
public static DiagnosticResult Diagnostic(string diagnosticId)
=> CSharpCodeFixVerifier.Diagnostic(diagnosticId);
///
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> CSharpCodeFixVerifier.Diagnostic(descriptor);
///
public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
{
var test = new Test
{
TestCode = source,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
///
public static async Task VerifyCodeFixAsync(string source, string fixedSource)
=> await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
=> await VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
{
var test = new Test
{
TestCode = source,
FixedCode = fixedSource,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
}
///
{
///
public static DiagnosticResult Diagnostic()
=> CSharpCodeFixVerifier.Diagnostic();
///
public static DiagnosticResult Diagnostic(string diagnosticId)
=> CSharpCodeFixVerifier.Diagnostic(diagnosticId);
///
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> CSharpCodeFixVerifier.Diagnostic(descriptor);
///
public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
{
var test = new Test
{
TestCode = source,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
///
public static async Task VerifyCodeFixAsync(string source, string fixedSource)
=> await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
=> await VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
{
var test = new Test
{
TestCode = source,
FixedCode = fixedSource,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
}
///
{
///
public static DiagnosticResult Diagnostic()
=> CSharpCodeFixVerifier.Diagnostic();
///
public static DiagnosticResult Diagnostic(string diagnosticId)
=> CSharpCodeFixVerifier.Diagnostic(diagnosticId);
///
public static DiagnosticResult Diagnostic(DiagnosticDescriptor descriptor)
=> CSharpCodeFixVerifier.Diagnostic(descriptor);
///
public static async Task VerifyAnalyzerAsync(string source, params DiagnosticResult[] expected)
{
var test = new Test
{
TestCode = source,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
///
public static async Task VerifyCodeFixAsync(string source, string fixedSource)
=> await VerifyCodeFixAsync(source, DiagnosticResult.EmptyDiagnosticResults, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult expected, string fixedSource)
=> await VerifyCodeFixAsync(source, new[] { expected }, fixedSource);
///
public static async Task VerifyCodeFixAsync(string source, DiagnosticResult[] expected, string fixedSource)
{
var test = new Test
{
TestCode = source,
FixedCode = fixedSource,
};
test.ExpectedDiagnostics.AddRange(expected);
await test.RunAsync(CancellationToken.None);
}
}
// ReSharper disable once ArrangeObjectCreationWhenTypeEvident - current bug in Roslyn analyzer means it won't pick this up when implied
// ReSharper disable once UseCollectionExpression
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class DoNotThrowFromUserCodeAnalyzer : DiagnosticAnalyzer
{
// ReSharper disable once ArrangeObjectCreationWhenTypeEvident - current bug in Roslyn analyzer means it won't pick this up when implied
private static readonly DiagnosticDescriptor _rule = new DiagnosticDescriptor(
RuleIdentifiers.DoNotThrowFromUserCode,
"Value objects should not throw exceptions",
"Type '{0}' throws an exception which can cause surprising side effects, for instance, in implicit conversions",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Value objects can contain user code; methods such as NormalizeInput and Validate. These should not throw exceptions, because that can cause surprising side effects such as when doing implicit conversions (which should not throw). The only place to throw, and which is handle by the generated code, is an exception related to validation.");
// ReSharper disable once UseCollectionExpression
public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ThrowStatement, SyntaxKind.ThrowExpression);
}
private static void Analyze(SyntaxNodeAnalysisContext ctx)
{
var throwStatement = ctx.Node;
var containingType = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingType is null)
{
return;
}
if (!VoFilter.IsTarget(containingType))
{
return;
}
var containingMethod = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingMethod?.Identifier.Text is not ("Validate" or "NormalizeInput"))
{
return;
}
var diagnostic = DiagnosticsCatalogue.BuildDiagnostic(_rule, containingType.Identifier.Text, throwStatement.GetLocation());
ctx.ReportDiagnostic(diagnostic);
}
}
{
// ReSharper disable once ArrangeObjectCreationWhenTypeEvident - current bug in Roslyn analyzer means it won't pick this up when implied
private static readonly DiagnosticDescriptor _rule = new DiagnosticDescriptor(
RuleIdentifiers.DoNotThrowFromUserCode,
"Value objects should not throw exceptions",
"Type '{0}' throws an exception which can cause surprising side effects, for instance, in implicit conversions",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Value objects can contain user code; methods such as NormalizeInput and Validate. These should not throw exceptions, because that can cause surprising side effects such as when doing implicit conversions (which should not throw). The only place to throw, and which is handle by the generated code, is an exception related to validation.");
// ReSharper disable once UseCollectionExpression
public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ThrowStatement, SyntaxKind.ThrowExpression);
}
private static void Analyze(SyntaxNodeAnalysisContext ctx)
{
var throwStatement = ctx.Node;
var containingType = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingType is null)
{
return;
}
if (!VoFilter.IsTarget(containingType))
{
return;
}
var containingMethod = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingMethod?.Identifier.Text is not ("Validate" or "NormalizeInput"))
{
return;
}
var diagnostic = DiagnosticsCatalogue.BuildDiagnostic(_rule, containingType.Identifier.Text, throwStatement.GetLocation());
ctx.ReportDiagnostic(diagnostic);
}
}
{
// ReSharper disable once ArrangeObjectCreationWhenTypeEvident - current bug in Roslyn analyzer means it won't pick this up when implied
private static readonly DiagnosticDescriptor _rule = new DiagnosticDescriptor(
RuleIdentifiers.DoNotThrowFromUserCode,
"Value objects should not throw exceptions",
"Type '{0}' throws an exception which can cause surprising side effects, for instance, in implicit conversions",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "Value objects can contain user code; methods such as NormalizeInput and Validate. These should not throw exceptions, because that can cause surprising side effects such as when doing implicit conversions (which should not throw). The only place to throw, and which is handle by the generated code, is an exception related to validation.");
// ReSharper disable once UseCollectionExpression
public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(_rule);
public override void Initialize(AnalysisContext context)
{
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
context.EnableConcurrentExecution();
context.RegisterSyntaxNodeAction(Analyze, SyntaxKind.ThrowStatement, SyntaxKind.ThrowExpression);
}
private static void Analyze(SyntaxNodeAnalysisContext ctx)
{
var throwStatement = ctx.Node;
var containingType = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingType is null)
{
return;
}
if (!VoFilter.IsTarget(containingType))
{
return;
}
var containingMethod = throwStatement.AncestorsAndSelf().OfType().FirstOrDefault();
if (containingMethod?.Identifier.Text is not ("Validate" or "NormalizeInput"))
{
return;
}
var diagnostic = DiagnosticsCatalogue.BuildDiagnostic(_rule, containingType.Identifier.Text, throwStatement.GetLocation());
ctx.ReportDiagnostic(diagnostic);
}
}
{
[Fact]
public async Task Does_not_raise_for_empty_code() => await Run("", []);
[Fact]
public async Task Does_not_raise_when_no_methods_throw()
{
var source = """
using System;
using Vogen;
namespace Whatever;
[ValueObject]
public partial struct CustomerId
{
public static int NormalizeInput(int value) => value;
}
""";
await Run(source, []);
}
[Fact]
public async Task Raises_for_a_method_that_throws()
{
var source = """
using System;
using Vogen;
namespace Whatever;
[ValueObject]
public partial struct CustomerId
{
public static int NormalizeInput(int value)
{
{|#0:throw new Exception("Oh no!");|}
}
}
""";
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0));
}
[Fact]
public async Task Raises_for_a_method_that_throws_twice()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) {|#0:throw new Exception(“Oh no!”);|} if(2 == 2) {|#1:throw new Exception(“Oh no!”);|} } } “”“;
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0, 1));
}
[Fact]
public async Task Raises_for_methods_named_NormalizeInput_and_Validate()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) {|#0:throw new Exception(“Oh no!”);|} if(2 == 2) {|#1:throw new Exception(“Oh no!”);|} }
public static Validation Validate(int value)
{
if(1 == 1) {|#2:throw new Exception("Oh no!");|}
if(2 == 2) {|#3:throw new Exception("Oh no!");|}
}
} “”“;
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0, 1, 2, 3));
}
[Fact]
public async Task Ignores_methods_that_are_not_NormalizeInput_or_Validate()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) {|#0:throw new Exception(“Oh no!”);|} if(2 == 2) {|#1:throw new Exception(“Oh no!”);|} }
public static void AnotherMethods(int value)
{
if(1 == 1) throw new Exception("Oh no!");
if(2 == 2) throw new Exception("Oh no!");
}
} “”“;
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0, 1));
}
[Fact]
public async Task Ignores_methods_that_are_not_NormalizeInput_or_Validate2()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) {|#0:throw new Exception(“Oh no!”);|} if(2 == 2) {|#1:throw new Exception(“Oh no!”);|} }
public static void AnotherMethod(int value)
{
if(1 == 1) throw new Exception("Oh no!");
if(2 == 2) throw new Exception("Oh no!");
}
public static Validation Validate(int value)
{
if(1 == 1) {|#2:throw new Exception("Oh no!");|}
if(2 == 2) Throw();
return Validation.Ok;
void Throw() => {|#3:throw new Exception("Oh no!")|};
}
} “”“;
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0, 1, 2, 3));
}
[Fact]
public async Task Raises_for_inner_method()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) Throw(); return 1;
void Throw()
{
{|#0:throw new Exception("Oh no!");|}
}
}
} “”“;
await Run(
source,
WithDiagnostics("VOG032", DiagnosticSeverity.Warning, 0));
}
[Fact]
public async Task Raises_for_inner_method_expression()
{
var source = """
using System; using Vogen;
namespace Whatever;
[ValueObject] public partial struct CustomerId { public static int NormalizeInput(int value) { if(1 == 1) Throw(); return 1;
void Throw() => {|#0:throw new Exception("Oh no!")|};
}
} “”“;
await Run(
source,
WithDiagnostics("VOG03
Top-Level Directory Explanations
samples/ - This directory contains example projects that demonstrate the usage of the Vogen library. Each subdirectory represents a different example, and contains the necessary files and configurations for that example.
src/ - This directory contains the source code for the project. It includes the Vogen library itself, as well as any shared types and code fixers.
src/obj/ - This directory contains object files generated during the compilation process.
src/Vogen/ - This subdirectory contains the core Vogen library code. It includes subdirectories for diagnostics, extensions, generators, properties, rules, suppressors, templates, and binaries and object files.
tests/ - This directory contains unit tests and benchmarks for the project. It includes subdirectories for analyzer tests, consumer tests, snapshot tests, and Vogen benchmarks.
tests/AnalyzerTests/ - This subdirectory contains unit tests for the analyzer component of the Vogen library.
tests/ConsumerTests/ - This subdirectory contains unit tests for the consumer-side components of the Vogen library.
tests/SnapshotTests/ - This subdirectory contains snapshot tests, which test the output of the code generation and serialization components of the Vogen library.
tests/Vogen.Benchmarks/ - This subdirectory contains benchmarks for the performance of the Vogen library.