Aspect Oriented Programming

aspect-oriented-programming

Aspect Oriented Programming bir uygulama içerisindeki authorization, logging, caching, exception handling, validation gibi cross-cutting concern sayılan işlevlerin ayrıştırılmasıyla modülariteyi arttırmayı hedefleyen bir yaklaşımdır.

Geleneksel katmanlı mimarilerde bir işlem gerçekleştirilirken genellikle yanında başka yan işlemler de gerçekleştirilir. İlgili işlem authorization gerektiriyorsa yetki kontrolü; caching gerektiriyorsa önbelleğe yazma ve okuma; ekleme, güncelleme işlemlerinde validation kontrolü, exception durumlarının yönetilmesi gibi işlemler yapılmaktadır. Bu işlemlerin her biri cross-cutting concern olarak adlandırılmaktadır.

cross-cutting-concerns

Kesişen ilgiler en basit tanımıyla uygulama katmanları arasında aynı işleve sahip kod bloklarının farklı noktalarda kendisini tekrarlayacak şekilde kullanılması olarak açıklanabilir. Yukarıdaki görsel incelendiğinde kesişen ilgilerin katman bağımsız şekilde doğrudan uygulamayı kestiği ve her katman tarafından kullanılabileceği görülmektedir.

İlgili konuya örnek teşkil edecek methoda göz atalım.

public IResult Get(int id)
{
    try
    {
        if (!_authService.CanPerform("articles.view"))
            return ErrorResult("You don't have permission.");

        if (!_cacheService.TryGetValue($"article-{id}", out Article article))
        {
            article = _articleDal.Get(id);
            _cacheService.Set($"article-{id}", article);
        }

        return SuccessResult(article);
    }
    catch (Exception ex)
    {
        _logger.LogException($"Exception occurred. Message: {ex.Message}");
        return ErrorResult("An error occurred.");
    }
}

Yukarıdaki örnek incelendiğinde yapılan işlemin yalnızca ilgili makaleyi getirmek olduğu görülmektedir. Buna rağmen işlemin yanında başka işlem ve kontroller de yapılmakta ve okunabilirlik düşmektedir.

Burada dikkat edilmesi gereken husus örnek içerisinde OOP ihlali olmadığıdır. Gerekli soyutlamalar yapılmış ve her sınıf kendi işlemlerinden sorumlu olacak şekilde tasarlanmıştır. Bu noktada Aspect Oriented Programming yaklaşımı Separation of Concern prensibiyle kesişen ilgilerinden birbirlerinden ayrılmaları gerektiğini belirtmektedir.

[AuthAspect("articles.view")]
[CacheAspect(Duration = 60)]
[ExceptionAspect]
public IResult Get(int id)
{
    return SuccessResult(_articleDal.Get(id));
}

AOP yaklaşımı uygulanarak kesişen ilgilerin birer Aspect olarak ele alınmasıyla okunabilirliğin ne ölçüde arttığı görülmektedir.

AOP yaklaşımı Interception ve IL Weaving olmak üzere iki farklı şekilde uygulanmaktadır. Interception yönteminde methodlar çalışma zamanında intercept edilerek araya girerken, IL Weaving yöntemindeyse Aspect kodları derleme zamanında ilgili methodların içerisine gömülür. Bu sebeple Interception yöntemine nazaran daha performanslıdır. PostSharp en yaygın kullanılan NuGet paketidir.

Bu yazıda Interception yöntemi kullanılarak AOP uygulanacaktır. Uygulamaya diğer katmanlar tarafından kullanılacak temel katmanı oluşturarak başlayalım.

dotnet new classlib -n Core
dotnet add package Castle.Core

Sınıf ve methodları işaretlememizi sağlayacak soyut Attribute sınıfımızı gerekli Castle.Core NuGet paketini kurarak yazalım.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public abstract class AttributeBase : Attribute
{
    public int Priority { get; set; }
}

İş katmanında ilgili methodlar hem method hem de class düzeyinde bu attribute üzerinden türetilen sınıflar ile işaretlenecektir. Intercept işlemini gerçekleştirecek temel abstract sınıf tanımlamasını da yapalım.

public abstract class InterceptorBase<TAttribute> : IInterceptor where TAttribute : AttributeBase
{
    protected virtual void OnBefore(IInvocation invocation, TAttribute attribute) { }
    protected virtual void OnAfter(IInvocation invocation, TAttribute attribute) { }
    protected virtual void OnException(IInvocation invocation, Exception ex, TAttribute attribute) { }
    protected virtual void OnSuccess(IInvocation invocation, TAttribute attribute) { }

    public void Intercept(IInvocation invocation)
    {
        var attribute = GetAttribute(invocation.MethodInvocationTarget, invocation.TargetType);
        if (attribute is null) invocation.Proceed();
        else
        {
            var succeeded = true;
            OnBefore(invocation, attribute);
            try
            {
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                succeeded = false;
                OnException(invocation, ex, attribute);
                throw;
            }
            finally
            {
                if (succeeded) OnSuccess(invocation, attribute);
            }
            OnAfter(invocation, attribute);
        }
    }

