Substituindo eventos C # por extensões reativas usando geração de código

Olá, meu nome é Ivan e sou desenvolvedor.





Recentemente foi realizada a NETConf 2020, programada para coincidir com o lançamento do .NET 5. Um dos palestrantes falou sobre Geradores de Fontes C # . Depois de pesquisar no youtube, encontrei outro bom vídeo sobre esse assunto . Aconselho você a observá-los. Eles mostram como, enquanto o desenvolvedor está escrevendo o código, o código é gerado e o InteliSense imediatamente pega o código gerado, oferece os métodos e propriedades gerados e o compilador não jura por sua ausência. Na minha opinião, esta é uma boa oportunidade para expandir as capacidades do idioma e tentarei demonstrar isso.





Idéia

Alguém conhece o LINQ ? Portanto, para eventos, há uma biblioteca similar Extensões reativas , que permite processar eventos da mesma forma que LINQ .





O problema é que, para usar extensões reativas, você precisa organizar eventos na forma de extensões reativas e, como todos os eventos nas bibliotecas padrão são escritos em uma forma padrão, não é conveniente usar extensões reativas. Existe uma muleta que converte eventos C # padrão em extensões reativas. Se parece com isso. Digamos que haja uma aula com algum evento:





public partial class Example
{
    public event Action<int, string, bool> ActionEvent;
}
      
      



Para usar este evento no estilo de extensões reativas , você precisa escrever um método de extensão de visualização:





public static IObservable<(int, string, bool)> RxActionEvent(this TestConsoleApp.Example obj)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    return Observable.FromEvent<System.Action<int, string, bool>, (int, string, bool)>(
    conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
    h => obj.ActionEvent += h,
    h => obj.ActionEvent -= h);
}
      
      



E depois disso você pode aproveitar todas as vantagens das Extensões Reativas , por exemplo:





var example = new  Example();
example.RxActionEvent().Where(obj => obj.Item1 > 10).Take(1).Subscribe((obj)=> { /* some action  */});
      
      



Então, a ideia é que essa muleta seja gerada por si mesma, e os métodos do InteliSense possam ser usados ​​durante o desenvolvimento.





Uma tarefa

1)  «.» «Rx», , example.RxActionEvent()



, , , Action ActionEvent, .RxActionEvent()



, :





public static IObservable<(System.Int32 Item1Int32, System.String Item2String, System.Boolean Item3Boolean)> RxActionEvent(this TestConsoleApp.Example obj)
{
    if (obj == null) throw new ArgumentNullException(nameof(obj));
    return Observable.FromEvent<System.Action<System.Int32, System.String, System.Boolean>, (System.Int32 Item1Int32, System.String Item2String, System.Boolean 
Item3Boolean)>(
    conversion => (obj0, obj1, obj2) => conversion((obj0, obj1, obj2)),
    h => obj.ActionEvent += h,
    h => obj.ActionEvent -= h);
}
      
      



2) InteliSense .





 

2 .





:





<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.8.0" />
  </ItemGroup>
</Project>
      
      



, netstandard2.0 2 Microsoft.CodeAnalysis.Analyzers Microsoft.CodeAnalysis.CSharp.Workspaces.





:





<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="System.Reactive" Version="5.0.0" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
  </ItemGroup>
</Project>
      
      



, :





<ProjectReference Include="..\SourceGenerator\RxSourceGenerator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
      
      



 

[Generator]



ISourceGenerator:





[Generator]
public class RxGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)  {  }
    public void Execute(GeneratorExecutionContext context)  {  }
}
      
      



M Initialize , Execute .





Initialize ISyntaxReceiver.





, :





  • ->





  • ISyntaxReceiver->





  • ISyntaxReceiver , ->





  • Execute ISyntaxReceiver, .





, :





[Generator]
public class RxGenerator : ISourceGenerator
{
    private const string firstText = @"using System; using System.Reactive.Linq; namespace RxGenerator{}";
    public void Initialize(GeneratorInitializationContext context)
    {
        //  ISyntaxReceiver
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }
    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver) return;
        //      "RxGenerator.cs"  ,   firstText
        context.AddSource("RxGenerator.cs", firstText);
    }
    class SyntaxReceiver : ISyntaxReceiver
    {
        public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
        {
        //     ,     .
        }
    }
}
      
      



VS, using RxGenerator;



VS.





ISyntaxReceiver

OnVisitSyntaxNode MemberAccessExpressionSyntax.





