Как сделать свой атрибут, который обновляет значение свойства

Хочу сделать атрибут, который будет обновлять значения свойств, как в библиотеке ReactiveUI.

[Reactive] public string Text { get; set; }

А при развернутом положении можно было дать дополнительную логику и вызывать метод обновления, уже не используя атрибут.

string text;
public string Text 
{
    get => text;
    set
    { 
        доп. логика...
        this.RaiseAndSetIfChanged(ref text, value);
    }
}

Как можно сделать подобное со своим атрибутом, если кто знаете подскажите пожалуйста, или наведите на статейку или тему, где можно почитать изучить.


Ответы (2 шт):

Автор решения: rotabor

Это решения типа Metalama/Postsharp, которое заключается в постобработке .NET сборки согласно заданным вами правилам, например, по разворачиванию кода.

Вот тут обсуждается в чём-то похожая тема. То есть запрашиваемое там протоколирование может быть реализовано с помощью постобратоки - в каждый метод будет вставлено неколько дополнительных команд

    ParameterInfo[] args = MethodBase.GetCurrentMethod().GetParameters();
    MY_Debug_Log(MethodBase.GetCurrentMethod().Name, args, [arg1, arg2], this);

где, среди прочего, будут автоматически (а значит без ошибок) вставлены в вызов протоколирования аргументы метода.

Кстати, диспетчеризация / проксимизация является альтернативой постобработке, и у каждого подхода есть свои минусы и плюсы.

Тут много ссылок на решения для аспектно-ориентированного программирования.

Aspect-oriented programming in .NET with AspectInjector

Introduction to Aspect-Oriented Programming (AOP) in .NET with Autofac Interceptors

→ Ссылка
Автор решения: Vladislav Mikkoev

Пример кода, как работать с Source Generator. Класс наследующийся от ISourceGenerator должен располагаться в проекте, нацеленном на .net standard 2.0.

Как дебажить генератор в Visual Studio IDE: https://github.com/JoanComasFdz/dotnet-how-to-debug-source-generator-vs2022

Как дебажить генератор в Rider IDE: https://blog.jetbrains.com/dotnet/2023/07/13/debug-source-generators-in-jetbrains-rider/

  1. Консольный проект (SourceGeneratorTest), его .csproj

     <PropertyGroup>
         <OutputType>Exe</OutputType>
         <TargetFramework>net8.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
     </PropertyGroup>
    
     <ItemGroup>
       <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
         <PrivateAssets>all</PrivateAssets>
         <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       </PackageReference>
       <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.10.0" />
     </ItemGroup>
    
     <ItemGroup>
       <ProjectReference Include="..\Core\Core.csproj" />
       <ProjectReference Include="..\SomeSourceGenerator\SomeSourceGenerator.csproj"
                         OutputItemType="Analyzer"
                         ReferenceOutputAssembly="false"/>
     </ItemGroup>
    
  2. Библиотека с генератором (SomeSourceGenerator), его .csproj

     <PropertyGroup>
         <TargetFramework>netstandard2.0</TargetFramework>
         <Nullable>enable</Nullable>
         <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
         <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
         <IsRoslynComponent>true</IsRoslynComponent>
         <LangVersion>latest</LangVersion>
     </PropertyGroup>
    
     <ItemGroup>
         <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" PrivateAssets="all" />
         <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
     </ItemGroup>
    
     <ItemGroup>
         <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
     </ItemGroup>
    
     <ItemGroup>
       <ProjectReference Include="..\Core\Core.csproj" />
     </ItemGroup>
    
  3. Библиотека с кастомным атрибутом (Core), его .csproj

     <PropertyGroup>
         <TargetFramework>netstandard2.0</TargetFramework>
         <LangVersion>11</LangVersion>
     </PropertyGroup>
    

Пример кода генератора:

using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

namespace SomeSourceGenerator
{
    [Generator]
    public class SourceGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
        }

        public void Execute(GeneratorExecutionContext context)
        {
            var compilation = context.Compilation;
            var assembly = compilation.Assembly;
            var module = assembly.Modules.First();
            foreach (var namespaceMember in module.GlobalNamespace.GetNamespaceMembers())
            {
                foreach (var namespaceTypeMember in namespaceMember.GetTypeMembers())
                {
                    var sourceBuilder = new StringBuilder();
                    sourceBuilder.Append($@"
public class {namespaceTypeMember.Name}Generated
{{");
                    bool hasAttribute = false;
                    foreach(IPropertySymbol propertyMember in namespaceTypeMember.GetMembers().Where(m => m.Kind == SymbolKind.Property))
                    {
                        var attributes = propertyMember.GetAttributes();
                        if (attributes.Length > 0 && attributes.Any(attribute => attribute.AttributeClass?.Name == nameof(SomeAttribute)))
                        {
                            hasAttribute = true;
                            
                            var privateVariableName = $"_{char.ToLowerInvariant(propertyMember.Name[0])}{propertyMember.Name.Substring(1)}";
                            sourceBuilder.Append($@"
    private {propertyMember.Type} {privateVariableName};
    public {propertyMember.Type} {propertyMember.Name}
    {{
        get => {privateVariableName};
        set
        {{
            {privateVariableName} = value;
            /* Какой-то код */ 
        }}
    }}
");
                        }
                    }

                    sourceBuilder.Append($@"}}");
                    if(hasAttribute)
                        context.AddSource($"{namespaceTypeMember.Name}Generated.g.cs", sourceBuilder.ToString());
                }
            }
        }
    }
}

Определение кастомного атрибута:

public class SomeAttribute : Attribute { }

Код основного консольного проекта:

using SomeSourceGenerator;

namespace SourceGeneratorTest;

public class SomeFirstClass
{
    [Some] public string Text { get; set; } = null!;
    [Some] public string Message { get; set; } = null!;
}

public class SomeSecondClass
{
    [Some] public string SomePropFirst { get; set; } = null!;
    [Some] public string SomePropSecond { get; set; } = null!;
}

class Program
{
    static void Main(string[] args)
    {
        
    }
}

Что сгенерировалось:

public class SomeFirstClassGenerated
{
    private string _text;
    public string Text
    {
        get => _text;
        set
        {
            _text = value;
            /* Какой-то код */ 
        }
    }

    private string _message;
    public string Message
    {
        get => _message;
        set
        {
            _message = value;
            /* Какой-то код */ 
        }
    }
}

public class SomeSecondClassGenerated
{
    private string _somePropFirst;
    public string SomePropFirst
    {
        get => _somePropFirst;
        set
        {
            _somePropFirst = value;
            /* Какой-то код */ 
        }
    }

    private string _somePropSecond;
    public string SomePropSecond
    {
        get => _somePropSecond;
        set
        {
            _somePropSecond = value;
            /* Какой-то код */ 
        }
    }
}
→ Ссылка