Как сделать свой атрибут, который обновляет значение свойства
Хочу сделать атрибут, который будет обновлять значения свойств, как в библиотеке ReactiveUI.
[Reactive] public string Text { get; set; }
А при развернутом положении можно было дать дополнительную логику и вызывать метод обновления, уже не используя атрибут.
string text;
public string Text
{
get => text;
set
{
доп. логика...
this.RaiseAndSetIfChanged(ref text, value);
}
}
Как можно сделать подобное со своим атрибутом, если кто знаете подскажите пожалуйста, или наведите на статейку или тему, где можно почитать изучить.
Ответы (2 шт):
Это решения типа 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
Пример кода, как работать с 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/
Консольный проект (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>
Библиотека с генератором (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>
Библиотека с кастомным атрибутом (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;
/* Какой-то код */
}
}
}