private class SyntaxReceiver : ISyntaxReceiver
{
    public List<MemberAccessExpressionSyntax> GenerateCandidates { get; } =
        new List<MemberAccessExpressionSyntax>();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (!(syntaxNode is MemberAccessExpressionSyntax syntax)) return;
        if (syntax.HasTrailingTrivia || syntax.Name.IsMissing) return;
        if (!syntax.Name.ToString().StartsWith("Rx")) return;
        GenerateCandidates.Add(syntax);

    }
}
      
      



:





  • syntax.Name.IsMissing







  • syntax.HasTrailingTrivia



    -





  • !syntax.Name.ToString().StartsWith("Rx")



    "Rx"





, .





     

:





  •  ,    





  •   . , 





    System.Action<System.Int32, System.String, System.Boolean,xSouceGeneratorXUnitTests.SomeEventArgs>







  •     





:





private static IEnumerable<(string ClassType, string EventName, string EventType, List<string> ArgumentTypes)>
    GetExtensionMethodInfo(GeneratorExecutionContext context, SyntaxReceiver receiver)
{
    HashSet<(string ClassType, string EventName)>
        hashSet = new HashSet<(string ClassType, string EventName)>();
    foreach (MemberAccessExpressionSyntax syntax in receiver.GenerateCandidates)
    {
        SemanticModel model = context.Compilation.GetSemanticModel(syntax.SyntaxTree);
        ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
        {
            IMethodSymbol s => s.ReturnType,
            ILocalSymbol s => s.Type,
            IPropertySymbol s => s.Type,
            IFieldSymbol s => s.Type,
            IParameterSymbol s => s.Type,
            _ => null
        };
        if (typeSymbol == null) continue;

...
      
      



SemanticModel. . ITypeSymbol. ITypeSymbol .





...
        string eventName = syntax.Name.ToString().Substring(2);

        if (!(typeSymbol.GetMembersOfType<IEventSymbol>().FirstOrDefault(m => m.Name == eventName) is { } ev)
        ) continue;

        if (!(ev.Type is INamedTypeSymbol namedTypeSymbol)) continue;
        if (namedTypeSymbol.DelegateInvokeMethod == null) continue;
        if (!hashSet.Add((typeSymbol.ToString(), ev.Name))) continue;

        string fullType = namedTypeSymbol.ToDisplayString(SymbolDisplayFormat);
        List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters
            .Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
        yield return (typeSymbol.ToString(), ev.Name, fullType, typeArguments);
    }
}
      
      



:





string fullType = namedTypeSymbol.ToDisplayString(symbolDisplayFormat);
      
      



SymbolDisplayFormat SymbolDisplayFormat ToDisplayString() . ToDisplayString() :





System.Action<System.Int32, System.String, System.Boolean, RxSouceGeneratorXUnitTests.SomeEventArgs>
      
      







Action<int, string, bool, SomeEventArgs>
      
      



.





:





List<string> typeArguments = namedTypeSymbol.DelegateInvokeMethod.Parameters.Select(m => m.Type.ToDisplayString(SymbolDisplayFormat)).ToList();
      
      



.





StringBuilder , , .





Execute:





Spoiler
public void Execute(GeneratorExecutionContext context)
{
    if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) return;

    if (!(receiver.GenerateCandidates.Any()))
    {
        context.AddSource("RxGenerator.cs", startText);
        return;
    }

    StringBuilder sb = new();
    sb.AppendLine("using System;");
    sb.AppendLine("using System.Reactive.Linq;");
    sb.AppendLine("namespace RxMethodGenerator{");
    sb.AppendLine("    public static class RxGeneratedMethods{");

    foreach ((string classType, string eventName, string eventType, List<string> argumentTypes) in
        GetExtensionMethodInfo(context,
            receiver))
    {
        string tupleTypeStr;
        string conversionStr;

        switch (argumentTypes.Count)
        {
            case 0:
                tupleTypeStr = classType;
                conversionStr = "conversion => () => conversion(obj),";
                break;
            case 1:
                tupleTypeStr = argumentTypes.First();
                conversionStr = "conversion => obj1 => conversion(obj1),";
                break;
            default:
                tupleTypeStr =
                    $"({string.Join(", ", argumentTypes.Select((x, i) => $"{x} Item{i + 1}{x.Split('.').Last()}"))})";
                string objStr = string.Join(", ", argumentTypes.Select((x, i) => $"obj{i}"));
                conversionStr = $"conversion => ({objStr}) => conversion(({objStr})),";
                break;
        }

        sb.AppendLine(@$"        public static IObservable<{tupleTypeStr}> Rx{eventName}(this {classType} obj)");
        sb.AppendLine( @"        {");

        sb.AppendLine(  "            if (obj == null) throw new ArgumentNullException(nameof(obj));");
        sb.AppendLine(@$"            return Observable.FromEvent<{eventType}, {tupleTypeStr}>(");
        sb.AppendLine(@$"            {conversionStr}");
        sb.AppendLine(@$"            h => obj.{eventName} += h,");
        sb.AppendLine(@$"            h => obj.{eventName} -= h);");

        sb.AppendLine(  "        }");
    }
    sb.AppendLine(      "    }");
    sb.AppendLine(      "}");

    context.AddSource("RxGenerator.cs", sb.ToString());
}
      
      







  InteliSense     

