Eliminate the hassle of writing constructors for dependency injection with C # Source Generators

In April 2020, the developers of the .NET 5 platform   announced a  new way to generate source code in the C # programming language - using an interface implementation  ISourceGenerator



. This method allows developers to analyze custom code and  create new source files  at compile time. At the same time, the API of the new source code generators is similar to the API  of the Roslyn analyzers . You can generate code both using the  Roslyn Compiler API and by concatenating ordinary strings.





In this article, we will look at the HarabaSourceGenerators.Generators library and how it is implemented.





HarabaSourceGenerators.Generators

We are all used to injecting a bunch of dependencies into a class and initializing them in the constructor. The output is usually something like this





public partial class HomeController : Controller
{
     private readonly TestService _testService;
        
     private readonly WorkService _workService;
        
     private readonly ExcelService _excelService;
        
     private readonly MrNService _mrNService;
        
     private readonly DotNetTalksService _dotNetTalksService;
       
     private readonly ILogger<HomeController> _logger;

     public HomeController(
         TestService testService,
         WorkService workService,
         ExcelService excelService,
         MrNService mrNService,
         DotNetTalksService dotNetTalksService,
         ILogger<HomeController> logger)
     {
         _testService = testService;
         _workService = workService;
         _excelService = excelService;
         _mrNService = mrNService;
         _dotNetTalksService = dotNetTalksService;
         _logger = logger;
     }
}
      
      



It's time to end it!





I present to your attention a new, convenient and elegant way:





public partial class HomeController : Controller
{
    [Inject]
    private readonly TestService _testService;
        
    [Inject]
    private readonly WorkService _workService;
        
    [Inject]
    private readonly ExcelService _excelService;
        
    [Inject]
    private readonly MrNService _mrNService;
        
    [Inject]
    private readonly DotNetTalksService _dotNetTalksService;
        
    [Inject]
    private readonly ILogger<HomeController> _logger;
 }
      
      



But what if you are too lazy to specify the Inject attribute for each dependency?





Not a problem, you can specify the Inject attribute for the entire class. In this case, all private fields with the readonly modifier will be taken:





[Inject]
public partial class HomeController : Controller
{
    private readonly TestService _testService;
        
    private readonly WorkService _workService;
        
    private readonly ExcelService _excelService;
        
    private readonly MrNService _mrNService;
        
    private readonly DotNetTalksService _dotNetTalksService;
        
    private readonly ILogger<HomeController> _logger;
}
      
      



Excellent. But what if there is a field that is not needed for injection?





We specify the InjectIgnore attribute for such a field:





[Inject]
public partial class HomeController : Controller
{
    [InjectIgnore]
    private readonly TestService _testService;
        
    private readonly WorkService _workService;
        
    private readonly ExcelService _excelService;
        
    private readonly MrNService _mrNService;
        
    private readonly DotNetTalksService _dotNetTalksService;
        
    private readonly ILogger<HomeController> _logger;
}
      
      



Okay, so what if I want to sequence the dependencies?





Guess what? That's right, not a problem. There are two ways:





1) Arrange the fields in the desired sequence in the class itself.

2) Pass the serial number of the dependency to the Inject attribute





public partial class HomeController : Controller
{
    [Inject(2)]
    private readonly TestService _testService;

    [Inject(1)]
    private readonly WorkService _workService;

    [Inject(3)]
    private readonly ExcelService _excelService;

    [Inject(4)]
    private readonly MrNService _mrNService;

    [Inject(5)]
    private readonly DotNetTalksService _dotNetTalksService;

    [Inject(6)]
    private readonly ILogger<HomeController> _logger;
}
      
      



As you can see, the sequence has been successfully saved.





InjectSourceGenerator, ISourceGenerator.

. , , Inject. - partial , .

"{className}.Constructor.cs"





public void Execute(GeneratorExecutionContext context)
{
	var compilation = context.Compilation;
	var attributeName = nameof(InjectAttribute).Replace("Attribute", string.Empty);
	foreach (var syntaxTree in compilation.SyntaxTrees)
	{
		var semanticModel = compilation.GetSemanticModel(syntaxTree);
		var targetTypes = syntaxTree.GetRoot().DescendantNodes()
			.OfType<ClassDeclarationSyntax>()
			.Where(x => x.ContainsClassAttribute(attributeName) || x.ContainsFieldAttribute(attributeName))
			.Select(x => semanticModel.GetDeclaredSymbol(x))
			.OfType<ITypeSymbol>();

		foreach (var targetType in targetTypes)
		{
			string source = GenerateInjects(targetType);
			context.AddSource($"{targetType.Name}.Constructor.cs", SourceText.From(source, Encoding.UTF8));
		}
	}
}
      
      



. , , . , , .





private string GenerateInjects(ITypeSymbol targetType)
{
            return $@" 
using System;
namespace {targetType.ContainingNamespace}
{{
    public partial class {targetType.Name}
    {{
        {GenerateConstructor(targetType)}
    }}
}}";
}
      
      



( ).

, . Inject , , readonly InjectIgnore. , Inject. , .





private string GenerateConstructor(ITypeSymbol targetType)
{
	var parameters = new StringBuilder();
	var fieldsInitializing = new StringBuilder();
	var fields = targetType.GetAttributes().Any(x => x.AttributeClass.Name == nameof(InjectAttribute)) 
					? targetType.GetMembers()
						.OfType<IFieldSymbol>()
						.Where(x => x.IsReadOnly && !x.GetAttributes().Any(y => y.AttributeClass.Name == nameof(InjectIgnoreAttribute)))
					: targetType.GetMembers()
						.OfType<IFieldSymbol>()
						.Where(x => x.GetAttributes().Any(y => y.AttributeClass.Name == nameof(InjectAttribute)));

	var orderedFields = fields.OrderBy(x => x.GetAttributes()
											 .First(e => e.AttributeClass.Name == nameof(InjectAttribute))
											 .ConstructorArguments.FirstOrDefault().Value ?? default(int)).ToList();
	foreach (var field in orderedFields)
	{
		var parameterName = field.Name.TrimStart('_');
		parameters.Append($"{field.Type} {parameterName},");
		fieldsInitializing.AppendLine($"this.{field.Name} = {parameterName};");
	}

	return $@"public {targetType.Name}({parameters.ToString().TrimEnd(',')})
			  {{
				  {fieldsInitializing}
			  }}";
}
      
      



partial, . , !





   GitHub.

Nuget HarabaSourceGenerators








All Articles