Makale Özeti

Harici bir .NET Assembly dosyasını embedded resource olarak uygulamanıza eklemeyi ve tek başına çalışan bilen exe assemblyleri elde etmek için bir yöntemi anlatıyoruz.

Makale

Harici Assembly dosyalarını embedded resource olarak kullanmak

.NET uygulamaları geliştirirken .NET Framework Base Class Library (BCL) kütüphanelerini projelerimize referans ekleyerek kullanırız ve bu kütüphaneleri projemizi dağıtırken dağıtmamız yani istemci bilgisayarlara kopyalamamıza gerek yoktur, zira örneğin System.Net.Dll dosyasını referans ekleyip kullandığınızda sadece bu dosyayı kullanan kendi proje dosyalarınızı uygulamanızın çalışacağı ortama kopyalamanız gerekir.

Ancak bir şekilde edindiğiniz .NET uygulamalarının tek bir dosya olarak dağıtıldığına şahit olmuşsunuzdur. Eğer .NET Framework BCL dışında kullanılan tüm sınıflar sizin tarafınızdan yazılmış ise, ya da açık kaynak kodlu bir kütüphane desteği alıyorsanız ve bu harici kütüphanenin lisanslama modeli kaynak kodları kendi projenize dahil etmenizi ve bu şekilde derleyip dağıtmanızı engellemiyor ise sorun yok tek bir dosyadan oluşan ve kolay dağıtılabilir bir projenizi olmasını sağlayabilirsiniz.

Ancak harici bir kütüphane kullanıyorsanız ve bunu kaynak kod olarak değil bir dll olarak kullanmak durumundaysanız. Projenizi tek bir dosyadan oluşacak şekilde dağıtma şansı elde edebilirmisiniz? Cevap; evet yapılabilir. Nasıl?

.NET Framework Runtime bir assembly (genellikle bir dll’dir) dosyasını yüklerken sizin yazdığınız kod ile yükleme işlemine müdahale etmenize izin verir. Bir event handler’a (AppDomain.CurrentDomain.AssemblyResolve) register olduktan sonra yükleme sırasında kendi kodunuzu çalıştırabilirsiniz. Tabi AppDomain.CurrentDomain.AssemblyResolve eventine programınız çalışır çalışmaz register olmanız gerekir. Event Handler methodunu register etmenizden önce ilgili typelardan birisine erişilmeye çalışılırsa kendiniz yükleme işlemine müdahale edemezsiniz.

Peki tek dosya olarak deploy etmek için nasıl bir yükleme yöntemi geliştirebilirsiniz. İlgili DLL dosyalarını geliştirme anında projenize standart yöntemler  ile referans ekler kullanırsınız. Ancak aynı zamanda bu şekilde dağıtmak istediğiniz dll dosyalarını projenize embedded resource olarak eklersiniz ve custom assembly load yaptığınız noktada çalışmakta alan exe dosyasınıza gömülü olan resource dosyasını okur ve hiç diske kaydetmeden assembly olarak yüklersiniz.

Şimdi bu senaryoyu bir örnek proje ile adım adım gerçekleştirelim.

Örnek bir harici kütüphane olarak  SharpZipLib (http://www.icsharpcode.net/OpenSource/SharpZipLib) kütüphanesini seçtim. Proje gereği bu kütüphaneyi kullanan bir windows uygulamam var. Büyük bir yazılım ürünün client tarafındaki bazı yapılandırmalarını yapmak için kullanacağım küçük bir tool geliştiriyorum ve bu tool da aldığı sıkıştırılmış biçimli yapılandırma dosyasını okuyup client sistemini ana uygulamamız için hazır hale getiriyor.

Yeni bir Windows Application projesi oluşturdum. ICSharpCode.SharpZipLib kütüphanesini Add Reference diyerek referanslara ekledim. Uygulama açılınca çalıştırılacak forma bir buton ekleyip sharpziplib kütüphanesini kullanarak ilgili işlemleri yapan kodu yazdım.

static void Main()

{

Application.Run(new Form1());

}

namespace ClientSetupTool

{

    public partial class Form1 : Form

    {

        public Form1()

        {

            InitializeComponent();

        }

        private void button1_Click(object sender, EventArgs e)

        {

            ICSharpCode.SharpZipLib.Zip.FastZip fz = new ICSharpCode.SharpZipLib.Zip.FastZip();

            //sharpziplib kütüphanesinden bir type'i kullanarak sharpziplib kütüphanesinin

            //.net runtime'i tarafından yüklenmeye çalışılmasını sağlamak için yazılmış

            //fonksiyonel olmayan bir method

        }

    }

}

 

Projeyi bu şekilde derleyip çalıştırdığımda hiç bir sorun (tür yükleme hatası  vs) olmayacaktır.

Uygulamanın çalışacağı bilgisayara esas uygulamamız (ClientSetupTool.exe) ve yanından sharpziplib kütüphanesi (ICSharpCode.SharpZipLib.dll) kopyalanarak çalıştırılmalıdır. Ancak ICSharpCode.SharpZipLib.dll dosyası gönderilmez ve ClientSetupTool.exe tek başına istemci bilgisayara yüklenir ve çalıştırılırsa aşağıdaki hata mesajı alınacaktır.

System.IO.FileNotFoundException: Could not load file or assembly 'ICSharpCode.SharpZipLib, Version=0.85.5.452, Culture=neutral, PublicKeyToken=1b03e6acf1164f73' or one of its dependencies. The system cannot find the file specified.

File name: 'ICSharpCode.SharpZipLib, Version=0.85.5.452, Culture=neutral, PublicKeyToken=1b03e6acf1164f73'

 

Bu durumda biz sharpzip kütüphanesinin dll dosyasını projemize embedded resource olarak ekleyelim.

Ben projeye DLLResources  adında yeni bir klasör ekledim. Add > Existing Item seçeneği ile ICSharpCode.SharpZipLib.dll dosyasını ekledim. ICSharpCode.SharpZipLib.dll dosyasını seçip klavyeden F4 tuşuna basara dosyanın özelliklerini gösterecek şekilde Properties penceresine geçtim. Buradan Build Action seçeneğini Embedded Resource olarak seçtim.

Artık projemiz derlendiği zaman binary olarak ICSharpCode.SharpZipLib.dll dosyası ClientSetupTool.exe içerisine dahil edilecek.

Bir Assembly yüklemesini .Net Runtime yapamadığı zaman (aynı klasörde,GAC’da aradıktan sonra) AssemblyResolve metodunu çağırır. Bu sebeble projemiz derlendiğinde ICSharpCode.SharpZipLib.dll dosyasının derleme output klasörüne düşmemesi gerekir. Bunun için reference altında ICSharpCode.SharpZipLib.dll dosyasını seçin F4 ile properties penceresine geçin ve copy local seçeneğini false yapın. Bu durumda .Net Runtime ICSharpCode.SharpZipLib.dll kütüphanesini bulamayacak ve AssemblyResolve ile işi bize devredecektir. Referansın ekli kalmasının sebebi derleme anında compilerin dll dosyasını bulabilmesi ve projenin derlenebilmesi içindir.

using System;

using System.Collections.Generic;

using System.Windows.Forms;

namespace ClientSetupTool

{

    static class Program

    {

        static Dictionary<string, System.Reflection.Assembly> yuklenenAssemblyList

            = new Dictionary<string, System.Reflection.Assembly>();

 

        [STAThread]

        static void Main()

        {

            AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

            Application.EnableVisualStyles();

            Application.SetCompatibleTextRenderingDefault(false);

            Application.Run(new Form1());

        }

        static System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)

        {

            System.Reflection.AssemblyName asmName = new System.Reflection.AssemblyName(args.Name);

            System.Reflection.Assembly asm = null;

            if (!yuklenenAssemblyList.TryGetValue(asmName.Name, out asm))

            {

                string resourceName = "ClientSetupTool.DLLResources." + asmName.Name + ".dll";

                System.IO.Stream dllStream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName);

                byte[] asmData = new System.IO.BinaryReader(dllStream).ReadBytes((int)dllStream.Length);

                dllStream.Close();

                asm = System.Reflection.Assembly.Load(asmData);

                yuklenenAssemblyList[asmName.Name] = asm;

            }

            return asm;

        }

    }

}

 

Program.cs içerisinde Main metodu içerisine aşağıdaki satırı ekleyerek .NET Runtime yükleyemediğinde assembly yüklemesi için bizim methodumuzn çalışmasını sağlıyoruz. Eğer uygulamanızı çalıştırırken aynı klasörde sharpziplib (yada siz hangi dll i embedded olarak kullanacaksanız) dll dosyasını uygulama ile aynı klasöre koyarsanız .net runtime otomatik oradan yükleyecektir ve sizin methodunuz çalışmayacaktır.

AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve

 

CurrentDomain_AssemblyResolve metodu ise gayet açık aslında, ilgili assembly dosyasını bulur (kaynağımız burada embedded resource)  Assembly.Load methodu ile yükler ve geriye döner. args.Name parametresi ile o an yüklenmeye çalışılan assembly adını öğreniyoruz ve zaten yüklediklerimizi tuttuğumuz dictionary listemizden kontrol ettikten sonra getmanifestresourcestream methodu ile exe içine gömülü resource u stream olarak okuyoruz ve Assembly.Load ile yüklüyoruz. Kodun içinde ki typelerın namespacelerini using ile eklemek yerine uzun uzun yazdım tylerin BCL’de ki yerlerini görebilmeniz için.

Uygulamamızı sadece ClientSetupTool.exe olarak istemci bilgisayarlara yükleyerek çalıştırabiliriz. Bu şekilde aslında harici bir dll dosyasını kullanan uygulamamızı bir zip dosyaları vs gibi dağıtmak yerine gayet basit bir şekilde tek dosya, kopyala, yapıştır, çalıştır şeklinde çalıştırılabilir olmasını sağladık.

Uygulamalarınızda faydası olabileceğini düşündüğüm bu yöntemi manuel resource eklemek yerine veya büyük bir ekipte her yazılımcıya bu disiplini kazandırmak yerine continuous integration sürecinizde de derlenmmiş bir assembly dosyasına başka bir assembly dosyasını (harici kütüphaneniz) embedded resource olarak ekleyebilirsiniz.

 

Cengiz Han

cengiz@cengizhan.com