«.» InteliSense . . «.» . , MS . .





CompletionProvider InteliSense «.». NuGet, .





.





CompletionProvider , , CompletionProvider:





public override bool ShouldTriggerCompletion(SourceText text, int caretPosition, CompletionTrigger trigger, OptionSet options)
{
    switch (trigger.Kind)
    {
        case CompletionTriggerKind.Insertion:
            int insertedCharacterPosition = caretPosition - 1;
            if (insertedCharacterPosition <= 0) return false;
            char ch = text[insertedCharacterPosition];
            char previousCh = text[insertedCharacterPosition - 1];
            return ch == '.' && !char.IsWhiteSpace(previousCh) && previousCh != '\t' && previousCh != '\r' && previousCh != '\n';
        default:
            return false;
    }
}
      
      



«.» - .





True , InteliSense:





public override async Task ProvideCompletionsAsync(CompletionContext context)
{
    SyntaxNode? syntaxNode = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false);
    if (!(syntaxNode?.FindNode(context.CompletionListSpan) is ExpressionStatementSyntax
        expressionStatementSyntax)) return;
    if (!(expressionStatementSyntax.Expression is MemberAccessExpressionSyntax syntax)) return;
    if (!(await context.Document.GetSemanticModelAsync(context.CancellationToken).ConfigureAwait(false) is { }
        model)) return;

    ITypeSymbol? typeSymbol = model.GetSymbolInfo(syntax.Expression).Symbol switch
    {
        IMethodSymbol s => s.ReturnType,
        ILocalSymbol s => s.Type,
        IPropertySymbol s => s.Type,
        IFieldSymbol s => s.Type,
        IParameterSymbol s => s.Type,
        _ => null
    };
    if (typeSymbol == null) return;

    foreach (IEventSymbol ev in typeSymbol.GetMembersOfType<IEventSymbol>())
    {
        ...        
        //     InteliSense
        CompletionItem item = CompletionItem.Create($"Rx{ev.Name}");
        context.AddItem(item);
    }
}
      
      



, , .





, InteliSense:





public override Task<CompletionDescription> GetDescriptionAsync(Document document, CompletionItem item, CancellationToken cancellationToken)
{
    return Task.FromResult(CompletionDescription.FromText(" "));
}
      
      



InteliSense , , «.» :





public override async Task<CompletionChange> GetChangeAsync(Document document, CompletionItem item,
    char? commitKey, CancellationToken cancellationToken)
{
    string newText = $".{item.DisplayText}()";
    TextSpan newSpan = new TextSpan(item.Span.Start - 1, 1);

    TextChange textChange = new TextChange(newSpan, newText);
    return await Task.FromResult(CompletionChange.Create(textChange));
}
      
      



!





    

Visual Studio №16.8.3. GitHub Visual Studio. Rider ReSharper 2020.3. ReSharper , 2020.3.





, . WPF , GitHub Roslyn.





CompletionProvider Vsix . NuGet . . using , NuGet.





   

Initialize Debugger.Launch();



VS





public void Initialize(GeneratorInitializationContext context)
{
    #if (DEBUG)
    Debugger.Launch();
    #endif
    context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
      
      



. - VS, .





CompletionProvider VS «Analyzer with code Fix». , Vsix. CompletionProvider , .





 

O código do gerador cabe em 140 linhas. Nessas 140 linhas, acabou mudando a sintaxe da linguagem, livrando-se de eventos substituindo-os por Extensões Reativas com uma abordagem mais conveniente, na minha opinião. Eu acho que a tecnologia de geradores de código-fonte mudará muito a abordagem para o desenvolvimento de bibliotecas e extensões.





Links

NuGet





Github








All Articles