Umut Özel    About    Archive    Feed

C# Source Generators

C# harika bir dil, yıllar geçtikçe de gönlümdeki yerini gittikçe daha sağlamlaştırdı. Geliştirme yaparken en keyif aldığım ikinci dil, TypeScript’ten sonra :)

Ancak yıllardır biz C# hayranlarının özellikle Javacı çocukları kıskandığımız bir bir eksiği var. Gerçek dile entegre bir AOP (Aspect Oriented Programming) desteği.

Bakın Java ile neler yapabiliyorlar:

@Loggable(Loggable.DEBUG)
public String load(URL url) {
  return url.openConnection().getContent();
}

Metod her çağırıldığında kayıt tutmak için esas kodların arasına ekleme yapmak gerekmiyor, Loggable Aspect’i bu işi derleme aşamasında ürettiği kodlar ile halledecek.

Peki bizim elimiz kolumuz bağlı mıydı?

Bizim de çözümlerimiz vardı, yıllarca PostSharp kullandık. Çalıştığım bazı yerlerde parasını ödedik, bazılarında ücretsiz yetenekleriyle idare etmeye çalıştık.

[NotifyPropertyChanged]
public class CustomerViewModel
{
   [Required]
   public Customer Customer { get; set; }

   public string FullName { get { return this.Customer.FirstName + " " + this.Customer.LastName; } }
}

INotifyPropertyChanged arayüzü otomatik uygulanıyor. Burada Required gibi çalışma zamanı bir başkasının kontrolüne ihtiyaç duyan özelliklerin AOP olmadığını vurgulamak lazım.

Daha önce dediğim gibi .Net bize hazır bir AOP platformu sağlamıyor. PostSharp nasıl çalışıyor peki? Kendisini derleme aşamasında MSBuild ile entegre ederek tabii. MSBuild .Net 2.0 zamanından beri kodlarımızı derleyen araç, Make, Gradle falan gibi. Daha sonrasında ise .Net için yazdığımız kodların derlendiği hedef ara dil olan MSIL seviyesinde kod üretimi yapıyor.

C#, Turbo Pascal ve Delphi’nin babası Anders Hejlsberg‘in PostSharp’a verdiği röportajında C# ve AOP sorusuna Cesedimi çiğnemeniz lazım şeklindeki cevabı herhalde PostSharp hissesi aldı diye düşünmemize sebep olmuştu.

Diğer bir alternatif ise, benim AOP olarak kabul etmemekte ısrar ettiğim DI (Dependency Injection) Interception. Yani bağımlılıklarımızı kodlarımıza sağlayan Windsor gibi bir aracın çalışma zamanında bize istediğimiz objeyi vermeden önce araya girip kod üretmesi. Biliyorsunuz çalışma zamanı kod üretmek pek de zor değil.

Burada bir kaç sorun var:

  • Çalışma zamanı yük getiriyoruz
  • DI Container tarafından sağlanmayan yerlerde kullanamıyoruz
  • Private üyelere müdahale edemiyoruz

Mono.Cecil üzerine oturan Fody‘yi unutmayalım. Kabaca PostSharp’ın yaptığı işi biraz daha uğraşarak yapabiliyoruz. Xml ve Xsd oluşturmak gibi.

<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
  <xs:element name="Weavers">
    <xs:complexType>
      <xs:all>
        <xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
          <xs:complexType>
            <xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="EventInvokerNames" type="xs:string">
              <xs:annotation>
                <xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="CheckForEquality" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
              </xs:annotation>
            </xs:attribute>
            <xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
              <xs:annotation>
                <xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
              </xs:annotation>
            </xs:attribute>

    ... böyle gidiyor

Bu arada PostSharp’ın Fody’ye Gold Sponsor olduğunu şimdi farkettim, helal olsun.

Özetle AOP yapabiliyoruz ama Microsoft bize Java rahatlığını sunmuyor.

Source Generators

Source Generator devrimsel Roslyn derleyiciye gelen yeni bir eklenti. Bu sayede derleme aşamasında dinamik olarak yeni C# kodu oluşturabiliyoruz. Yeni burada kalın harfle, çünkü varolan kodları değiştiremiyoruz. Microsoft’un tanımıyla:

A Source Generator is a piece of code that runs during compilation and can inspect your program to produce additional files that are compiled together with the rest of your code

Baştan hayalleri baltalayalım, bize daha önce yapamadığımız bir yetenek kazandırmıyor Source Generator. Ancak entegre IDE desteği sayesinde tasarım aşamasında IntelliSense imkanları (şimdilik Visual Studio restart etmek gerekebilir) ve daha hızlı ve güvenli kod üretimi sağlıyor. Kullanabilmek için .Net 5 Preview ve Visual Studio Preview kurmamız gerekiyor.

Microsoft’a göre Source Generator kullanabileceğimiz bazı alanlar:

  • XAML, Asp.Net gibi projelerde code-behind dosyaları oluşturmak
  • ToString, Equals, GetHashCode gibi tekrarlayan sıkıcı kodları oluşturmak
  • Burası çokomelli, Reflection ağırlıklı Asp.Net Controller taraması gibi çalışma zamanı açılışta zaman harcayan indeksleme işlerini halletmek

Source Generator Böyle çalışıyormuş

Hello World!

Bir örnek görelim.

Amacımız Source Generator paketimizi kurmuş kullanıcıların çağırabileceği ve tasarım aşamsında görebileceği bir metod eklemek. Kullanımı aşağıdaki gibi olacak (burası kullanıcı projesi).

public class SomeClassInMyCode
{
    public void SomeMethodIHave()
    {
        HelloWorldGenerated.HelloWorld.SayHello(); // calls Console.WriteLine("Hello World!") and then prints out syntax trees
    }
}

Şimdi de Source Generator projemizi yapalım. Proje dosyamızı Preview kullanacak şekilde ayarlayalım.

<Project Sdk="Microsoft.NET.Sdk">

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

  <PropertyGroup>
    <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="3.6.0-3.20207.2" PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0-beta2.final" PrivateAssets="all" />
  </ItemGroup>

</Project>

Generator sınıfımızı oluşturalım.

namespace SourceGeneratorSamples
{
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {
            // kullanıcıların kodlarına eklenecek kısım
            var sourceBuilder = new StringBuilder(@"
using System;
namespace HelloWorldGenerated
{
    public static class HelloWorld
    {
        public static void SayHello() 
        {
            Console.WriteLine(""Hello from generated code!"");
            Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"");
");

            // syntax tree listesini alıyoruz
            var syntaxTrees = context.Compilation.SyntaxTrees;

            // derlemeye dahil her dosyayı ekrana yazdıracak kodu ekliyoruz
            foreach (SyntaxTree tree in syntaxTrees)
            {
                sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");");
            }

            sourceBuilder.Append(@"
        }
    }
}");

            // oluşturduğumuz kodu derlemeye ekliyoruz
            context.AddSource("helloWorldGenerator", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
        }

        public void Initialize(InitializationContext context)
        {
        }
    }
}

Source Generator’ü projemize bir analyzer olarak eklememiz gerekiyor.

<PropertyGroup>
  <LangVersion>preview</LangVersion>
</PropertyGroup>

<ItemGroup>
    <ProjectReference Include="path-to-sourcegenerator-project.csproj" 
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
</ItemGroup>

Yapılan işler Roslyn ile uğraşanlara yabancı gelmeyecek

Artık yukarıdaki kullanıcı projesi Source Generator metodunu çağırabilecek ve ekrana önce “Hello World!” sonra da derlemeye dahil olan dosyaların listesi basılacak.

Yapabileceklerimiz hayal gücümüz ile sınırlı. Aklıma ilk gelen WPF zamanlarında çok haşır neşir olduğumuz INotifyPropertyChanged arayüzünü sınıflara otomatik uygulamak oldu. Tüh sadece yeni dosyalar ekleyebiliyoruz bu tür işleri yapamayız diye düşünürken Microsoft örneklerinde partial sınıflar ile hallettiklerini gördüm, partial tamamen aklımdan çıkmış :)

Daha neler neler yapılır incelemek isterseniz hemen Tarif Kitabı‘na göz atın. Ayrıca github üzerinde de örnekler var.

Her geçen gün daha çok gelişen bu araçlar ile kodlama yapmak gerçekten çok keyifli!

Mutlu kodlamalar!