    private TAttribute? GetAttribute(MethodInfo methodInfo, Type type)
    {
        var attribute = methodInfo.GetCustomAttribute<TAttribute>(true);
        if (attribute is not null) return attribute;
        return type.GetTypeInfo().GetCustomAttribute<TAttribute>(true);
    }
}

Görüldüğü üzere abstract interceptor sınıfı IInterceptor arayüzünü implemente etmektedir. Bu arayüz ile gelen IInvocation tipinde parametre alan Intercept methodu ezilmiştir. İlgili methodlar bu method üzerinden intercept edilecektir. IInvocation parametresi üzerinde tanımlı olan Proceed methoduyla intercept edilen methodlar işletilmektedir. Dolayısıyla bu method öncesi ve sonrasına yazılacak kodlar duruma göre ya öncesinde ya da sonrasında çalıştırılacaktır.

Proceed methodu çağırılmaksızın methodun davranışına ReturnValue property’siyle müdahale edilerek geri dönüş de aşağıdaki gibi sağlanabilmektedir.

invocation.ReturnValue = new Book();

Aynı zamanda temel interceptor sınıfının generic olduğuna dikkat edin. Öncesinde tanımladığımız AttributeBase türünden bir tip alabilmektedir. Bu şekilde hem interceptor hem de attribute sınıfları birbirine bağlanmaktadır. GetAttribute methoduyla ilgili attribute instance elde edilerek interceptor sınıfına aktarılmaktadır. Öncelikle method düzeyinde işaretlenme durumuna sonrasında da sınıf düzeyine bakılmaktadır.

Aspect tanımlamasına başlayabiliriz. Öncelikle attribute sınıf tanımlamasını yapalım.

public class PerformanceAttribute : AttributeBase
{
    public int Interval { get; set; }
}

Görüldüğü üzere sınıfımız AttributeBase üzerinden türemektedir. Interceptor sınıfımıza geçebiliriz.

public class PerformanceInterceptor : InterceptorBase<PerformanceAttribute>
{
    private readonly ILogger<PerformanceInterceptor> _logger;
    private readonly Stopwatch _stopwatch;

    public PerformanceInterceptor(ILogger<PerformanceInterceptor> logger)
    {
        _logger = logger;
        _stopwatch = new Stopwatch();
    }

    protected override void OnBefore(IInvocation invocation, PerformanceAttribute attribute)
    {
        _stopwatch.Start();
    }

    protected override void OnAfter(IInvocation invocation, PerformanceAttribute attribute)
    {
        if (_stopwatch.Elapsed.TotalMilliseconds > attribute.Interval)
        {
            _logger.LogInformation($"{invocation.Method.Name} elapsed {_stopwatch.Elapsed.TotalSeconds} second(s).");
        }
        _stopwatch.Stop();
    }
}

Oluşturduğumuz temel sınıflar sayesinde bir Aspect tanımlamak yukarıdaki kadar basittir. Yukarıda intercept edilen methodların çalışma sürelerinin belirli koşullar doğrultusunda loglanması işlemi yapılmaktadır. İhtiyaç duyulan servisler Dependency Injection yoluyla elde edilmektedir. Interception işleminin attribute ve interceptor olarak ikiye ayrıştırılmalarının sebebi budur. Interceptor bir attribute olarak kullanılsaydı bu özellikten faydalanılamayacaktı.

Bu katman içerisinde kullanılan servisleri built-in IoC container içerisine ekleyecek methodumuzu tanımlayalım.

public static IServiceCollection ConfigureCore(this IServiceCollection services)
{
    services.AddSingleton<IProxyGenerator, ProxyGenerator>();
    return services.AddTransient<InterceptorBase<PerformanceAttribute>, PerformanceInterceptor>();
}

İş katmanımızı oluşturalım ve Core katmanını referans olarak ekleyerek devam edelim.

dotnet new classlib -n Business
dotnet add reference ../Core

İş katmanında kullanılacak olan ilgili servis tanımlamalarını yapalım.

public interface IPostService
{
    Post Get(string id);
    List<Post> Get();
}

public class PostManager : IPostService
{
    public Post Get(string id) => _repository.Get(id);

    [Performance(Interval = 100)]
    public List<Post> Get() => _repository.Get();
}

Yukarı tanımlanan servis içerisinde geriye liste dönen methodun işaretlendiğine dikkat edin. Bu işaretleme method düzeyinde olabileceği gibi sınıf düzeyinde de olabilir. Sınıf düzeyinde olması durumunda tüm methodlar için geçerli olacaktır.

Bu katman içerisinde kullanılan servisleri IoC container içerisine ekleyecek methodlarımızı tanımlayalım.

public static IServiceCollection ConfigureBusiness(this IServiceCollection serviceCollection)
{
    return serviceCollection.AddScopedWithInterception<IPostService, PostManager>();
}

Yukarıda kullanılan AddScopedWithInterception methodu önemlidir. Attribute, Interceptor ve Method bağlantıları bu method aracılığıyla yapılmaktadır.

