Categories: .NET

Plug-in Tabanlı Uygulama Geliştirme ve Hesap Makinesi Örneği

Neden plug-in tabanlı uygulama geliştirmeye ihtiyaç duyarız?

Biz yazılımcıların karşılaştıkları en büyük sorunlardan birisi, bitirmiş olduğumuz bir uygulamanın üzerine ek bir özellik istenmesidir. Şahsen bu beni delirten bir şey. 🙂
İşte bu tarz durumların önüne geçmemizi sağlayan etmenlerden birisidir plug-in tabanlı uygulama geliştirme. Bu yoldaki en büyük dostumuz Reflection namespace’idir.

Kısaca Reflection’dan bahsetmek gerekirse nesneler ile ilgili çalışma zamanında nesneye ait property ve metotlara erişebilmemizi ve metotları çağırabilmemizi sağlar.

Şimdi konumuza Hesap Makinesi örneğimizle devam edelim.

Projemiz kısaca: İşlem fonksiyonları plug-in olarak tasarlanacak bir hesap makinesi olacaktır.

Öncelikle projemiz 3 parçadan oluşacaktır.

  • Contracts (Hesap makinemize plug-in geliştirirken Application Domain’ler arası plug-in komutlarımız için ana interface tanımlamamız)
  • MainAppicationDomain (Hesap makinesi uygulamamız ve Plug-in’leri çalıştıracak olan proje yer alacak)
  • PlugInApplicationDomain (Eklenmiş olan plug-in’leri tarayıp kendi ApplicationDomainin’de instance’larını türetip MainApplicationDomain’imize gönderecek olan PlugInEngine projemiz yer alacak)

Öncelikle Contract’ımız için Contracts isminde yeni bir klasör ekleyip içerisine GG.PlugInTabanliHesapMakinesi.Contracts isimli bir ClassLibrary projesi ekliyorum. İçerisine ICalculationCommand isimli bir interface tanımlıyorum.

namespace GG.PlugInTabanliHesapMakinesi.Contracts
{
    /// <summary>
    /// Hesap makinemiz için plug-in tabanlı olarak geliştireceğimiz hesaplama fonksiyonlarımız için ApplicationDomain'ler arası contract'ımız.
    /// </summary>
    public interface ICalculationCommand
    {
        /// <summary>
        /// Hesaplama fonksiyon adı
        /// </summary>
        string Name { get; }

        /// <summary>
        /// 2 argümanlı hesaplama fonksiyonumuz için execute edecek metotumuz
        /// </summary>
        /// <param name="arg1"></param>
        /// <param name="arg2"></param>
        /// <returns></returns>
        double Execute(double arg1, double arg2);
    }
}

Evet contract’ımız hazır durumda.
Şimdi sıra geldi PlugInApplicationDomain‘imizi oluşturmaya.

Yine öncelikle projemize PlugInApplicationDomain isminde bir klasör daha ekleyerek içerisine GG.PlugInTabanliHesapMakinesi.PlugInEngine isimli bir ClassLibrary projesi daha ekleyerek GG.PlugInTabanliHesapMakinesi.Contracts projemizi referans olarak ekliyorum.

Bu proje plug-in’lerimiz tarayıp ICalculationCommand contract’ımızdan inherit alan assembly’lerimizi bulup instance’lerini üretip MainAppicationDomain ‘imize gönderecek olan class’ımız yer alacaktır.

GG.PlugInTabanliHesapMakinesi.PlugInEngine projemizin içerisine CalculationCommandCommunicator isimli bir class ekliyorum ve MarshalByRefObject, ICalculationCommand interface’lerinden türetiyorum.

Bu class’ımızın amacı: Geliştireceğimiz olan plug-in’lerin sarmalanıp serialize hale getirilip (MarshalByRefObject aracılığı ile) farklı bir ApplicationDomain‘de instance’sının ve metotlarının kullanılmasını sağlayacaktır.

İşin özünde orjinal nesnemizmiş gibi davranan Transparent Proxy‘ler var aslında. Bu sayede uygulamamız için Plug-in geliştiricilerimiz extradan MarshalByRefObject‘den türeyen nesnelere ihtiyaç duymayacaktır sadece Contract’ımız yeterli olacaktır. Aksi durumda onlara contract’ımız haricinde MarshalByRefObject’den de türetmelerini söylemek pekte hoş bir durum olmayacaktır, eminim. 🙂

