using System.ComponentModel.DataAnnotations; namespace NotifyPropertyChangedGenerator.Demo; public enum Gender { NotSpecified, [Display(Name = "مرد")] Male, [Display(Name = "زن")] Female }
public static class Extensions { public static string GetDisplayName(this Enum value) { if (value is null) throw new ArgumentNullException(nameof(value)); var attribute = value.GetType().GetField(value.ToString())? .GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault(); if (attribute is null) return value.ToString(); return attribute.GetType().GetProperty("Name")?.GetValue(attribute, null)?.ToString(); } }
یعنی در اصل متد کمکی که برای اینکار نیاز داریم، چنین خروجی را دارد:
namespace NotifyPropertyChangedGenerator.Demo { public static class GenderExtensions { public static string GetDisplayName(this Gender @enum) { return @enum switch { Gender.NotSpecified => "NotSpecified", Gender.Male => "مرد", Gender.Female => "زن", _ => throw new ArgumentOutOfRangeException(nameof(@enum)) }; } } }
با ارائهی Source Generators، مشکلات یاد شده دیگر وجود ندارند. یعنی کار تولید متدهای اختصاصی برای هر enum، خودکار است و همچنین به روز رسانی آنی آنها با هر تغییری در enumها نیز پیشبینی شدهاست.
تهیهی تولید کنندهی خودکار کدی که نام نمایشی enumها را به صورت از پیش محاسبه شده ارائه میدهد
در قسمت قبل، با روش تهیه و استفاده از Source Generators آشنا شدیم. در اینجا نیز از همان قالب، در جهت تولید کد متد الحاقی GetDisplayName فوق، استفاده خواهیم کرد. یعنی هدف رسیدن به کلاس GenderExtensions فوق و متد GetDisplayName آن، در زمان کامپایل برنامه و به صورت خودکار است:
[Generator] public class EnumExtensionsGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) {} public void Execute(GeneratorExecutionContext context) { var compilation = context.Compilation; foreach (var syntaxTree in compilation.SyntaxTrees) { var semanticModel = compilation.GetSemanticModel(syntaxTree); var immutableHashSet = syntaxTree.GetRoot() .DescendantNodesAndSelf() .OfType<EnumDeclarationSyntax>() .Select(enumDeclarationSyntax => semanticModel.GetDeclaredSymbol(enumDeclarationSyntax)) .OfType<ITypeSymbol>() /*.Where(typeSymbol => typeSymbol.GetAttributes().Any( attributeData => string.Equals(attributeData.AttributeClass?.Name, "GenerateExtensions", StringComparison.Ordinal) ))*/ .ToImmutableHashSet(); foreach (var typeSymbol in immutableHashSet) { var source = GenerateEnumExtensions(typeSymbol); context.AddSource($"{typeSymbol.Name}Extensions.cs", source); } } }
در اینجا در متد Execute، دسترسی کاملی را به اطلاعات تهیه شدهی توسط کامپایلر داریم. توسط آن تمام Enumهای برنامه را یا همان EnumDeclarationSyntax را در اینجا، یافته و سپس حلقهای را بر روی اطلاعات آنها تشکیل داده و برای تک تک آنها، توسط متد GenerateEnumExtensions، کد معادل کلاس GenderExtensions را که در این مطلب معرفی شد، تولید میکنیم. در پایان کار نیز این کد را توسط متد AddSource، به کامپایلر معرفی خواهیم کرد تا بلافاصله در IDE ظاهر شده و قابلیت استفاده را پیدا کند.
یک نکته: اگر میخواهید صرفا enumهای خاصی در این بین بررسی شوند، میتوانید کدهای یک Attribute سفارشی را مثلا با نام فرضی [GenerateExtensions] در همینجا توسط متد context.AddSource به مجموعه سورسها اضافه کنید و سپس بر اساس نام آن، در قسمت Where ایی که کامنت شدهاست، تنها اطلاعات مدنظر را فیلتر و پردازش کنید.
متدی هم که ابتدا کلاس Extensions را بر اساس نام هر Enum موجود تولید و سپس بدنهی متد GetDisplayName اختصاصی آنرا تکمیل میکند، به صورت زیر است:
private string GenerateEnumExtensions(ITypeSymbol typeSymbol) { return $@"namespace {typeSymbol.ContainingNamespace} {{ public static class {typeSymbol.Name}Extensions {{ public static string GetDisplayName(this {typeSymbol.Name} @enum) {{ {GenerateExtensionMethodBody(typeSymbol)} }} }} }}"; } private static string GenerateExtensionMethodBody(ITypeSymbol typeSymbol) { var sb = new StringBuilder(); sb.Append(@"return @enum switch { "); foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>()) { var displayAttribute = fieldSymbol.GetAttributes() .FirstOrDefault(attributeData => string.Equals(attributeData.AttributeClass?.Name, "DisplayAttribute", StringComparison.Ordinal)); if (displayAttribute is null) { sb.AppendLine( $@" {typeSymbol.Name}.{fieldSymbol.Name} => ""{fieldSymbol.Name}"","); } else { var displayAttributeName = displayAttribute.NamedArguments .FirstOrDefault(x => string.Equals(x.Key, "Name", StringComparison.Ordinal)) .Value; sb.AppendLine( $@" {typeSymbol.Name}.{fieldSymbol.Name} => ""{displayAttributeName.Value}"","); } } sb.Append( @" _ => throw new ArgumentOutOfRangeException(nameof(@enum)) };"); return sb.ToString(); }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: SourceGeneratorTests-part3.zip
سؤال: چگونه میتوان کدهای تولید شدهی توسط یک Source Generator را ذخیره کرد؟
Source Generators به صورت پیشفرض هیچ فایلی را بر روی دیسک سخت ذخیره نمیکنند و تمام عملیات آنها در حافظه انجام میشود. اگر علاقمند به مطالعهی این خروجیهای خودکار، به صورت فایلهای واقعی هستید، نیاز به انجام تغییرات زیر در فایل csproj پروژهی مصرف کنندهی Source Generator است:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup> <Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)"> <RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" /> </Target> <ItemGroup> <!-- Exclude the output of source generators from the compilation --> <Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" /> <Content Include="$(CompilerGeneratedFilesOutputPath)/**" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup> </Project>
- EmitCompilerGeneratedFiles سبب ثبت فایلهای خودکار تولید شده، بر روی دیسک سخت میشود که قالب مسیر پیشفرض ذخیره سازی آن به صورت زیر است:
{BaseIntermediateOutpath}/generated/{Assembly}/{SourceGeneratorName}/{GeneratedFile}
- چون این فایلهای cs. جدید ثبت شدهی بر روی دیسک سخت، مجددا وارد پروسهی کامپایل میشوند و خود Source Generator هم یک نمونهی از آنهارا پیشتر به کامپایلر معرفی کردهاست، برنامه دیگر به علت وجود اطلاعات تکراری، کامپایل نخواهد شد. به همین جهت نیاز است تا قسمت Compile Remove فوق را نیز معرفی کرد تا کامپایلر از پوشهی Generated تنظیمی، صرفنظر کند.
- اطلاعات موجود در پوشهی Generated، فقط یکبار تولید میشوند؛ صرفنظر از اطلاعات موجود در حافظه که همیشه به روز است. به همین جهت اگر میخواهید نمونههای به روز شدهی آنها را نیز بر روی دیسک سخت داشته باشید، نیاز به قسمت RemoveDir تنظیمی وجود دارد.