6

Observing and Modifying Code Execution Dynamically

This chapter is about some common types that are included with .NET for performing code reflection and applying and reading attributes, working with expression trees, and creating source generators.

This chapter covers the following topics:

  • Working with reflection and attributes
  • Working with expression trees
  • Creating source generators

Working with reflection and attributes

Reflection is a programming feature that allows code to understand and manipulate itself. An assembly is made up of up to four parts:

  • Assembly metadata and manifest: Name, assembly, file version, referenced assemblies, and so on.
  • Type metadata: Information about the types, their members, and so on.
  • IL code: Implementation of methods, properties, constructors, and so on.
  • Embedded resources (optional): Images, strings, JavaScript, and so on.

The metadata comprises items of information about your code. The metadata is generated automatically from your code (for example, information about the types and members) or applied to your code using attributes.

Attributes can be applied at multiple levels: to assemblies, to types, and to their members, as shown in the following code:

// an assembly-level attribute
[assembly: AssemblyTitle("Working with reflection and attributes")]
// a type-level attribute
[Serializable] 
public class Person
{
  // a member-level attribute 
  [Obsolete("Deprecated: use Run instead.")] 
  public void Walk()
  {
...

Attribute-based programming is used a lot in app models like ASP.NET Core to enable features like routing, security, and caching.

Versioning of assemblies

Version numbers in .NET are a combination of three numbers, with two optional additions. If you follow the rules of semantic versioning, the three numbers denote the following:

  • Major: Breaking changes.
  • Minor: Non-breaking changes, including new features, and often, bug fixes.
  • Patch: Non-breaking bug fixes.

Optionally, a version can include these:

  • Prerelease: Unsupported preview releases.
  • Build number: Nightly builds.

Good Practice: Follow the rules of semantic versioning, as described at the following link: http://semver.org.

Reading assembly metadata

Let’s explore working with attributes:

  1. Use your preferred code editor to add a new Console App/console project named WorkingWithReflection to a Chapter06 solution/workspace:
    • In Visual Studio 2022, set the startup project to the current selection.
    • In Visual Studio Code, select WorkingWithReflection as the active OmniSharp project.
  2. In the project file, statically and globally import the Console class, as shown in the following markup:
    <ItemGroup>
      <Using Include="System.Console" Static="true" />
    </ItemGroup>
    
  3. In Program.cs, import the namespace for reflection, and add statements to get the console app’s assembly, output its name and location, and get all assembly-level attributes and output their types, as shown in the following code:
    using System.Reflection; // Assembly
    WriteLine("Assembly metadata:");
    Assembly? assembly = Assembly.GetEntryAssembly();
    if (assembly is null)
    {
      WriteLine("Failed to get entry assembly.");
      return;
    }
    WriteLine($"  Full name: {assembly.FullName}"); 
    WriteLine($"  Location: {assembly.Location}");
    WriteLine($"  Entry point: {assembly.EntryPoint?.Name}");
    IEnumerable<Attribute> attributes = assembly.GetCustomAttributes(); 
    WriteLine($"  Assembly-level attributes:");
    foreach (Attribute a in attributes)
    {
      WriteLine($"   {a.GetType()}");
    }
    
  4. Run the code and view the result, as shown in the following output:
    Assembly metadata:
      Full name: WorkingWithReflection, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
      Location: C:apps-services-net7Chapter06WorkingWithReflectioninDebug
    et7.0WorkingWithReflection.dll
      Entry point: <Main>$
      Assembly-level attributes:
        System.Runtime.CompilerServices.CompilationRelaxationsAttribute
        System.Runtime.CompilerServices.RuntimeCompatibilityAttribute
        System.Diagnostics.DebuggableAttribute
        System.Runtime.Versioning.TargetFrameworkAttribute
        System.Reflection.AssemblyCompanyAttribute
        System.Reflection.AssemblyConfigurationAttribute
        System.Reflection.AssemblyFileVersionAttribute
        System.Reflection.AssemblyInformationalVersionAttribute
        System.Reflection.AssemblyProductAttribute
        System.Reflection.AssemblyTitleAttribute
    

    Note that because the full name of an assembly must uniquely identify the assembly, it is a combination of the following:

    • Name, for example, WorkingWithReflection
    • Version, for example, 1.0.0.0
    • Culture, for example, neutral
    • Public key token, although this can be null

    Now that we know some of the attributes decorating the assembly, we can ask for them specifically.

  1. Add statements to get the AssemblyInformationalVersionAttribute and AssemblyCompanyAttribute classes and then output their values, as shown in the following code:
    AssemblyInformationalVersionAttribute? version = assembly
      .GetCustomAttribute<AssemblyInformationalVersionAttribute>();
    WriteLine($"  Version: {version?.InformationalVersion}");
    AssemblyCompanyAttribute? company = assembly
      .GetCustomAttribute<AssemblyCompanyAttribute>();
    WriteLine($"  Company: {company?.Company}");
    
  2. Run the code and view the result, as shown in the following output:
      Version: 1.0.0
      Company: WorkingWithReflection
    

    Hmmm, unless you set the version, it defaults to 1.0.0, and unless you set the company, it defaults to the name of the assembly.

    Let’s explicitly set this information. The legacy .NET Framework way to set these values was to add attributes in the C# source code file, as shown in the following code:

    [assembly: AssemblyCompany("Packt Publishing")] 
    [assembly: AssemblyInformationalVersion("1.3.0")]
    

    The Roslyn compiler used by .NET sets these attributes automatically, so we can’t use the old way. Instead, they must be set in the project file.

  1. Edit the WorkingWithReflection.csproj project file to add elements for version and company, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <Version>7.0.1</Version>
        <Company>Packt Publishing</Company>
      </PropertyGroup>
    
  2. Run the code and view the result, as shown in the following partial output:
    Assembly metadata:
      Full name: WorkingWithReflection, Version=7.0.1.0, Culture=neutral, PublicKeyToken=null
      ...
      Version: 7.0.1
      Company: Packt Publishing
    

Creating custom attributes

You can define your own attributes by inheriting from the Attribute class:

  1. Add a class file to your project named CoderAttribute.cs.
  2. In CoderAttribute.cs, define an attribute class that can decorate either classes or methods with two properties to store the name of a coder and the date they last modified some code, as shown in the following code:
    namespace Packt.Shared;
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
      AllowMultiple = true)]
    public class CoderAttribute : Attribute
    {
      public string Coder { get; set; }
      public DateTime LastModified { get; set; }
      public CoderAttribute(string coder, string lastModified)
      {
        Coder = coder;
        LastModified = DateTime.Parse(lastModified);
      }
    }
    
  3. Add a class file to your project named Animal.cs.
  4. In Animal.cs, add a class with a method, and decorate the method with the Coder attribute with data about two coders, as shown in the following code:
    namespace Packt.Shared;
    public class Animal
    {
      [Coder("Mark Price", "22 August 2022")]
      [Coder("Johnni Rasmussen", "13 September 2022")] 
      public void Speak()
      {
        WriteLine("Woof...");
      }
    }
    
  5. In Program.cs, import namespaces for working with your custom attribute, as shown in the following code:
    using Packt.Shared; // CoderAttribute
    
  6. In Program.cs, add code to get the types in the current assembly, enumerate their members, read any Coder attributes on those members, and output the information, as shown in the following code:
    WriteLine();
    WriteLine($"* Types:");
    Type[] types = assembly.GetTypes();
    foreach (Type type in types)
    {
      WriteLine();
      WriteLine($"Type: {type.FullName}"); 
      MemberInfo[] members = type.GetMembers();
      foreach (MemberInfo member in members)
      {
        WriteLine("{0}: {1} ({2})",
          member.MemberType, member.Name,
          member.DeclaringType?.Name);
        IOrderedEnumerable<CoderAttribute> coders = 
          member.GetCustomAttributes<CoderAttribute>()
          .OrderByDescending(c => c.LastModified);
        foreach (CoderAttribute coder in coders)
        {
          WriteLine("-> Modified by {0} on {1}",
            coder.Coder, coder.LastModified.ToShortDateString());
        }
      }
    }
    
  7. Run the code and view the result, as shown in the following partial output:
    * Types:
    ...
    Type: Packt.Shared.Animal
    Method: Speak (Animal)
    -> Modified by Johnni Rasmussen on 13/09/2022
    -> Modified by Mark Price on 22/08/2022
    Method: GetType (Object)
    Method: ToString (Object)
    Method: Equals (Object)
    Method: GetHashCode (Object)
    Constructor: .ctor (Program)
    ...
    Type: Program+<>c
    Method: GetType (Object)
    Method: ToString (Object)
    Method: Equals (Object)
    Method: GetHashCode (Object)
    Constructor: .ctor (<>c)
    Field: <>9 (<>c)
    Field: <>9__0_0 (<>c)
    

