I want to talk about one of the most exciting new features in C# 9. A way to generate the source code you want and access it instantly in your editor. Stay tuned.
What is a source generator?
A Source Generator is a new kind of component that C# developers can write that lets you do two major things:
Retrieve a Compilation object that represents all user code that is being compiled. This object can be inspected and you can write code that works with the syntax and semantic models for the code being compiled, just like with analyzers today.
Generate C# source files that can be added to a Compilation object during the course of compilation. In other words, you can provide additional source code as input to a compilation while the code is being compiled. When combined, these two things are what make Source Generators so useful. You can inspect user code with all of the rich metadata that the compiler builds up during compilation, then emit C# code back into the same compilation that is based on the data you’ve analyzed! If you’re familiar with Roslyn Analyzers, you can think of Source Generators as analyzers that can emit C# source code.
Source generators run as a phase of compilation visualized below:
A Source Generator is a .NET Standard 2.0 assembly that is loaded by the compiler along with any analyzers. It is usable in environments where .NET Standard components can be loaded and run.
What are its prerequisites?
C# 9.0+ (SDK 5.0.100+)
Microsoft Visual Studio 16.8.0+ or JetBrains Rider 2020.3.0+
What are its limitations?
Source Generators do not allow you to rewrite user source code. You can only augment a compilation by adding C# source files to it.
Run un-ordered, each generator will see the same input compilation, with no access to files created by other source generators.
What is the scenario?
The need to mock static methods in order to add a unit test is a very common problem. It’s often the case that these static methods are in third-party libraries. There are many utility libraries that are completely made up of static methods. While this makes them very easy to use, it makes them really difficult to test.
The way to mock a static method is by creating a class that wraps the call, extracting an interface, and passing in the interface. Then from your unit tests you can create a mock of the interface and pass it in.
In the following, we describe this method and choose Dapper as a real-world example to show you how a wrapper class and interface helps us to test its static (extension) methods.
What is Dapper?
A simple object mapper for .Net.
1 2 3 4 5 6 7 8 9
publicclassStudent { publicint Id { get; set; } publicstring Name { get; set; } publicstring Family { get; set; } public DateTime BirthDate { get; set; } }
var student = connection.Query<Student>("SELECT * FROM STUDENT);
Dapper contains a lot of extension (static) methods so I’m going to look at how to mock its methods with the instruction above.
Solution structure
Make MockableStaticGenerator solution with these projects:
Name
Template
Target
MockableStaticGenerator
class library
netstandard2.0
DapperSample
class library
netstandard2.0
DapperSampleTest
xUnit test project
net5.0
MockableStaticGenerator
Open MockableStaticGenerator project and add the following configuration to csproj file.
using DapperSample; using Moq; using System.Data; using Xunit;
namespaceDapperSampleTest { publicclassStudentRepositoryTest { [Fact] publicvoidSTUDENT_REPOSITORY_TEST() { var mockConn = new Mock<IDbConnection>(); var sut = new StudentRepository(mockConn.Object); var stu = sut.GetStudents(); Assert.NotNull(stu); } } }
How is the test run and what happens next?
Run the test then you will get a error like below
1 2 3 4 5 6 7 8 9 10 11 12 13 14
DapperSampleTest.StudentRepositoryTest.STUDENT_REPOSITORY_TEST Source: StudentRepositoryTest.cs line 11 Duration: 92 ms
Message: System.NullReferenceException : Object reference not set to an instance of an object. Stack Trace: CommandDefinition.SetupCommand(IDbConnection cnn, Action`2 paramReader) line 113 SqlMapper.QueryImpl[T](IDbConnection cnn, CommandDefinition command, Type effectiveType)+MoveNext() line 1080 List`1.ctor(IEnumerable`1 collection) Enumerable.ToList[TSource](IEnumerable`1 source) SqlMapper.Query[T](IDbConnection cnn, String sql, Object param, IDbTransaction transaction, Boolean buffered, Nullable`1 commandTimeout, Nullable`1 commandType) line 725 StudentRepository.GetStudents() line 18 StudentRepositoryTest.STUDENT_REPOSITORY_TEST() line 15
You may have guessed why. Because mock object of IDbConnection has no Query method in his interface. This is the problem.
How to fix it?
The way to mock a static method is by creating a class that wraps the call, extracting an interface, and passing in the interface. Then from your unit tests you can create a mock of the interface and pass it in.
Do this step-by-step changes just like above instruction and add them to DapperSample project.
Extracting an interface.
1 2 3 4 5 6 7 8
// IDapperSqlMapper.cs using System.Collections.Generic; using System.Data;
using DapperSample; using Moq; using System.Data; using Xunit;
namespaceDapperSampleTest { publicclassStudentRepositoryTest { [Fact] publicvoidSTUDENT_REPOSITORY_TEST() { var mockConn = new Mock<IDbConnection>(); var mockDapper = new Mock<IDapperSqlMapper>(); var sut = new StudentRepository(mockConn.Object, mockDapper.Object); var stu = sut.GetStudents(); Assert.NotNull(stu); } } }
Run the test, you will see the green result!
What is the new problem?
Everything is good but very tough repetitive work especially when you are using external libraries like Dapper with a lot of extension (static) methods to use.
What if this repetitive method was already prepared for all methods?
How does a source generator help us solve this problem?
So far, we have learned about the problem and how to deal with it. But now we want to use a source generator to reduce the set of these repetitive tasks to zero.
What if I have both of the following possibilities?
Generate a wrapper class like above sample for Math class in background.
1 2 3 4 5 6 7 8 9
// Internal usage // My class with a lot of static (extension) methods.
[MockableStatic] publicclassMath { publicstaticintAdd(int a, int b) { return a + b; } publicstaticintSub(int a, int b) { return a - b; } }
Generate a wrapper class for Dapper.SqlMapper that exists in Dapper assembly in background.
1 2 3 4 5 6 7 8
// External usage // A referenced assembly with a type that contains a lot of static (extension) methods.
publicvoidOnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Count > 0) { Classes.Add(classDeclarationSyntax); } } } }
The purpose of this class is to identify the nodes we need to process the current source and generate new code. Here we store all the received classes in the Classes property.
Then, Create MockableGenerator class with below code.
namespaceMockableStaticGenerator { internalstaticclassConstants { internalconststring MockableStaticAttribute = @" namespace System { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] public sealed class MockableStaticAttribute : Attribute { public MockableStaticAttribute() { } public MockableStaticAttribute(Type type) { } } }"; } }
As I explained before, We need an attribute (MockableStaticAttribute) to annotate our classes. So, we will use above source code text in our source generator.
[MockableStatic] useful for internal usage and current class.
[MockableStatic(typeof(Dapper.SqlMapper))] useful for external usage and making a wrapper for an external type.
Let’s go to the Execute method, Add below code to it
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Text; using System.Text;
// 12 // ... // 13 if (isParameterlessCtor) { // 14 var methods = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Where(x => x.IsPublic() && x.IsStatic()).ToList(); if (methods.Count == 0) continue;
// 15 var className = clsSymbol.Name; // 16 var ns = string.IsNullOrEmpty(cls.GetNamespace()) ? "" : cls.GetNamespace() + "."; // 17 var baseList = string.IsNullOrEmpty(cls.BaseList?.ToFullString()) ? ":" : cls.BaseList?.ToFullString().Trim() + ","; // 18 assemblyName = clsSymbol.ContainingAssembly.Identity.Name; // 19 var wrapperClassName = !className.Contains('<') ? className + "Wrapper" : className.Replace("<", "Wrapper<"); // 20 var classTypeParameters = cls.GetTypeParameters() ?? ""; // 21 var constraintClauses = cls.GetConstraintClauses() ?? ""; // 22 sbInterface.AppendLine($"\tpublic partial interface I{wrapperClassName}{classTypeParameters}{constraintClauses} {{"); // 23 sbClass.AppendLine($"\tpublic partial class {wrapperClassName}{classTypeParameters}{baseList} I{wrapperClassName}{classTypeParameters}{constraintClauses} {{"); // 24 foreach (MethodDeclarationSyntax method in methods) { // 25 var text = method.GetSignatureText();
// 26 if (!sbInterface.ToString().Contains(text)) sbInterface.AppendLine($"\t\t{text};");
// 27 if (!sbClass.ToString().Contains(text)) { // 28 var returnKeyword = method.ReturnsVoid() ? "" : "return ";
// 29 var obsoleteAttrText = ""; var isObsolete = method.TryGetObsoleteAttribute(out obsoleteAttrText); if (isObsolete) sbClass.AppendLine($"\t\t{obsoleteAttrText}");
(17) If it has inherited from a class or implemented interfaces, its information is available here.
(18) Assembly name.
(19) We append Wrapper to end of the class name. so We will have something like these samples:
SqlMapper => ISqlMapperWrapper and SqlMapperWrapper SqlMapper<T> => ISqlMapperWrapper and SqlMapperWrapper
(20) Your class may have type parameters (generic).
Add below method to SourceGeneratorExtensions class.
1 2 3 4 5
internalstaticstringGetTypeParameters(this ClassDeclarationSyntax classDeclarationSyntax) { var result = classDeclarationSyntax.TypeParameterList?.ToFullString().Trim(); returnstring.IsNullOrEmpty(result) ? null : result; }
Now we can check it there.
(21) If your class is generic, Has it any constraint clauses? With the following extension method we can find out.
1 2 3 4 5 6
// SourceGeneratorExtensions.cs internalstaticstringGetConstraintClauses(this ClassDeclarationSyntax classDeclarationSyntax) { var result = classDeclarationSyntax.ConstraintClauses.ToFullString().Trim(); returnstring.IsNullOrEmpty(result) ? null : result; }
(22) (23) With current information we are able to create our interface and class for wrapping the main class static methods.
(24) Let’s start examining the methods.
(25) We should add our methods to interface so we need to know about its signature.
So add the following extension method to your SourceGeneratorExtensions class.
1 2 3 4 5 6 7 8 9 10
internalstaticstringGetSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax) { var name = methodDeclarationSyntax.Identifier.ValueText; var parameters = methodDeclarationSyntax.ParameterList?.ToFullString().Trim(); var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim(); var constraintClauses = methodDeclarationSyntax.ConstraintClauses.ToFullString().Replace(System.Environment.NewLine, "").Trim(); var returnType = methodDeclarationSyntax.ReturnType.ToFullString().Trim();
internalstaticstringGetCallableSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax) { var name = methodDeclarationSyntax.Identifier.ValueText; var parameters = methodDeclarationSyntax.ParameterList.GetParametersText(); var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim();
internalstaticstringGetParametersText(this ParameterListSyntax parameterListSyntax) { if (parameterListSyntax == null || parameterListSyntax.Parameters.Count == 0) return"()"; var result = new List<string>(); foreach (var item in parameterListSyntax.Parameters) { var variableName = item.Identifier; var modifiers = item.Modifiers.Select(x => x.ValueText).ToList(); var modifiersText = modifiers.Count == 0 ? "" : modifiers.Aggregate((a, b) => a + " " + b); result.Add($"{modifiersText}{variableName}"); } return result.Count == 0 ? "()" : $"({result.Aggregate((a, b) => a + ", " + b).Trim()})"; }
The information it returns includes:
Method name.
Type parameter(s), if it is generic.
Method parameter(s) (with name and without type).
Now, We can add the whole wrapper method.
(31) At the end, we should complete our interface and class with final }.
Part one finished. What happens if we want to generate a wrapper for a type?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 31 // ... // 32 else { // 33 var ctor = ((INamedTypeSymbol)attr?.ConstructorArguments[0].Value); // 34 var assemblySymbol = ctor.ContainingAssembly.GlobalNamespace; // 35 assemblyName = ctor.ContainingAssembly.Identity.Name; // 36 var visitor = new MethodSymbolVisitor(ctor.ToDisplayString()); visitor.Visit(assemblySymbol); // 62 sbInterface.AppendLine(_interfaces.Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}"); sbClass.AppendLine(_classes.Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}"); }
(32) This part is for constructor with a parameter.
(33) Getting the value of the constructor argument.
(34) Getting the assembly information of the type introduced.
(35) Assembly name.
(36) We need a visitor class to know about static methods of the type introduced in the external assembly. We sends the type’s name to the constructor of our visitor because we want to generate wrapper for that type.
To write MethodSymbolVisitor add below code to MockableGenerator class as a nested class.
(54) Just like the part one, We should check the method has ObsoleteAttribute or not.
(55) It’s time to build the interface and the class.
(56) If the generated source code does not include it, add the interface source.
(57) If the generated source code does not include it, add the class source.
(58) Add the method signature to the interface if does not exist.
(59) Add the method signature to the class if does not exist.
(60) If the method have an ObsoleteAttribute Add it on top of the generated wrapper method too.
(61) With the whole information we have, we are able to complete the wrapper method.
(62) Finally, we should complete our interface and class with final }.
At the end of the foreach we have
1 2 3 4 5 6 7 8 9 10
foreach (var cls in receiver.Classes) { // ... // 63 var interfaceWrapper = sbInterface.ToString(); var classWrapper = sbClass.ToString(); // 64 sources.AppendLine(interfaceWrapper); sources.AppendLine(classWrapper); }
(63) Convert our interface and class string builder to string.
(64) And append them to sources variable which we created at the beginning of the source code.
Finally, Our Execute method ends with
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// 65 var defaultUsings = new StringBuilder(); defaultUsings.AppendLine("using System;"); defaultUsings.AppendLine("using System.Collections.Generic;"); defaultUsings.AppendLine("using System.Linq;"); defaultUsings.AppendLine("using System.Text;"); defaultUsings.AppendLine("using System.Threading.Tasks;"); var usings = defaultUsings.ToString(); // 66 var src = sources.ToString(); var @namespace = new StringBuilder(); @namespace.AppendLine(usings); @namespace.AppendLine($"namespace {assemblyName}.MockableGenerated {{"); @namespace.AppendLine(src); @namespace.Append("}"); var result = @namespace.ToString(); // 67 context.AddSource($"{assemblyName}MockableGenerated", SourceText.From(result,Encoding.UTF8));
(65) We are able to add some default using statements.
(66) To use the end result, we need a specific namespace to aggregate all the generated code. As you can see, the final code is accessible through the following namespace.
Assembly name + .MockableGenerated => Dapper.MockableGenerated
(67) Finally, we add all the generated source code to the current source so that the compiler knows about it.
Now, It’s time to add MockableStaticGenerator project to DapperSample as a reference project but you should update DapperSample.csproj file as following.
This is not a “normal” ProjectReference. It needs the additional ‘OutputItemType’ and ‘ReferenceOutputAssmbly’ attributes to act as an analyzor.
So you should be able to access to generated namespace. No need to use DapperSqlMapper and IDapperSqlMapper any more just update your StudentRepository as following
using DapperSample; using Moq; using System.Data; using Xunit; using Dapper.MockableGenerated; // HERE
namespaceDapperSampleTest { publicclassStudentRepositoryTest { [Fact] publicvoidSTUDENT_REPOSITORY_TEST() { var mockConn = new Mock<IDbConnection>(); var mockDapper = new Mock<ISqlMapperWrapper>(); // HERE var sut = new StudentRepository(mockConn.Object, mockDapper.Object /*HERE*/); var stu = sut.GetStudents(); Assert.NotNull(stu); } } }
using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Text.RegularExpressions;
namespaceMicrosoft.CodeAnalysis { internalstaticclassSourceGeneratorExtensions { internalstaticstringToStringValue(this RefKind refKind) { if (refKind == RefKind.RefReadOnly) return"ref readonly"; switch (refKind) { case RefKind.Ref: return"ref"; case RefKind.Out: return"out"; case RefKind.In: return"in"; default: return""; } }
internalstaticstringGetSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax) { var name = methodDeclarationSyntax.Identifier.ValueText; var parameters = methodDeclarationSyntax.ParameterList?.ToFullString().Trim(); var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim(); var constraintClauses = methodDeclarationSyntax.ConstraintClauses.ToFullString().Replace(System.Environment.NewLine, "").Trim(); var returnType = methodDeclarationSyntax.ReturnType.ToFullString().Trim();
internalstaticstringGetParametersText(this ParameterListSyntax parameterListSyntax) { if (parameterListSyntax == null || parameterListSyntax.Parameters.Count == 0) return"()"; var result = new List<string>(); foreach (var item in parameterListSyntax.Parameters) { var variableName = item.Identifier; var modifiers = item.Modifiers.Select(x => x.ValueText).ToList(); var modifiersText = modifiers.Count == 0 ? "" : modifiers.Aggregate((a, b) => a + " " + b); result.Add($"{modifiersText}{variableName}"); } return result.Count == 0 ? "()" : $"({result.Aggregate((a, b) => a + ", " + b).Trim()})"; }
internalstaticstringGetCallableSignatureText(this MethodDeclarationSyntax methodDeclarationSyntax) { var name = methodDeclarationSyntax.Identifier.ValueText; var parameters = methodDeclarationSyntax.ParameterList.GetParametersText(); var typeParameters = methodDeclarationSyntax.TypeParameterList?.ToFullString().Trim();
internalstaticstringGetConstraintClauses(this INamedTypeSymbol namedTypeSymbol) { if (namedTypeSymbol.TypeParameters.Length == 0) return""; var result = new List<string>(); foreach (var item in namedTypeSymbol.TypeParameters) { var constraintType = item.ToDisplayString(); var constraintItems = item.ConstraintTypes.Select(x => x.ToDisplayString()).Aggregate((a, b) => $"{a}, {b}").Trim(); result.Add($"where {constraintType} : {constraintItems}".Trim()); }
return result.Aggregate((a, b) => $"{a}{b}").Trim(); }
internalstaticstringGetBaseList(this INamedTypeSymbol namedTypeSymbol, paramsstring[] others) { var result = new List<string>(); if (namedTypeSymbol.BaseType != null && !string.Equals(namedTypeSymbol.BaseType.Name, "object", StringComparison.InvariantCultureIgnoreCase)) result.Add(namedTypeSymbol.BaseType.Name); if (namedTypeSymbol.AllInterfaces.Length != 0) { foreach (var item in namedTypeSymbol.AllInterfaces) { result.Add(item.Name); } } if (others != null && others.Length != 0) { foreach (var item in others) { if (!string.IsNullOrEmpty(item)) result.Add(item); } } return result.Count == 0 ? "" : $": {result.Aggregate((a, b) => $"{a}, {b}")}".Trim(); }
// MockableGenerator.cs using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; using System.Linq; using System.Text;
if (context.SyntaxReceiver isnot SyntaxReceiver receiver) return;
CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(Constants.MockableStaticAttribute, Encoding.UTF8), options)); INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName($"System.{nameof(Constants.MockableStaticAttribute)}");
var sources = new StringBuilder(); var assemblyName = ""; foreach (var cls in receiver.Classes) { SemanticModel model = compilation.GetSemanticModel(cls.SyntaxTree); var clsSymbol = model.GetDeclaredSymbol(cls);
var attr = clsSymbol.GetAttributes().FirstOrDefault(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
if (attr == null) continue; var isParameterlessCtor = attr?.ConstructorArguments.Length == 0;
var sbInterface = new StringBuilder(); var sbClass = new StringBuilder();
if (isParameterlessCtor) { var methods = cls.DescendantNodes().OfType<MethodDeclarationSyntax>().Where(x => x.IsPublic() && x.IsStatic()).ToList(); if (methods.Count == 0) continue;
var className = clsSymbol.Name; var ns = string.IsNullOrEmpty(cls.GetNamespace()) ? "" : cls.GetNamespace() + "."; var baseList = string.IsNullOrEmpty(cls.BaseList?.ToFullString()) ? ":" : cls.BaseList?.ToFullString().Trim() + ","; assemblyName = clsSymbol.ContainingAssembly.Identity.Name; var wrapperClassName = !className.Contains('<') ? className + "Wrapper" : className.Replace("<", "Wrapper<"); var classTypeParameters = cls.GetTypeParameters() ?? ""; var constraintClauses = cls.GetConstraintClauses() ?? ""; sbInterface.AppendLine($"\tpublic partial interface I{wrapperClassName}{classTypeParameters}{constraintClauses} {{"); sbClass.AppendLine($"\tpublic partial class {wrapperClassName}{classTypeParameters}{baseList} I{wrapperClassName}{classTypeParameters}{constraintClauses} {{");
foreach (MethodDeclarationSyntax method in methods) { var text = method.GetSignatureText();
if (!sbInterface.ToString().Contains(text)) sbInterface.AppendLine($"\t\t{text};");
if (!sbClass.ToString().Contains(text)) { var returnKeyword = method.ReturnsVoid() ? "" : "return "; var obsoleteAttrText = ""; var isObsolete = method.TryGetObsoleteAttribute(out obsoleteAttrText); if (isObsolete) sbClass.AppendLine($"\t\t{obsoleteAttrText}");
sbInterface.AppendLine($"\t}}"); sbClass.AppendLine($"\t}}"); } else { var ctor = ((INamedTypeSymbol)attr?.ConstructorArguments[0].Value); var assemblySymbol = ctor.ContainingAssembly.GlobalNamespace; assemblyName = ctor.ContainingAssembly.Identity.Name; var visitor = new MethodSymbolVisitor(ctor.ToDisplayString()); visitor.Visit(assemblySymbol); sbInterface.AppendLine(_interfaces.Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}"); sbClass.AppendLine(_classes.Aggregate((a, b) => a + Environment.NewLine + b) + Environment.NewLine + "\t}"); }
var interfaceWrapper = sbInterface.ToString(); var classWrapper = sbClass.ToString();
var defaultUsings = new StringBuilder(); defaultUsings.AppendLine("using System;"); defaultUsings.AppendLine("using System.Collections.Generic;"); defaultUsings.AppendLine("using System.Linq;"); defaultUsings.AppendLine("using System.Text;"); defaultUsings.AppendLine("using System.Threading.Tasks;"); var usings = defaultUsings.ToString();
var src = sources.ToString(); var @namespace = new StringBuilder(); @namespace.AppendLine(usings); @namespace.AppendLine($"namespace {assemblyName}.MockableGenerated {{"); @namespace.AppendLine(src); @namespace.Append("}"); var result = @namespace.ToString();
Visual Studio does not detect my source generators, What should I do?
Unfortunately, the current version of Visual Studio (16.8.2) has a lot of problems while you are using code generators, but you can try the following steps.
Make sure you follow the steps above correctly.
Use dotnet clean, Maybe you need to delete all bin and obj folders.
After that, use dotnet build to make sure your source code has no error and the problem is caused by Visual Studio.
Reset your Visual Studio.
How to debug it?
To start debug you can add System.Diagnostics.Debugger.Launch(); as following:
1 2 3 4 5
publicvoidInitialize(GeneratorInitializationContext context) { System.Diagnostics.Debugger.Launch(); // HERE context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); }
Run the debugger and you will see it stops at System.Diagnostics.Debugger.Launch() line.
If you have any problem for debugging, like what I had before
Make sure you are running Visual Studio as administrator.
Open Visual Studio as Administrator
If you want to start Visual Studio as Administrator you can do the following:
Right-click on your VS task bar shortcut
Right-click on your VS product and select Properties
From Properties window select Advanced…
From Advanced Properties check on Run as Administrator option
select Ok in Advanced Properties window, Apply and then Ok on VS 2019 Properties.
Open every Visual Studio Solution (.sln) as Administrator
Although not the best idea if you open third-party VS solutions, it may come in handy if you need to open the same solutions as Administrator again and again. To do so, right-click on devenv.exe and select Troubleshoot Compatibility.
You may then proceed to the following steps:
On the Program Compatibility Troubleshooter window, click on Troubleshoot Program
Check The program requires additional permissions and click Next
On the next window, click on Test the program… and VS will open as administrator
Click next and then click on Yes, save these settings for this program
Following the above, whenever you open a solution (.sln) it will always open as Adminsitrator. If you want to disable this function, you will need to follow again the steps above without checking though The Program requires additional permissions.
How to work with files?
If you are using a specific physical file with source generators you should use AdditionalFiles in your csproj.