Şimdi class’ımıza bir göz atalım:

using GG.PlugInTabanliHesapMakinesi.Contracts;
using System;

namespace GG.PlugInTabanliHesapMakinesi.PlugInEngine
{
    /// <summary>
    /// Plug-in'lerimizi serialize hale getirecek class'ımız.
    /// </summary>
    public class CalculationCommandCommunicator : MarshalByRefObject, ICalculationCommand
    {
        #region Constructor
        /// <summary>
        /// CalculationCommand türündeki gerçek nesnemiz.
        /// </summary>
        private ICalculationCommand _RealCommand;
        public CalculationCommandCommunicator(ICalculationCommand realCommand)
        {
            this._RealCommand = realCommand;
        }
        #endregion

        #region Properties
        /// <summary>
        /// Hesaplama fonksiyon adı
        /// </summary>
        public string Name { get { return this._RealCommand.Name; } }
        #endregion

        #region Public Methods
        /// <summary>
        /// 2 argümanlı hesaplama fonksiyonumuz için execute edecek metotumuz
        /// </summary>
        /// <param name="arg1"></param>
        /// <param name="arg2"></param>
        /// <returns></returns>
        public double Execute(double arg1, double arg2)
        {
            return this._RealCommand.Execute(arg1, arg2);
        }

        public override string ToString()
        {
            return this.Name;
        }
        #endregion
    }
}

CalculationCommandCommunicator class’ımızıda tamamlamış olduk.

Şimdi sıra geldi plug-in’lerimizi bulup instance’larını türetip bir liste halinde MainApplicationDomain’imize gönderecek olan PlugInEngine class’ımızı oluşturmaya.

using GG.PlugInTabanliHesapMakinesi.Contracts;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace GG.PlugInTabanliHesapMakinesi.PlugInEngine
{
    /// <summary>
    /// Plug-in'lerimizi bulup instance'larını türetip geyire dönderecek olan class'ımız.
    /// </summary>
    public class PlugInEngine : MarshalByRefObject
    {
        public List<ICalculationCommand> LoadPlugInCommands()
        {
            var commandList = new List<ICalculationCommand>(); // Plug-in'lerimizin instance'larını ekleyeceğimiz listemiz
            string basePath = AppDomain.CurrentDomain.BaseDirectory + "PlugIns"; // Plug-in'lerimizi bulmak için tarıyacağımız ana PlugIns path'imiz.

            foreach (var filePath in Directory.GetFiles(basePath, "*.dll")) // GetFiles metotuna yazdığımız "*.dll" patterni ile ilgili path'imizdeki assembly'lerimizi buluyoruz
            {
                var loadedAssembly = Assembly.LoadFile(filePath); // Reflection namespace'si altındaki Assembly sınıfı ile ilgili assembly'imizi yüklüyoruz
                var calculationTypes = loadedAssembly.GetTypes().Where(t => typeof(ICalculationCommand).IsAssignableFrom(t)); // ICalculationCommand'dan inherit alan nesneleri buluyoruz

                foreach (var calculationType in calculationTypes)
                {
                    var cmd = Activator.CreateInstance(calculationType); // Instance'sini üretiyoruz
                    commandList.Add(new CalculationCommandCommunicator(cmd as ICalculationCommand)); // Ürettiğimiz instance'ımızı hazırlamış olduğumuz bizim için serialize hale getirecek olan CalculationCommandCommunicator ile sarmalıyor ve commandList'imize ekliyoruz
                }
            }

            return commandList;
        }
    }
}

PlugInEngine sınıfımızıda MarshalByRefObject’den türeterek hazırlamış olduk.

Şimdi sıra geldi şu bahsedip durduğumuz meşhur MainApplicationDomain’imizi oluşturmaya. 🙂 Hemen MainApplicationDomain isminde bir klasör daha ekleyerek içerisine GG.PlugInTabanliHesapMakinesi.PlugInStarter isimli bir ClassLibrary projesi daha ekliyorum. GG.PlugInTabanliHesapMakinesi.Contracts ve GG.PlugInTabanliHesapMakinesi.PlugInEngine projelerini referans olarak eklemeyide unutmayalım. 🙂
Bu proje plug-in’lerimiz için olan PlugInApplicationDomain ‘imizi bazı yetkilerle oluşturacak ve tanımlı plug-in’leri hesap makinesi uygulamamıza dahil edecektir.

Hemen GG.PlugInTabanliHesapMakinesi.PlugInStarter isimli library’mize PlugInStarter isimli bir class ekliyorum ve kodlara bir göz atalım:

using GG.PlugInTabanliHesapMakinesi.Contracts;
using System;
using System.Collections.Generic;
using System.Security;
using System.Security.Permissions;

namespace GG.PlugInTabanliHesapMakinesi.PlugInStarter
{
    /// <summary>
    /// Ana hesap makinesi uygulamamız içinde plug-in'lerimizi çalıştıracak olan class'ımız.
    /// </summary>
    public static class PlugInStarter
    {
        public static List<ICalculationCommand> Start()
        {
            #region Create AppDomain
            var setUp = new AppDomainSetup();
            setUp.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;

            // Kısıtlı ve güvenli olarak üretiyoruz. (CAS)
            var permissionSet = new PermissionSet(PermissionState.None);
            permissionSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution)); // Çalıştırabilmek için yetki veriyoruz
            permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.PathDiscovery, AppDomain.CurrentDomain.BaseDirectory)); // Sadece gerekli klasörümüze keşife izin veriyoruz
            permissionSet.AddPermission(new FileIOPermission(FileIOPermissionAccess.Read, AppDomain.CurrentDomain.BaseDirectory + "PlugIns")); // Sadece gerekli klasörümüze okuma izni veriyoruz

            var plugInApplicationDomain = AppDomain.CreateDomain("Plug In App Domain", null, setUp, permissionSet); // Gerekli bilgileri vererek domain ismi, kurulum bilgisi ve güvenlik izinleri gibi domainimizi üretiyoruz
            #endregion

            // Diğer application domain'imizde PlugInEngine'mizi üretip bu objenin bize ObjectHandle bilgisini geriye döner.
            var plugInEngine = (PlugInEngine.PlugInEngine)plugInApplicationDomain.CreateInstanceAndUnwrap(typeof(PlugInEngine.PlugInEngine).Assembly.FullName, typeof(PlugInEngine.PlugInEngine).FullName);

            return plugInEngine.LoadPlugInCommands();
        }
    }
}

PlugInStarter class’ımızıda hazırlamış olduk.

Şimdi sıra geldi yine MainApplicationDomain klasörü içerisinde UI katmanımızı hazırlamaya. Hemen bir GG.PlugInTabanliHesapMakinesi.WinUI isminde bir Windows Application projesi ekliyorum GG.PlugInTabanliHesapMakinesi.Contracts ve GG.PlugInTabanliHesapMakinesi.PlugInStarter projelerini referans olarak ekleyip UI formunu aşağıdaki şekilde tasarlıyorum.


Sayı 1 ve Sayı 2 kısımlarına işlem yapacağımız arg1 ve arg2 gelecek, sağ tarafdaki ListBox’a ise Command’larımız gelecek yani plug-in’lerimiz.

Hemen ilgili formumuzun kod tarafına bir göz atalım:

using GG.PlugInTabanliHesapMakinesi.Contracts;
using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace GG.PlugInTabanliHesapMakinesi.WinUI
{
    public partial class frmMain : Form
    {
        public frmMain()
        {
            InitializeComponent();

            // PlugInStarter plug-inlerimizi ilgili application domain'de instance'larını üretip bize geriye dönüyor.
            List<ICalculationCommand> cmdList = PlugInStarter.PlugInStarter.Start();
            lstCommands.Items.AddRange(cmdList.ToArray());
        }

        private void btnHesapla_Click(object sender, EventArgs e)
        {
            if (lstCommands.SelectedItem != null)
            {
                var cmd = lstCommands.SelectedItem as ICalculationCommand;
                var arg1 = Convert.ToDouble(txtSayi1.Text);
                var arg2 = Convert.ToDouble(txtSayi2.Text);
                var result = cmd.Execute(arg1, arg2);

                MessageBox.Show(string.Format("Sonuç: {0}", result));
            }
        }
    }
}

İşte bu kadar. PlugInStarter aracılığı ile ilgili plug-in’lerimiz listemize ekleniyor ve Execute metotumuz aracılığı ile kullanılabilir bir hale geliyor. Uygulamamız neredeyse tamam. 🙂 Geriye sadece örnek bir plug-in tasarlamak kaldı. Hiç fonksiyonsuz bir hesap makinesi işimize yaramayacaktır galiba. 😛