Understanding compiler-generated types and members

What is the Program+<>c type and its strangely named fields?

It is a compiler-generated display class. <> indicates compiler-generated and c indicates a display class. They are undocumented implementation details of the compiler and could change at any time. You can ignore them, so as an optional challenge, add statements to your console app to filter compiler-generated types by skipping types decorated with CompilerGeneratedAttribute.

Hint: Import the namespace for working with compiler-generated code, as shown in the following code:

using System.Runtime.CompilerServices; // CompilerGeneratedAttribute

Making a type or member obsolete

Over time, you might decide to refactor your types and their members while maintaining backward compatibility. To encourage developers who use your types to use the newer implementations, you can decorate the old types and members with the [Obsolete] attribute.

Let’s see an example:

  1. In Animal.cs, add a new method and mark the old method as obsolete, as shown highlighted in the following code:
    [Coder("Mark Price", "22 August 2022")]
    [Coder("Johnni Rasmussen", "13 September 2022")]
    [Obsolete($"use {nameof(SpeakBetter)} instead.")]
    public void Speak()
    {
      WriteLine("Woof...");
    }
    public void SpeakBetter()
    {
      WriteLine("Wooooooooof...");
    }
    
  2. In Program.cs, modify the statements to detect obsolete methods, as shown highlighted in the following code:
    foreach (MemberInfo member in members)
    {
      ObsoleteAttribute? obsolete =
        member.GetCustomAttribute<ObsoleteAttribute>();
      WriteLine("{0}: {1} ({2}) {3}",
        member.MemberType, member.Name,
        member.DeclaringType?.Name, 
        obsolete is null ? "" : $"Obsolete! {obsolete.Message}");
    
  3. Run the code and view the result, as shown in the following output:
    Type: Packt.Shared.Animal
    Method: Speak (Animal) Obsolete! use SpeakBetter instead.
    -> Modified by Johnni Rasmussen on 13/09/2022
    -> Modified by Mark Price on 22/08/2022
    Method: SpeakBetter (Animal)
    Method: GetType (Object)
    Method: ToString (Object)
    Method: Equals (Object)
    Method: GetHashCode (Object)
    Constructor: .ctor (Animal)
    

Dynamically loading assemblies and executing methods

Normally, if a .NET project needs to execute in another .NET assembly, you reference the package or project, and then at compile time, the compiler knows the assemblies that will be loaded into the memory of the calling codebase during start up at runtime. But sometimes you may not know the assemblies that you need to call until runtime. For example, a word processor does not need to have the functionality to perform a mail merge loaded all the time. The mail merge feature could be implemented as a separate assembly that is only loaded into memory when it is activated by the user. Another example would be an application that allows custom plugins, perhaps even created by other developers.

You can dynamically load a set of assemblies into an AssemblyLoadContext, execute methods in them, and then unload the AssemblyLoadContext, which unloads the assemblies too. A side effect of this is reduced memory usage.

In .NET 7, the overhead of using reflection to invoke a member of a type, like calling a method or setting, or getting a property, has been made up to four times faster when it is done more than once on the same member.

Let’s see how to dynamically load an assembly and then instantiate a class and interact with its members:

  1. Use your preferred code editor to add a new Class Library/classlib project named DynamicLoadAndExecute.Library to the Chapter06 solution/workspace.
  2. In the project file, treat warnings as errors, statically and globally import the Console class, and globally import the namespace for working with reflection, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
      <ItemGroup>
        <Using Include="System.Reflection" />
        <Using Include="System.Console" Static="true" />
      </ItemGroup>
    </Project>
    
  3. Rename Class1.cs to Dog.cs.
  4. In Dog.cs, define a Dog class with a Speak method that writes a simple message to the console based on a string parameter passed to the method, as shown in the following code:
    namespace DynamicLoadAndExecute.Library;
    public class Dog
    {
      public void Speak(string? name)
      {
        WriteLine($"{name} says Woof!");
      }
    }
    
  5. Use your preferred code editor to add a new Console App/console project named DynamicLoadAndExecute.Console to the Chapter06 solution/workspace.
  6. In the project file, treat warnings as errors, statically and globally import the Console class, and globally import the namespace for working with reflection, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
      </PropertyGroup>
      <ItemGroup>
        <Using Include="System.Reflection" />
        <Using Include="System.Console" Static="true" />
      </ItemGroup>
    </Project>
    
  7. Build the DynamicLoadAndExecute.Library project to create the assembly in its bin folder structure.
  8. Build the DynamicLoadAndExecute.Console project to create its bin folder structure.
  9. Copy the three files from the DynamicLoadAndExecute.Library project’s binDebug et7.0 folder to the equivalent folder in the DynamicLoadAndExecute.Console project, as shown in the following list:
    • DynamicLoadAndExecute.Library.deps.json
    • DynamicLoadAndExecute.Library.dll
    • DynamicLoadAndExecute.Library.pdb
  10. In the DynamicLoadAndExecute.Console project, add a new class file named Program.Helpers.cs and modify its contents to define a method to output information about an assembly and its types, as shown in the following code:
    partial class Program
    {
      static void OutputAssemblyInfo(Assembly a)
      {
        WriteLine("FullName: {0}", a.FullName);
        WriteLine("Location: {0}", Path.GetDirectoryName(a.Location));
        WriteLine("IsCollectible: {0}", a.IsCollectible);
        WriteLine("Defined types:");
        foreach (TypeInfo info in a.DefinedTypes)
        {
          if (!info.Name.EndsWith("Attribute"))
          {
            WriteLine("  Name: {0}, Members: {1}", 
              info.Name, info.GetMembers().Count());
          }
        }
        WriteLine();
      }
    }
    
  11. In the DynamicLoadAndExecute.Console project, add a new class file named DemoAssemblyLoadContext.cs, and modify its contents to load a named assembly into the current context at runtime using an assembly dependency resolver, as shown in the following code:
    using System.Runtime.Loader; // AssemblyDependencyResolver
    internal class DemoAssemblyLoadContext : AssemblyLoadContext
    {
      private AssemblyDependencyResolver _resolver;
      public DemoAssemblyLoadContext(string mainAssemblyToLoadPath)
        : base(isCollectible: true)
      {
        _resolver = new AssemblyDependencyResolver(mainAssemblyToLoadPath);
      }
    }
    
  12. In Program.cs, delete the existing statements. Then, use the load context class to load the class library and output information about it, and then dynamically create an instance of the Dog class and call its Speak method, as shown in the following code:
    Assembly? thisAssembly = Assembly.GetEntryAssembly();
    if (thisAssembly is null)
    {
      WriteLine("Could not get the entry assembly.");
      return;
    }
    OutputAssemblyInfo(thisAssembly);
    WriteLine("Creating load context for:
      {0}
    ", 
      Path.GetFileName(thisAssembly.Location));
    DemoAssemblyLoadContext loadContext = new(thisAssembly.Location);
    string assemblyPath = Path.Combine(
      Path.GetDirectoryName(thisAssembly.Location) ?? "", 
      "DynamicLoadAndExecute.Library.dll");
    WriteLine("Loading:
      {0}
    ",
      Path.GetFileName(assemblyPath));
    Assembly dogAssembly = loadContext.LoadFromAssemblyPath(assemblyPath);
    OutputAssemblyInfo(dogAssembly);
    Type? dogType = dogAssembly.GetType("DynamicLoadAndExecute.Library.Dog");
    if (dogType is null)
    {
      WriteLine("Could not get the Dog type.");
      return;
    }
    MethodInfo? method = dogType.GetMethod("Speak");
    if (method != null)
    {
      object? dog = Activator.CreateInstance(dogType);
      for (int i = 0; i < 10; i++)
      {
        method.Invoke(dog, new object[] { "Fido" });
      }
    }
    WriteLine();
    WriteLine("Unloading context and assemblies.");
    loadContext.Unload();
    
  13. Start the console app and note the results, as shown in the following output:
    FullName: DynamicLoadAndExecute.Console, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    Location: C:apps-services-net7Chapter06DynamicLoadAndExecute.ConsoleinDebug
    et7.0
    IsCollectible: False
    Defined types:
      Name: DemoAssemblyLoadContext, Members: 29
      Name: Program, Members: 5
    Creating load context for:
      DynamicLoadAndExecute.Console.dll
    Loading:
      DynamicLoadAndExecute.Library.dll
    FullName: DynamicLoadAndExecute.Library, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
    Location: C:apps-services-net7Chapter06DynamicLoadAndExecute.ConsoleinDebug
    et7.0
    IsCollectible: True
    Defined types:
      Name: Dog, Members: 6
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Fido says Woof!
    Unloading context and assemblies.
    

Note that the entry assembly (the console app) is not collectible, meaning that it cannot be removed from memory, but the dynamically loaded class library is collectible.

Doing more with reflection

This is just a taster of what can be achieved with reflection. Reflection can also do the following:

Working with expression trees

Expression trees represent code as a structure that you can examine or execute. Expression trees are immutable so you cannot change one, but you can create a copy with the changes you want.

If you compare expression trees to functions, then although functions have flexibility in the parameter values passed to them, the structure of the function, what it does with those values, and how are all fixed. Expression trees provide a structure that can dynamically change, so what and how a function is implemented can be dynamically changed at runtime.

Expression trees are also used to represent an expression in an abstract way, so instead of being expressed using C# code, the expression is expressed as a data structure in memory. This then allows that data structure to be expressed in other ways, using other languages.

When you write a LINQ expression for the EF Core database provider, it is represented by an expression tree that is then translated into an SQL statement. But even the simplest C# statement can be represented as an expression tree.

Let’s look at a simple example, adding two numbers:

int three = 1 + 2;

This statement would be represented as the tree in Figure 6.1:

Figure 6.1: An expression tree of a simple statement adding two numbers

Understanding components of expression trees

The System.Linq.Expressions namespace contains types for representing the components of an expression tree. For example:

Type

Description

BinaryExpression

An expression with a binary operator.

BlockExpression

A block containing a sequence of expressions where variables can be defined.

CatchBlock

A catch statement in a try block.

ConditionalExpression

An expression that has a conditional operator.

LambdaExpression

A lambda expression.

MemberAssignment

Assigning to a field or property.

MemberExpression

Accessing a field or property.

MethodCallExpression

A call to a method.

NewExpression

A call to a constructor.

Only expression trees that represent lambda expressions can be executed.

Executing the simplest expression tree

Let’s see how to construct, compile, and execute an expression tree:

  1. Use your preferred code editor to add a new Console App/console project named WorkingWithExpressionTrees to the Chapter06 solution/workspace.
  2. In the project file, statically and globally import the Console class, as shown in the following markup:
    <ItemGroup>
      <Using Include="System.Console" Static="true" />
    </ItemGroup>
    
  3. In Program.cs, delete the existing statements and then define an expression tree and execute it, as shown in the following code:
    using System.Linq.Expressions; // Expression and so on
    ConstantExpression one = Expression.Constant(1, typeof(int));
    ConstantExpression two = Expression.Constant(2, typeof(int));
    BinaryExpression add = Expression.Add(one, two);
    Expression<Func<int>> expressionTree = Expression.Lambda<Func<int>>(add);
    Func<int> compiledTree = expressionTree.Compile();
    WriteLine($"Result: {compiledTree()}");
    
  4. Run the console app and note the result, as shown in the following output:
    Result: 3
    

Creating source generators

Source generators were introduced with C# 9 and .NET 5. They allow a programmer to get a compilation object that represents all the code being compiled, dynamically generate additional code files, and compile those too. Source generators are like code analyzers that can add more code to the compilation process.

A great example is the System.Text.Json source generator. The classic method for serializing JSON uses reflection at runtime to dynamically analyze an object model, but this is slow. The better method uses source generators to create source code that is then compiled to give improved performance.

You can read more about the System.Text.Json source generator at the following link: https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/.

Implementing the simplest source generator

We will create a source generator that programmatically creates a code file that adds a method to the Program class, as shown in the following code:

// source-generated code
static partial class Program
{
  static partial void Message(string message)
  {
    System.Console.WriteLine($"Generator says: '{message}'"); 
  }
}

This method can then be called in the Program.cs file of the project that uses this source generator.

Let’s see how to do this:

  1. Use your preferred code editor to add a new Console App/console project named GeneratingCodeApp to the Chapter06 solution/workspace.
  2. In the project file, statically and globally import the Console class, as shown in the following markup:
    <ItemGroup>
      <Using Include="System.Console" Static="true" />
    </ItemGroup>
    
  3. Add a new class file name Program.Methods.cs.
  4. In Program.Methods.cs, define a partial Program class with a partial method with a string parameter, as shown in the following code:
    partial class Program
    {
      static partial void Message(string message);
    }
    
  5. In Program.cs, delete the existing statements and then call the partial method, as shown in the following code:
    Message("Hello from some source generator code.");
    
  6. Use your preferred code editor to add a new Class Library/classlib project named GeneratingCodeLib that targets .NET Standard 2.0 to the Chapter06 solution/workspace.

    Currently, source generators must target .NET Standard 2.0. The default C# version used for class libraries that target .NET Standard 2.0 is C# 7.3, as shown at the following link: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version#defaults.

  1. In the project file, set the C# language version to 10 or later (to support global using statements), statically and globally import the Console class, and add the NuGet packages Microsoft.CodeAnalysis.Analyzers and Microsoft.CodeAnalysis.CSharp, as shown highlighted in the following markup:
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
        <LangVersion>10</LangVersion>
      </PropertyGroup>
    	
      <ItemGroup>
        <Using Include="System.Console" Static="true" />
      </ItemGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" 
                          Version="3.3.3">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; 
                         buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" 
                          Version="4.1.0" />
      </ItemGroup>
    </Project>
    

    This project does not enable null warnings because the <Nullable>enable</Nullable> element is missing. If you add it, then you will see some null warnings later.

  1. Build the GeneratingCodeLib project.
  2. Rename Class1.cs as MessageSourceGenerator.cs.
  3. In the GeneratingCodeLib project, in MessageSourceGenerator.cs, define a class that implements ISourceGenerator and is decorated with the [Generator] attribute, as shown in the following code:
    using Microsoft.CodeAnalysis; // [Generator], GeneratorInitializationContext
                                  // ISourceGenerator, GeneratorExecutionContext
    namespace Packt.Shared;
    [Generator]
    public class MessageSourceGenerator : ISourceGenerator
    {
      public void Execute(GeneratorExecutionContext execContext)
      {
        IMethodSymbol mainMethod = execContext.Compilation
          .GetEntryPoint(execContext.CancellationToken);
        string sourceCode = $@"// source-generated code
    static partial class {mainMethod.ContainingType.Name}
    {{
      static partial void Message(string message)
      {{
        System.Console.WriteLine($""Generator says: '{{message}}'"");
      }}
    }}
    ";
        string typeName = mainMethod.ContainingType.Name;
        execContext.AddSource($"{typeName}.Methods.g.cs", sourceCode);
      }
      public void Initialize(GeneratorInitializationContext initContext)
      {
        // this source generator does not need any initialization
      }
    }
    

    Good Practice: Include .g. or .generated. in the filename of source-generated files.

  1. In the GeneratingCodeApp project, in the project file, add a reference to the class library project, as shown in the following markup:
    <ItemGroup>
      <ProjectReference Include="..GeneratingCodeLibGeneratingCodeLib.csproj"
                        OutputItemType="Analyzer"
                        ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    Good Practice: It is sometimes necessary to restart Visual Studio 2022 to see the results of working with source generators.

    Build the GeneratingCodeApp project and note the auto-generated class file:

    • In Visual Studio 2022, in Solution Explorer, expand the Dependencies | Analyzers | GeneratingCodeLib | Packt.Shared.MessageSourceGenerator nodes to find the Program.Methods.g.cs file, as shown in Figure 6.2:

    Figure 6.2: The source-generated Program.Methods.g.cs file

    Visual Studio Code does not automatically run analyzers. We must add an extra entry in the project file to enable the automatic generation of the source generator file.

    • In Visual Studio Code, in the GeneratingCodeApp project, in the project file, in the <PropertyGroup>, add an entry to enable the generation of the code file, as shown in the following markup:
      <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
      
    • In Visual Studio Code, in Terminal, build the GeneratingCodeApp project.
    • In Visual Studio Code, in the obj/Debug/net7.0 folder, note the generated folder and its subfolder GeneratingCodeLib/Packt.Shared.MessageSourceGenerator, and the auto-generated file named Program.Methods.g.cs.
  1. Open the Program.Methods.g.cs file and note its contents, as shown in the following code:
    // source-generated code
    static partial class Program
    {
      static partial void Message(string message)
      {
        System.Console.WriteLine($"Generator says: '{message}'");
      }
    }
    
  2. Run the console app and note the message, as shown in the following output:
    Generator says: 'Hello from some source generator code.'
    

You can control the path for automatically generated code files by adding a <CompilerGeneratedFilesOutputPath> element.

Doing more with source generators

Source generators are a massive topic.

To learn more, use the following links:

Practicing and exploring

Test your knowledge and understanding by answering some questions, getting some hands-on practice, and exploring with deeper research into the topics in this chapter.

Exercise 6.1 – Test your knowledge

Use the web to answer the following questions:

  1. What are the four parts of a .NET assembly and which are optional?
  2. What can an attribute be applied to?
  3. What are the names of the parts of a version number and what do they mean if they follow the rules of semantic versioning?
  4. How do you get a reference to the assembly for the currently executing console app?
  5. How do you get all the attributes applied to an assembly?
  6. How should you create a custom attribute?
  7. What class do you inherit from to enable dynamic loading of assemblies?
  8. What is an expression tree?
  9. What is a source generator?
  10. Which interface must a source generator class implement and what methods are part of that interface?

Exercise 6.2 – Explore topics

Use the links on the following page to learn more detail about the topics covered in this chapter:

https://github.com/markjprice/apps-services-net7/blob/main/book-links.md#chapter-6---controlling-the-roslyn-compiler-reflection-and-expression-trees

Summary

In this chapter, you:

  • Reflected on code and attributes.
  • Constructed, compiled, and executed a simple expression tree.
  • Built a source generator and used it in a console app project.

In the next chapter, we will learn how to work with data stored in SQL Server.

Join our book’s Discord space

Join the book’s Discord workspace for Ask me Anything sessions with the author.

https://packt.link/apps_and_services_dotnet7

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset