Location>code7788 >text

A simple but complete example of how the c#12 experimental feature Interceptor can be used

Popularity:527 ℃/2024-08-06 22:28:47

There has been a lot of reproduced dotnet on the Interceptor description of the document, but few instructions on how to use the Interceptor, here to write a simple example to show a little

c# 12 What is the experimental feature Interceptor?

The official explanation is as follows (in fact, it's simply the built-in aop function of the static weaving method in the language feature, unlike other il modification code, you have to combine it with source generater to generate code)

An interceptor is a method that can be declared at compile time to replace a call to an interceptable method with a call to itself. This replacement can be done by having the interceptor declare the source location of the intercepted call. Interceptors can add new code to the compilation (e.g., in the source generator), thus providing a limited ability to change the semantics of existing code.

Using interceptors in the source generator modifies existing compiled code rather than adding code to it. The source generator replaces calls to interceptable methods with calls to interceptor methods.

If you are interested in trying out the interceptor, you can read the feature specification for details. If using this feature, be sure to stay informed of any changes in the functional specification for this experimental feature. More guidance will be added to the Microsoft documentation site when the feature is finalized.

typical example

Example Purpose

Here we use a simple static method as the target for rewriting the contents of our method.

public static partial class DBExtensions
{
  public static string TestInterceptor<T>(object o)
  {
      return ().ToString();
  }
}

With a static method like this, we assume that the goal of the rewrite is to return the value of one of the string-type attributes of the o argument

So it should be possible to use the UT method as follows

[Fact]
public void CallNoError()
{
    ("sss", <AEnum>(new { A = "sss", C= "ddd" }));
}

How to realize

Step 1 Create a class library

Create a netstandard2.0 class library and set it up as follows

<Project Sdk="">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
	  <LangVersion>preview</LangVersion>
	  <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
	  <!-- Generates a package at build -->
	  <IncludeBuildOutput>false</IncludeBuildOutput>
	  <!-- Do not include the generator as a lib dependency -->
  </PropertyGroup>
	
	<ItemGroup>
		<PackageReference Include="" Version="3.3.4" PrivateAssets="all" />
		<PackageReference Include="" Version="4.10.0"  PrivateAssets="all"/>
	</ItemGroup>

	<ItemGroup>
		<!-- Package the generator in the analyzer directory of the nuget package -->
		<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
	</ItemGroup>

</Project>

Step 2 Set the UT Project to Enable Interceptor Functionality

The Generated directory is not required to generate code files, but in order to see the contents of the code files generated by the source generator, it is helpful for us to try it for the first time.

<Project Sdk="">

  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
	  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
	  <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
	  <InterceptorsPreviewNamespaces>$(InterceptorsPreviewNamespaces);</InterceptorsPreviewNamespaces>
  </PropertyGroup>


  <ItemGroup>
    <ProjectReference Include="..\..\src\SlowestEM.Generator2\SlowestEM." OutputItemType="Analyzer" ReferenceOutputAssembly="true" />
  </ItemGroup>

  <ItemGroup>
    <Using Include="Xunit" />
  </ItemGroup>
	<Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)">
		<RemoveDir Directories="Generated" />
	</Target>

	<ItemGroup>
		<Compile Remove="Generated\**" />
		<Content Include="Generated\**" />
	</ItemGroup>
</Project>

Step 3 Implement the InterceptorGenerator

[Generator()]
public class InterceptorGenerator : IIncrementalGenerator
{
}

Here.IIncrementalGenerator A new generation of interfaces designed for source generator with enhanced performance and convenience capabilities.

Next, we'll implement the interface

[Generator()]
public class InterceptorGenerator : IIncrementalGenerator
{
        public void Initialize(IncrementalGeneratorInitializationContext context)
        {
            var nodes = (FilterFunc, TransformFunc) // FilterFunc provides us with a range of syntax nodes to filter when traversing the grammar nodes, and TransformFunc provides us with the data we need to convert to syntax processing.
                .Where(x => x is not null)
                    .Select((x, _) => x!); var combined = (()); var combined = (()); var combined = (())
            
            (combined, Generate); // Generate is the final method that actually converts the code file
        }
}

Let's move on to the implementationFilterFunc

private bool FilterFunc(SyntaxNode node, CancellationToken token) // Here we only filter invocations TestInterceptor The place of the method
{
    if (node is InvocationExpressionSyntax ie && ().FirstOrDefault() is MemberAccessExpressionSyntax ma)
    {
        return ().StartsWith("TestInterceptor");
    }

    return false;
}

// It can be seen that more than the previous ISyntaxContextReceiver simpler

Let's move on to the implementationTransformFunc