public static IServiceCollection AddInterceptedScoped<TInterface, TImplementation>(this IServiceCollection serviceCollection) where TInterface : class where TImplementation : class, TInterface
{
    serviceCollection.AddScoped<TImplementation>();
    serviceCollection.AddScoped(typeof(TInterface), serviceProvider =>
    {
        var proxyGenerator = serviceProvider.GetRequiredService<IProxyGenerator>();
        var implementation = serviceProvider.GetRequiredService<TImplementation>();

        var interceptors = serviceProvider.GetInterceptors<TImplementation>();

        var options = new ProxyGenerationOptions() { Selector = new InterceptorSelector<TImplementation>() };

        return proxyGenerator.CreateInterfaceProxyWithTarget<TInterface>(implementation, options, interceptors);
    });
    return serviceCollection;
}

private static IInterceptor[]? GetInterceptors<TImplementation>(this IServiceProvider serviceProvider) where TImplementation : class
{
    var classAttributes = typeof(TImplementation).GetCustomAttributes(typeof(AttributeBase), true).Cast<AttributeBase>();
    var methodAttributes = typeof(TImplementation).GetMethods().SelectMany(s => s.GetCustomAttributes(typeof(AttributeBase), true).Cast<AttributeBase>());

    var attributes = classAttributes.Union(methodAttributes).OrderBy(o => o.Priority);

    var interceptors = attributes.Select(f => serviceProvider.GetRequiredService(typeof(InterceptorBase<>).MakeGenericType(f.GetType()))).Cast<IInterceptor>();

    return interceptors.ToArray();
}

Yukarıda CreateInterfaceProxyWithTarget methoduyla talep edilen servisi kapsayacak bir proxy oluşturulmaktadır. İlgili servis içerisinde kullanılan attribute listesi üzerinden interceptor listesi GetInterceptors extension methoduyla elde edilmektedir. Öncelikle sınıf düzeyinde sonrasında method düzeyinde tanımlanmış attribute listesi elde edilerek ilişkili interceptor listesi oluşturularak proxy oluşturulmaktadır. Sonrasındaysa ProxyGenerationOptions ile yalnızca kullanılan interceptor seçimi yapılmaktadır.

public class InterceptorSelector<TImplementation> : IInterceptorSelector where TImplementation : class
{
    public IInterceptor[] SelectInterceptors(Type type, MethodInfo methodInfo, IInterceptor[] interceptors)
    {
        var classAttributes = type.GetTypeInfo().GetCustomAttributes(typeof(AttributeBase), true).Cast<AttributeBase>().ToList();

        var methodParameterTypes = methodInfo.GetParameters().Select(s => s.ParameterType).ToArray();
        var concreteMethod = typeof(TImplementation).GetMethod(methodInfo.Name, methodParameterTypes);
        if (concreteMethod is not null)
        {
            var methodAttributes = concreteMethod.GetCustomAttributes<AttributeBase>(true).Cast<AttributeBase>();
            classAttributes.AddRange(methodAttributes);
        }

        var interceptorList = new List<IInterceptor>();
        foreach (var item in classAttributes.OrderBy(o => o.Priority))
        {
            var baseType = typeof(InterceptorBase<>).MakeGenericType(item.GetType());

            var interceptor = interceptors.FirstOrDefault(a => a.GetType().IsAssignableTo(baseType));
            if (interceptor is not null) interceptorList.Add(interceptor);
        }
        return interceptorList.ToArray();
    }
}

Artık sunum aşamasına geçilerek API katmanı oluşturulabilir.

dotnet new webapi -n Api
dotnet add reference ../Business

Oluşturduğumuz konfigürasyon methodlarını Program.cs içerisine ekleyelim.

var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureCore();
builder.Services.ConfigureBusiness();

Controller sınıfını oluşturarak testimizi gerçekleştirebiliriz.

[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
    private readonly IPostService _postService;

    public PostsController(IPostService postService)
    {
        _postService = postService;
    }

    [HttpGet("{id}")]
    public IActionResult Get(string id) => Ok(_postService.Get(id));

    [HttpGet]
    public IActionResult Get() => Ok(_postService.Get());
}

Yazı içerisinde kullanılan proje dosyalarına GitHub üzerinden erişebilirsiniz. Proje içerisinde ayrıca BenchmarkDotNet kütüphanesini kullanan bir console uygulaması da mevcuttur. Api projesini iki farklı port kullanarak ayağa kaldıralım.

 dotnet run -c Release  --urls=https://localhost:5001
 dotnet run -c Release  --urls=https://localhost:5002

Ardından Performance projesini de çalıştırarak performans testi gerçekleştirelim. Böylece AOP ile normal yaklaşım arasındaki farka göz atılabilir.

dotnet run -c Release

Uygulama çalıştıktan sonra verdiği sonuçlara göz atarsak benchmark testinden sonra pek bir fark olmadığı görülecektir.

|      Method |     Mean |    Error |   StdDev | Allocated |
|------------ |---------:|---------:|---------:|----------:|
|      Normal | 286.9 ms | 55.57 ms | 162.1 ms | 112.11 KB |
| Intercepted | 295.0 ms | 56.53 ms | 163.1 ms | 112.31 KB |

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir