I have three c# projects, designed to use a custom attribute to generate code referencing a common class using a custom IIncremental Generator. I used and extended this guide from dotnet https://www.youtube.com/watch?v=Yf8t7GqA6zA
The Common Library Project has the following classes:
public class TestRelayCommand<T> : IRelayCommand<T>
{
#region "Constructors..."
public TestRelayCommand(ViewModelBase viewModel, Action<T> execute)
{
ViewModel = viewModel;
_command = new(execute);
}
public TestRelayCommand(ViewModelBase viewModel, Action<T> execute, Predicate<T> canExecute)
{
ViewModel = viewModel;
_command = new(execute, canExecute);
_command.CanExecuteChanged += _command_CanExecuteChanged;
}
#endregion
#region "Properties..."
public ViewModelBase ViewModel { get; }
private RelayCommand<T> _command;
public event EventHandler? CanExecuteChanged;
#endregion
#region "Methods..."
public bool CanExecute(T? parameter)
{
return _command.CanExecute(parameter);
}
public bool CanExecute(object? parameter)
{
return _command.CanExecute(parameter);
}
public void Execute(object? parameter)
{
//Action Started
_command.Execute(parameter);
//Action Ended
}
public void Execute(T? parameter)
{
//Action Started
_command.Execute(parameter);
//Action Ended
}
public void NotifyCanExecuteChanged()
{
_command.NotifyCanExecuteChanged();
}
#endregion
#region "Events..."
private void _command_CanExecuteChanged(object? sender, EventArgs e)
{
CanExecuteChanged?.Invoke(this, e);
}
#endregion
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public class TestRelayCommandAttribute : Attribute
{
public string? CanExecute { get; init; }
public bool AllowConcurrentExecutions { get; init; }
public bool FlowExceptionsToTaskScheduler { get; init; }
public bool IncludeCancelCommand { get; init; }
}
The Generator Project has the following Generator Class:
[Generator(LanguageNames.CSharp)]
public class TestRelayCommandGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorInitialize.txt", "Started");
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
//context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
var provider = context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => node is MethodDeclarationSyntax,
transform: static (ctx, _) => (MethodDeclarationSyntax)ctx.Node
).Where(m => m != null);// && m.AttributeLists.SelectMany(att => att.Attributes).Any(att => att.Name.ToString() == nameof(TestRelayCommand) || att.Name.ToString() == nameof(TestRelayCommandAttribute)));
var compilation = context.CompilationProvider.Combine(provider.Collect());
context.RegisterSourceOutput(compilation, Execute);
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorInitialized.txt", "Done Startup");
}
public void Execute(SourceProductionContext context, (Compilation Left, ImmutableArray<MethodDeclarationSyntax> Right) compilation)
{
File.WriteAllText(@"C:\Users\LMatheson\Desktop\GeneratorExecuting.txt", "Executing");
foreach (var method in compilation.Right)
{
var semanticModel = compilation.Left.GetSemanticModel(method.SyntaxTree);
var methodSymbol = semanticModel.GetDeclaredSymbol(method) as IMethodSymbol;
if (methodSymbol == null || !methodSymbol.GetAttributes().Any())
continue;
var attributeData = methodSymbol.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "TestRelayCommand");
var canExecuteArg = attributeData.NamedArguments.FirstOrDefault(arg => arg.Key == nameof(TestRelayCommandAttribute.CanExecute));
string canExecute = canExecuteArg.Value.Value != null ? canExecuteArg.Value.Value.ToString() : null;
if (!string.IsNullOrEmpty(canExecute)) canExecute = ", " + canExecute;
var classDeclaration = method.FirstAncestorOrSelf<ClassDeclarationSyntax>();
if (classDeclaration == null)
continue;
var namespaceDeclaration = classDeclaration.FirstAncestorOrSelf<NamespaceDeclarationSyntax>();
var namespaceName = namespaceDeclaration?.Name.ToString();
var className = classDeclaration.Identifier.Text;
var methodName = method.Identifier.Text;
var parameters = string.Join(", ", methodSymbol.Parameters.Select(p => $"{p.Type}"));
if (!string.IsNullOrEmpty(parameters)) parameters = "<" + parameters + ">";
//var returnType = methodSymbol.ReturnType.ToString();
//if (returnType != "void")
// returnType = "<" + returnType + ">";
//else
// returnType = "";
var source = $@"
space {namespaceName}
public partial class {className}
{{
private WPF.UI.Analyzer.{nameof(TestRelayCommand)}{parameters} _{methodName.Substring(0, 1).ToLower()}{methodName.Substring(1)}Command;
public WPF.UI.Analyzer.{nameof(TestRelayCommand)}{parameters} {methodName}Command => _{methodName.Substring(0, 1).ToLower()}{methodName.Substring(1)}Command ??= new(this, new({methodName}){canExecute});
}}
context.AddSource($"{className}_{methodName}_{nameof(TestRelayCommand)}.g.cs", SourceText.From(source, Encoding.UTF8));
File.WriteAllText("C:\\Users\\LMatheson\\Desktop\\Generator.txt", source);
}
}
}
The final project that uses the common attribute references the generator project as such:
<ProjectReference Include="..\WPF.UI.Analyzer\WPF.UI.Analyzer.csproj">
<OutputItemType>Analyzer</OutputItemType>
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
</ProjectReference>
And contains this class with the attribute:
public partial class UserRoleListViewModel
{
[TestRelayCommand]
public override async void SelectedViewModelChanging(object args)
{
Debug.Print("Testing");
}
}
The generator should be creating a partial class matching the method's parent, with a field and property of type TestRelayCommand<object> but the code is never generated regardless of whether I rebuild the solution or adjust the code by adding new methods with the same attribute or adjusting the existing attribute. What is missing that is required for a Source Generator to generate source in a referencing project.
This is my first question on StackOverflow so if more information is required please let me know.
I tried the following changes to no result:
Switched the Generator from ISourceGenerator to IIncrementalGenerator and rebuilt how it generates the Compilation. Attempted to add output files at different steps in the generator process to see if the files are ever created. Added the following properties to the .csproj file, the output folder was created with other referenced analyzers but none from my custom analyzer:
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>C:\Users\Lachlan\Desktop\Generated</CompilerGeneratedFilesOutputPath>
Created a test compilation using text matching my class file containing the attributed method, this process output the expected response in the test environment but didn't effect the analyzers generated output:
[Generator(LanguageNames.CSharp)]
public class TestRelayCommandGenerator : IIncrementalGenerator
{
private static Compilation CreateCompilation(string source)
=> CSharpCompilation.Create("compilation",
new[] { CSharpSyntaxTree.ParseText(source) },
new[] { MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) },
new CSharpCompilationOptions(OutputKind.ConsoleApplication));
public static void Test()
{
Compilation inputCompilation = CreateCompilation(@"
using WPF.UI.Analyzer;
namespace WPF.UI.UserManager.ViewModels
{
public partial class UserRoleListViewModel
{
[TestRelayCommand]
public override async void SelectedViewModelChanging(object args)
{
Debug.Print(""Testing"");
}
}
}
}");
TestRelayCommandGenerator generator = new();
// Create the driver that will control the generation, passing in our generator
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
// Run the generation pass
// (Note: the generator driver itself is immutable, and all calls return an updated version of the driver that you should use for subsequent calls)
driver = driver.RunGeneratorsAndUpdateCompilation(inputCompilation, out var outputCompilation, out var diagnostics);
// Or we can look at the results directly:
GeneratorDriverRunResult runResult = driver.GetRunResult();
}
...
The project requires being built using .Net Standard 2.0 as a framework. Any newer framework will make any generators unable to run.
Switching my existing .Net 8 project failed to get the analyzers to run so instead I created a new project and followed Andrew Lock's guide (found here: https://andrewlock.net/creating-a-source-generator-part-1-creating-an-incremental-source-generator/) to format the .csproj file, re-added the generators from my previous project and included this new project as a reference in the project that contains the attribute it looks for. After these steps it was able to analyze the project and output the expected code.