private TestData TransformFunc(GeneratorSyntaxContext ctx, CancellationToken token)
{
    try
    {
        // Filtering again ensures that only the processing Scenarios for method invocation
        if ( is not InvocationExpressionSyntax ie
            || (ie) is not IInvocationOperation op)
        {
            return null;
        }
        
        // Since our tests use the Anonymous Class Initialization statement,The parameters are object,So there is actually an implicit conversion when generating
        var s = (i => as IConversionOperation).Where(i => i is not null)
            .Select(i => as IAnonymousObjectCreationOperation) // Finds the first of the anonymous class because of string particular property
            .Where(i => i is not null)
            .SelectMany(i => )
            .Select(i => i as IAssignmentOperation)
            .FirstOrDefault(i => () == "string");

// generating come (or go) back first because of string particular property的 methodologies
        return new TestData { Location = (), Method = @$"
internal static {} {}_test({("", (i => @$"{} {}"))})
{{
{(s == null ? "return null;" : $@"
dynamic c = o;
return c.{( as IPropertyReferenceOperation).};
") }
}}
" };
    }
    catch (Exception ex)
    {
        ();
        return null;
    }
}

// Here we create an arbitrary class to make it easier for us to handle the intermediate data
public class TestData
{
    public Location Location { get; set; }
    public string Method { get; set; }
}

public static class TypeSymbolHelper
{
    // gain Physical path to the file where the syntax node is located
    internal static string GetInterceptorFilePath(this SyntaxTree? tree, Compilation compilation)
    {
        if (tree is null) return "";
        return ?.NormalizePath(, baseFilePath: null) ?? ;
    }

    public static Location GetMemberLocation(this IInvocationOperation call)
        => GetMemberSyntax(call).GetLocation();

    // unfortunately,Since the interceptor Replacement must code file physical file location,line number All column numbers must be accurate, for example , for example要 TestInterceptor the exact location of the, if from xxx. It was all wrong to begin with.,The compilation does not pass
    // 所以这里有一个比较繁琐的methodologies来帮助我们准确找到 placement
    public static SyntaxNode GetMemberSyntax(this IInvocationOperation call)
    {
        var syntax = call?.Syntax;
        if (syntax is null) return null!; // GIGO

        foreach (var outer in ())
        {
            var outerNode = ();
            if (outerNode is not null && outerNode is MemberAccessExpressionSyntax)
            {
                // if there is an identifier, we want the **last** one - think (...)
                SyntaxNode? identifier = null;
                foreach (var inner in ())
                {
                    var innerNode = ();
                    if (innerNode is not null && innerNode is SimpleNameSyntax)
                        identifier = innerNode;
                }
                // we'd prefer an identifier, but we'll allow the entire member-access
                return identifier ?? outerNode;
            }
        }
        return syntax;
    }
}

Let's move on to the implementationGenerate

private void Generate(SourceProductionContext ctx, (Compilation Left, ImmutableArray<TestData> Right) state)
{
    try
    {
      // The main thing here is to generate InterceptsLocation
        var s = ("", (i =>
        {
            var loc = ();
            var start = ;
            return @$"[global::({(, (()))},{ + 1},{ + 1})]
{}";
        }));
        var ss = $@"
namespace
{{
file static class GeneratedInterceptors
{{
{s}
}}
}}


namespace
{{
// this type is needed by the compiler to implement interceptors - it doesn't need to
// come from the runtime itself, though

[global::(""DEBUG"")] // not needed post-build, so: evaporate
[global::(global::, AllowMultiple = true)]
sealed file class InterceptsLocationAttribute : global::
{{
public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber)
{{
    _ = path;
    _ = lineNumber;
    _ = columnNumber;
}}
}}
}}
";
        (( ?? "package") + ".", ss);
    }
    catch (Exception ex)
    {
        ();
    }
}

Currently requires customizationInterceptsLocationAttributeSo it's necessary to generate one.

This is done at the moment mainly because it is still experimental characteristics, api design is still changing, and in fact, the physical file location has now been recognized as very inconvenient, has been designed in a new way, but the relevant design is not yet very convenient to use, so here we also still use the physical location of the way

Interested kids can refer to

Last step: Try compiling it.

If we compile the program, we will see that this file code is generated


namespace  
{
    file static class GeneratedInterceptors
    {
        [global::("D:\\code\\dotnet\\SlowestEM\\test\\UT\\GeneratorUT\\",26,35)]

internal static string TestInterceptor_test(object o)
{
    
    dynamic c = o;
    return ;

}

    }
}


namespace 
{
    // this type is needed by the compiler to implement interceptors - it doesn't need to
    // come from the runtime itself, though

    [global::("DEBUG")] // not needed post-build, so: evaporate
    [global::(global::, AllowMultiple = true)]
    sealed file class InterceptsLocationAttribute : global::
    {
        public InterceptsLocationAttribute(string path, int lineNumber, int columnNumber)
        {
            _ = path;
            _ = lineNumber;
            _ = columnNumber;
        }
    }
}

If you run ut and the results are correct, debugging line by line also shows that the breakpoints go into our generated code file.