Şimdi solution’umuza PlugIns isimli bir klasör daha ekleyerek ABCSirketi.ToplamaPlugIn isimli bir ClassLibrary daha ekliyorum. (Sanki başka bir firma tarafından geliştirilmiş gibi bir plug-in :))

İlgili plug-in’i geliştirebilmeleri için dağıttığımız Contract‘ımız referans olarak ekleniyor. Hemen ToplamaIslemi isminde bir class oluşturuyorum ve ICalculationCommand interface’mizden türetiyorum.

Kodlarımıza bakmadan önce ilgili ABCSirketi.ToplamaPlugIn projemizin derlendiğinde output klasörünü çalışmamız sırasında kolaylık olması açısından PlugInEngine’imizin plug-in’lerimizi bulmak için baktığı GG.PlugInTabanliHesapMakinesi.WinUI klasörü altındaki bin/PlugIns yolunu output klasörü olarak belirliyorum.


Plug-in’imizin kodlarına şimdi bir bakalım:

using GG.PlugInTabanliHesapMakinesi.Contracts;

namespace ABCSirketi.ToplamaPlugIn
{
    /// <summary>
    /// ABCSirketi tarafından hazırlanan toplama plug-in'i.
    /// </summary>
    public class ToplamaIslemi : ICalculationCommand
    {
        // Plug-in'imizin ismi
        public string Name
        {
            get { return "Toplama"; }
        }

        public double Execute(double arg1, double arg2)
        {
            return arg1 + arg2; // Plug-in'imizin toplama işlevi
        }
    }
}

İşte bu kadar.
Artık ToplamaPlugIn’imizi derlediğimizde ilgili assembly’miz GG.PlugInTabanliHesapMakinesi.WinUI altındaki bin/PlugIns yoluna derlenecektir.

Artık plug-in tabanlı hesap makinemiz çalışmaya hazır! Ben çalıştırıyorum ve ilgili toplama işlemini deniyorum 🙂

Görüldüğü gibi uygulamamız başarıyla çalışıyor plug-in tabanlı. 🙂
Plug-in tabanlı uygulama geliştirmede kendimi geliştirmemde çok faydası olan Sayın Mustafa Tahir Çakmak hocamada teşekkürü bir borç bilirim. 🙂

Bir sonraki makalede görüşmek dileğiyle.

İlgili proje’nin çalışır hali ektedir.

GG.PlugInTabanliHesapMakinesi

 

Gökhan Gökalp

View Comments

  • Plug-in tabanlı uygulama geliştirmeyle alakalı ne zamandır düzgün bir örnek bulamıyordum. Ellerinize gökhan bey mükemmel bir yazı olmuş. Devamını bekliyoruz sitenizin takipçisiyim artık

Recent Posts

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Policy Enforcement-Automated Governance with OPA Gatekeeper and Ratify) – Part 2

{:tr} Makalenin ilk bölümünde, Software Supply Chain güvenliğinin öneminden ve containerized uygulamaların güvenlik risklerini azaltabilmek…

6 months ago

Securing the Supply Chain of Containerized Applications to Reduce Security Risks (Security Scanning, SBOMs, Signing&Verifying Artifacts) – Part 1

{:tr}Bildiğimiz gibi modern yazılım geliştirme ortamında containerization'ın benimsenmesi, uygulamaların oluşturulma ve dağıtılma şekillerini oldukça değiştirdi.…

8 months ago

Delegating Identity & Access Management to Azure AD B2C and Integrating with .NET

{:tr}Bildiğimiz gibi bir ürün geliştirirken olabildiğince farklı cloud çözümlerinden faydalanmak, harcanacak zaman ve karmaşıklığın yanı…

1 year ago

How to Order Events in Microservices by Using Azure Service Bus (FIFO Consumers)

{:tr}Bazen bazı senaryolar vardır karmaşıklığını veya eksi yanlarını bildiğimiz halde implemente etmekten kaçamadığımız veya implemente…

2 years ago

Providing Atomicity for Eventual Consistency with Outbox Pattern in .NET Microservices

{:tr}Bildiğimiz gibi microservice architecture'ına adapte olmanın bir çok artı noktası olduğu gibi, maalesef getirdiği bazı…

2 years ago

Building Microservices by Using Dapr and .NET with Minimum Effort – 02 (Azure Container Apps)

{:tr}Bir önceki makale serisinde Dapr projesinden ve faydalarından bahsedip, local ortamda self-hosted mode olarak .NET…

2 years ago