Makale Özeti

Enterprise Library sitilinde yeni bir uygulama bloğu oluşturalım. Aspx page işlemlerinin anımsatacak bir mesaj işlem süreci uygulama bloğu yazıyoruz. Bu uzun makalenin sonuna kadar sabır edebilirseniz kendi Enterprise Library kütüphanenizi yazabilirsiniz.

Makale

Bir önce ki bölümle Enterprise Library için yeni Provider’lar nasıl eklendiğini inceledik. Ayrıca eklediğimiz yeni providerların Enterprise Library Configuration ile birlikte nasıl kullanıldığını da inceledik. Şimdi Enterprise Library sitilinde yeni bir uygulama bloğu oluşturalım. Aspx page işlemlerinin anımsatacak bir mesaj işlem süreci uygulama bloğu yazıyoruz. Bu uzun makalenin sonuna kadar sabır edebilirseniz kendi Enterprise Library kütüphanenizi yazabilirsiniz.

Mesaj İşlem Süreci Uygulama Bloğu

Muhtemel bir uygulama senaryosu kullanacağız. Bir mesajlaşma uygulama bloğu oluşturacağız. Müşterileriniz sizden bilgi talebinde bulunur. Daha sonra her talep işlenir ve müşteriye geri dönüş yapılacak mesajlar oluşturulur ve müşteriye cevap dönülür. Yazacağımız uygulama bloğu aslında Aspx page işlemlerinin bir benzeri. Uygulama bloğumuz her bir müşterimiz için Request -> Calculate -> Response döngüsü içinde mesaj trafiğini yönetmeli. Uygulama bloğumuz sadece mesaj saklama ve mesaj işlem sınıfları arasında mesajları hareket ettirmeden sorumlu olacak. Bizim uygulama bloğumuzu kullanan projeler her bir mesaj işleme adım için bir veya daha fazla sayıda kendi mesaj işlem sınıfları kullanacak. Hemen konuyu soyut olmaktan kurtaralım.
<MessageProcessApplicationBlock>
<ProcesserProviders>
<add type=" MessageProcessApplicationBlock.ProcessProvider" name="Test Message Process 1">
<Workers>
  <add ExecuteTime="60" MessageQPath=".\Private$\colorQ" State="Calculate" WorkerType="ColorCalculaterWorker, ….."
    type="MessageProcessApplicationBlock.WorkerProvider,…. "
    name="ColorWorker" />
  <add ExecuteTime="60" MessageQPath=".\Private$\errorQ" State="Error"
    WorkerType="ErrorWorker, ….."
    type="MessageProcessApplicationBlock.WorkerProvider, ……"
    name="Error" />
  <add ExecuteTime="60" MessageQPath=".\Private$\intQ" State="Calculate"
    WorkerType="IntCalculater….."
    type="MessageProcessApplicationBlock.WorkerProvider, ……"
    name="IntWorker" />
  <add ExecuteTime="10" MessageQPath=".\Private$\requestQ" State="Request"
    WorkerType="RequestWorker,……"
    type="MessageProcessApplicationBlock.WorkerProvider……"
    name="Request" />
  <add ExecuteTime="10" MessageQPath="Private$\responseQ" State="Response"    WorkerType="ResponseWorker,……"
    type="MessageProcessApplicationBlock.WorkerProvider, ……"
    name="Response" />
</Workers>
</add>
</ProcesserProviders>
</MessageProcessApplicationBlock>
Yukarıda ki xml ile göreceğiniz gibi yazmayı amaçladığımız uygulama bloğu her bir müşteri için bir mesajlaşma işlemi açmaktadır. Örneğimizde “Test Message Process 1” ile açılan mesajlaşma işlemi beş adet mesaj işlem sınıfı eklentisine sahiptir. Uygulama bloğumuza mesaj işlem sınıflarını eklerken mesaj işlem sınıfı ile birlikte bir mesaj kuyruğu ve mesaj işlem sınıfının çalışma aralığını da belirtmekteyiz.
<add ExecuteTime="10" MessageQPath=".\Private$\requestQ" State="Request"
    WorkerType="RequestWorker,……"
    type="MessageProcessApplicationBlock.WorkerProvider……"
    name="Request" />
Uygulama bloğu tüm mesaj kuyruklarını, mesajları, mesaj işlemci sınıfları ve mesaj işlemci sınıflarının çalışma zaman aralıklarını yönetmektedir. Mesaj işlemci sınıfları ise sadece kendisine verilen mesajı işlemekle yetinmektedir. Uygulama bloğu her ExecuteTime sürecinde şu şekilde çalışacaktır: 1) Mesaj kuyruğu üzerinde ki tüm mesajlar bitene kadar 2. Adımı tekrar et 2) Mesaj kuyruğunda en üste ki mesajı çek 2)a. Mesaj işlem sınıfına işlemek üzere gönder 2)b. Eğer mesaj işlem sınıfı sonuç mesajı döndürdü ise 2)b.i. Yeni sonuç mesajını bir sonra ki “State “ değerine sahip mesaj işlem sınıfı kuyruğuna ekle 2)b.ii. Eğer ekleyecek bir mesaj işlem kuyruğu bulanamazsa “Error State” özellikli mesaj kuyruğuna ekle Böylelikle nasıl bir uygulama bloğu yazacağımızı planladık. Şimdi sıra geldi uygulama bloğumuzu oluşturmaya.

New Application Block

Önce projemizi oluşturalım.



 

Şimdi uygulama bloğumuzu temel işlemlerini gösteren temel sınıfı ve bu soyut temel sınıfı yapılandırma dosyası üzerinde ki verilere göre üretecek factory sınıfını oluşturalım.



 

Temel sınıf hiçbir ekstra özelliğe sahip değildir. Sadece üst sınıfın adını vermekteyiz. Bu adım ile bizim için birçok sınıf oluşturulmaktadır.

 

 IProcesserProvider bizim uygulama bloğumuzun temel işlevlerini gösteren ara yüzdür. ProcessProviderFactory ise config dosyadan IProcesserProvider ara yüzünü uygulayan ProcesserProvider nesnelerini üreten sınıftır. Mesaj işleme sürecinin çok basit iki fonksiyonu vardır: Start, Stop
namespace MessageProcessApplicationBlock {
/// <summary>
/// Defines the basic functionality of an ProcesserProvider
/// </summary>
[ConfigurationNameMapper(typeof(ProcesserProviderDataRetriever))]
[CustomFactory(typeof(ProcesserProviderCustomFactory))]
public interface IProcesserProvider {
    /// <summary>
    /// Mesaj işleme sürecini başlat
    /// </summary>
    void Start();

    /// <summary>
    /// Mesaj işleme sürecini bitir
    /// </summary>
    void Stop();
}
}
Daha sonradan yazacağımız tüm uygulama bloğu provider sınıflarının atası olacak soyut ProcesserProvider sınıfı IProcesserProvider ara yüzünü basit bir uygulamasıdır.
namespace MessageProcessApplicationBlock {
/// <summary>
/// Abstract implementation of the <see cref="IProcesserProvider"/> interface.
/// </summary>
public abstract class ProcesserProvider : IProcesserProvider {
    #region IProcesserProvider Members

    public abstract void Start();

    public abstract void Stop();

    #endregion
}
}
Şimdi mesaj işleme sürecini temsil edecek olan ProcessProvider sınıfı oluşturalım.



 

Süreci temsil edecek olan ProcessProvider sınıfı uygulama bloğumuza ait temel sınıflardan türetilmektedir.

Assembler

Burada durup işin mutfağına küçük bir göz atalım. Uygulama bloğun yazarken en önemli sorun config üzerinde ki xml node elemanları ile kendi uygulama sınıflarımızı oluşturma sorunudur. Injection Pattern’i hatırlayınız. Injection Pattern ile config üzerinde ki xml node bilgilerinden sınıflarımızı oluşturabiliriz. Enterprise Library’de config üzerinde ki xml node bilgilerinden gerçek sınıflar oluşturmak için Injection Pattern kullanmaktadır. Data(xml node) sınıfları ile gerçek sınıflar arası dönüşümleri IAssembler ara yüzünü uygulayan sınıflar yapmaktadır. AssemblerBasedObjectFactory sınıfı Factory sınıflarımızın temel sınıfıdır. AssemblerBasedObjectFactory nesne oluşturmak için önce oluşturulacak nesneye ait Assembler sınıfı bulur. Daha sonra oluşturulacak nesneye ait Assembler sınıfından nesneyi oluşturması ister.
public abstract class AssemblerBasedObjectFactory<TObject, TConfiguration>
    where TObject : class
    where TConfiguration : class
{……………………
public virtual TObject Create(IBuilderContext context, TConfiguration objectConfiguration, IConfigurationSource configurationSource, ConfigurationReflectionCache reflectionCache)
{
IAssembler<TObject, TConfiguration> assembler = GetAssembler(objectConfiguration);
    TObject createdObject = assembler.Assemble(context, objectConfiguration, configurationSource, reflectionCache);
    return createdObject;
}}
Assembler sınıflar ABSF tarafından bizim için oluşturulmaktadır. Sadece tek bir fonksiyona sahiptir. Assemble fonksiyonu önce oluşturulacak nesneye ait yapılandırma bilgisini alır ve nesne için uygun yapılandırma bilgisine çevirir. Daha sonra bu yapılandırma verisi ile nesneyi oluşturur.
IAssembler<MessageProcessApplicationBlock.IProcesserProvider, MessageProcessApplicationBlock.Configuration.ProcesserProviderData> {

public MessageProcessApplicationBlock.IProcesserProvider Assemble(IBuilderContext context, MessageProcessApplicationBlock.Configuration.ProcesserProviderData objectConfiguration, IConfigurationSource configurationSource, ConfigurationReflectionCache reflectionCache) {

    ProcessProviderData castObjectConfiguration
        = (ProcessProviderData)objectConfiguration;

    ProcessProvider createdObject
        = new ProcessProvider(castObjectConfiguration);

    return createdObject;
}
}

EnterpriseLibraryFactory

Peki nesneler nerede saklanmaktadır. Injection yapacak Builder nerededir? Tüm nesneler static EnteriseLibraryFactory sınıfı içinde ki Builder nesnesi ile oluşturulmaktadır. ConfiguredObjectStrategy özel Factory sınıfına sahip nesneleri kendi factory sınıfı ile oluşturmaktadır. Böylelikle bizim Assembler sınıflarımız EnterpriseLibraryFactory tarafından fark edilmekte ve yazdığımız provider’lar kendilerine ait assembler sınıfları yardımı ile oluşturulmaktadırlar.
public static class EnterpriseLibraryFactory
{
private static IBuilder<BuilderStage> builder;
private static ConfigurationReflectionCache reflectionCache = new ConfigurationReflectionCache();
static EnterpriseLibraryFactory()
{
    builder = new BuilderBase<BuilderStage>();
builder.Strategies.AddNew<ConfigurationNameMappingStrategy>(BuilderStage.PreCreation);
builder.Strategies.AddNew<SingletonStrategy>(BuilderStage.PreCreation);
builder.Strategies.AddNew<ConfiguredObjectStrategy>(BuilderStage.PreCreation);
builder.Strategies.AddNew<InstrumentationStrategy>(BuilderStage.PostInitialization);
}
…………..
public static T BuildUp<T>(IReadWriteLocator locator, string id, IConfigurationSource configurationSource)
{
if (string.IsNullOrEmpty(id))
    throw new ArgumentException(Resources.ExceptionStringNullOrEmpty, "id");
if (configurationSource == null)
    throw new ArgumentNullException("configurationSource");
return GetObjectBuilder().BuildUp<T>(locator, id, null, GetPolicies(configurationSource));
}
}

public class ConfiguredObjectStrategy : EnterpriseLibraryBuilderStrategy
{
……..
public override object BuildUp(IBuilderContext context, Type t, object existing, string id)
{
…..
ICustomFactory factory = GetCustomFactory(t, reflectionCache);
if (factory != null)
{
    existing = factory.CreateObject(context, newId, configurationSource, reflectionCache);
} else{….}
….
return base.BuildUp(context, t, existing, newId);
}
}
Böylelikle Enterprise Library kütüphanesinde ki ve ABSF ile yazdığımız kendi uygulama bloğumuz içinde ki tüm nesneler EnterpriseLibraryFactory sınıfı tarafında yukarıda gösterildiği şekilde üretilmektedir.

WorkerProvider

Mesaj işleme sürecini oluşturduk. Şimdi süreç içerisinde her bir mesaj işleme adımını yerine getirecek olan WorkerProvider sınıfımızı oluşturalım.

 

 ProcessProvider sınıfı birçok WorkerProvider sınıfa ihtiyaç duymaktadır. Süreci yöneten ProcessProvider süreç içinde her bir mesaj işleme adımını sahibi olduğu WorkerProvider elemanlarına yaptırmaktadır. Öncelikle ProcessProviderData sınıfına sürec işci sınıflarını ekleyelim.
[Assembler(typeof(ProcessProviderAssembler))]
public class ProcessProviderData : MessageProcessApplicationBlock.Configuration.ProcesserProviderData {
……
private const string workersProperty = "Workers";
// konfigürasyon versine Worker sınıflarını ekle
[ConfigurationProperty(workersProperty)]
public NamedElementCollection<WorkerProviderData> Workers {
    get {
        return (NamedElementCollection<WorkerProviderData>)this[workersProperty];
    }
}        
}

public class ProcessProviderAssembler : IAssembler<…..> {
public MessageProcessApplicationBlock.IProcesserProvider Assemble(…….) {
ProcessProviderData castObjectConfiguration
          = (ProcessProviderData)objectConfiguration;
ProcessProvider createdObject
= new ProcessProvider(castObjectConfiguration);
// konfigürasyonda yer alan Worker sınıflarıne gerçek sınıfa cevir
foreach (WorkerProviderData workerData in castObjectConfiguration.Workers) {
            WorkerProvider worker = new WorkerProvider(workerData);
            createdObject.Workers.Add(worker);
            worker.ProcessWorkers = createdObject.Workers;
}
return createdObject;
}
}

[ConfigurationElementType(typeof(ProcessProviderData))]
public class ProcessProvider : MessageProcessApplicationBlock.ProcesserProvider {
public ProcessProvider(ProcessProviderData configuration) {
    _workers = new List<WorkerProvider>();
}
private List<WorkerProvider> _workers;
// konfigürasyonda yer alan Worker sınıflarında taşıyan liste
internal IList<WorkerProvider> Workers {
    get {
        return _workers;
    }
}

public override void Start() {
    foreach (WorkerProvider worker in Workers) {
        worker.Start();
    }
}

public override void Stop() {
    foreach (WorkerProvider worker in Workers) {
        worker.Stop();
    }
}
Böylelikle konfigurasyon dosyasında her bir Process xml node içinede “Workers” node açmış olduk. Workers node içerisinde WorkerProvider node’larını barındırmaktadır. Konfigurasyon sınıfına eklediğimiz “Workers” node’a karşılık olarak ProcessProvider sınıfına “Workers” listesini ekledik. Konfigürasyon ile gerçek sınıflar arasında dönüşümü sağlayan ProviderAssembler sınıfında konfigürasyonda ki “Workers” node bilgisinden gerçek WorkerProvider nesneleri ürettik ve üretilen WorkerProvider nesnelerini ProcessProvider nesnesi üzerinde ki Workers listesine ekledik. Artık mesaj işlem sürecini yöneten ProcessProvider sınıfımız hazır. Şimdi mesaj işlem adımlarını yerine getirecek WorkerProvider sınıfmızı tamamlamalıyız. WorkerProvider sınıfı mesaj işleme adımının çalışma periyodunu ve mesaj kuyruğu işlemlerini yönetmektedir.Mesaj işleme adımında mesajı değerlendirecek ve sonuç mesajı üretecek olan IWorker arayüzünü uygulayan sınıflardır. IWorker sınıfları sürec yönetimini düşünmeden sadece mesajı alır işler ve sonuç mesaj üretir.
/// <summary>
/// mesaj işleme arayüzü
/// </summary>
public interface IWorker {
/// <summary>
/// <see cref="message"/> bu işlemci tarafında işlenebilir mi testini yapar
/// </summary>
/// <param name="message">test edilecek mesaj nesnesi</param>
/// <returns><see cref="message"/> işlenebilirse true değilse false</returns>
bool CanExecute(object message);

/// <summary>
/// <see cref="message"/> nesnesini işle ve sonuc mesajı döndür
/// </summary>
/// <param name="message">işlenmesi gereken mesaj</param>
/// <returns>eğer sonuc mesaj varsa return edilmeli yoksa null</returns>
object Execute(object message);

/// <summary>
/// yeni bir mesaj oluştu uygun mesaj kuyruğunu bul ve kuyruğa ekle
/// </summary>
/// <remarks>
/// Her hangi bir mesaj kuyruk üzerine eklenmesi gerekiyorsa bu event çağrılır.
/// Event MessageProcessApplicationBlock tarafında handle edilmektedir.
/// Mesajı alır ve CanExecute true olan WorkerProvider'ın mesaj kuyruğune ekle
/// </remarks>        
event EventHandler<MessageEventArgs> AddMessageQ;

/// <summary>
/// Her executetime sürecinde bir defa çağırılır
/// </summary>
void Check();

/// <summary>
/// Mesaj kuyruğunda her hangi bir değişiklik olduğu zaman 
/// istemciyi uyarmak için kullanılır
/// </summary>
/// <param name="messages">kuyruk üzerinde ki tüm mesajlar</param>
void OnMessageQChanged(Message[] messages);
}
WorkerProvider dışarıdan IWorker arayüzünü uygulayan nesneyi almaktadır. Ayrıca mesaj kuyruğunu ve çalışma periyotlarını bilmesi gerekmektedir.
[Assembler(typeof(WorkerProviderAssembler))]
public class WorkerProviderData : MessageProcessApplicationBlock.Configuration.ProcesserProviderData {
………
 [ConfigurationProperty(executeTimeProperty, IsRequired = true, DefaultValue = 30)]
public int ExecuteTime {  get {…}  set {…}}
…
[ConfigurationProperty(messageQPathProperty, IsRequired = true, DefaultValue = "")]
public string MessageQPath {  get {…}  set {…}}

[ConfigurationProperty(stateProperty, IsRequired = true, DefaultValue = MessageState.None)]
public MessageState State {  get {…}  set {…}}

[ConfigurationProperty(workerTypeProperty, IsRequired = true)]
public string WorkerTypeName {  get {…}  set {…}}

public Type WorkerType {
    get {
        return (Type)typeConverter.ConvertFrom(WorkerTypeName);
    }
    set {
        WorkerTypeName = typeConverter.ConvertToString(value);
    }
}
}
WorkerProviderData sınıfına bilmemiz gereken tüm verileri ekledik.WorkerProivderData sınıfı çalışma zamanı periyotunu, mesaj kuyruğunu ve IWorker arayüzünü uygulayan sınıfı almaktadır. Uygulama bloğumuzun asıl işini oluşturan WorkerProvider sınıfını yazalım.
[ConfigurationElementType(typeof(WorkerProviderData))]
public class WorkerProvider : MessageProcessApplicationBlock.ProcesserProvider {
private Timer _timer;
private MessageQ _messageQ;
private IWorker _workerClient;
private IList<WorkerProvider> _workers;
private MessageState _state;

public WorkerProvider(WorkerProviderData configuration) {
    _state = configuration.State;
    _timer = new Timer();
    _timer.Interval = configuration.ExecuteTime * 1000;
    _timer.Elapsed += new ElapsedEventHandler(_timer_Elapsed);
    _messageQ = new MessageQ(configuration.MessageQPath);
    _workerClient = (IWorker)Activator.CreateInstance(configuration.WorkerType);
    _workerClient.AddMessageQ += delegate(object sender, MessageEventArgs e) {
        AddMesssageQ((MessageState)(_state + 1), e.Message);
    };
    _messageQ.MesssageQChanged += delegate(object sender, MessageQEventArgs e) {
        _workerClient.OnMessageQChanged(e.Messages);
    };
}

internal IList<WorkerProvider> ProcessWorkers {
    set {
        _workers = value;
    }
}

……
void _timer_Elapsed(object sender, ElapsedEventArgs e) {
    Message message;
    object resultMessage;
    while (_messageQ.MessageCount > 0 && _timer.Enabled) {
        message = _messageQ.Recive();
        resultMessage = _workerClient.Execute(message.Body);
        if (resultMessage != null)
            AddMesssageQ((MessageState)(_state + 1), resultMessage);
    }
    if (_timer.Enabled)
        _workerClient.Check();

}

private void AddMesssageQ(MessageState nextState, object message) {
    if (message != null) {
        if (nextState == MessageState.None) {
            nextState = (MessageState)(_state + 1);
        }
        foreach (WorkerProvider worker in _workers) {
            if (worker.State == nextState && worker.Worker.CanExecute(message)) {
                worker.MessageQ.Send(this, message);
                System.Threading.Thread.Sleep(1000);
                return;
            }
        }
        AddErrorQ(message);
    }
}
……….

public override void Start() {
    _timer.Start();
}
public override void Stop() {
    _timer.Stop();
}
}
İşte bu kadar! Mesaj işleme süreci uygulama bloğumuzu tamamladık. Aşağıda ki diyagramda görüldüğü gibi mesaj işlem süreci uygulama bloğumuz IProcesserProvider ile belirtilen işleri yerine getiren bir uygulam bloğudur. IProcesserProvider arayüzüne ait işleri özet ProcesserProvider sınıfı uygulamaktadır. Ata sınıfımız olan ProcesserProvider sınıfını uygulayan ProcessProvider mesaj işlem adımlarını yerine getirecek olan bir çok WorkerProvider sınıfına sahiptir. WorkerProvider sınıfı süreç kontrollerini yerine getirirken mesaj işleme işini IWorker arayüzünü uygulayan eklenti sınıfından beklemektedir.

 

Bir program ile uygulama bloğumuzu deneyelim. Bizim için rasgele mesajlar üreten bir RequestWorker sınıfı yazalım. Bu sınıf Color, Int ve String tipinde mesajlar oluştursun. ColorWorker, IntWorker ve StringWorker sınıfları bu mesajları alsın ve string tipine dönüştürüp ResponseWorker sınıfına versin. ResponseWorker sınıfı her ExecuteTime ile kendi mesaj kuyruğunda ki tüm mesajları silsin. Tüm mesaj kuyrukları girdler ile ekranda gösterelim ve ne olup bittiğini ekran üzerinden takip edelim.
public class RequestWorker : IWorker{
public RequestWorker() {
}

#region IWorker Members

public bool CanExecute(object message) {
// request worker hiçbir mesajı q üzerine kabul etmez
return false;
}

public object Execute(object message) {
// q üzerinde mesaj olmadı için mesajları işlemez
return null;
}

public event EventHandler<MessageEventArgs> AddMessageQ;

public void Check() {
// her çalışma zamanında random renkler, sayılar ve stringler üretir ve MessageQ üzerine ekletir            
Random random = new Random();
random.Next();
int randomColorCount = random.Next(1,5);
Color color;
int type;
for (int i = 0; i < randomColorCount; i++) {
    lock (this) {
        type = random.Next(1, 4);
        switch (type) {
            case 1:
                color = ColorTranslator.FromWin32(random.Next());
                AddMessageQ(this, new MessageEventArgs(color));
                break;
            case 2:
                AddMessageQ(this, new MessageEventArgs(random.Next()));
                break;
            case 3:
                AddMessageQ(this, new MessageEventArgs(random.Next(10000, 1000000000).ToString("X")));
                break;
            case 4:
                AddMessageQ(this, new MessageEventArgs(random.Next(1, 1000) / random.Next(2, 488)));
                break;
        } 
    }

    
}            
}

public void OnMessageQChanged(Message[] messages) {
// mesaj kuyruğu değişti ekran üzerinde göster
Program.MainForm.SetRequestMessages(messages);
}

#endregion
}


public class IntCalculater : IWorker{
public IntCalculater() {
}
#region IWorker Members

public bool CanExecute(object message) {
    return message is int;
}

public object Execute(object message) {
    return message.ToString();
}

public event EventHandler<MessageEventArgs> AddMessageQ;

public void Check() {
    
}
public void OnMessageQChanged(Message[] messages) {
    Program.MainForm.SetIntMessages(messages);
}
#endregion

}

Konfigürasyon üzerinde makalenin en başında ki gibi  tanımlanmış sürec bilgilerine ekleyelim. Artık tüm bu süreci başlatmak ve dururmak gerçekten çok kolay. 

public partial class Form1 : Form, IMainForm {
…..
IProcesserProvider _processer = null;
private void _btnStart_Click(object sender, EventArgs e) {
if (_btnStart.Text == StartString) {
    _processer = ProcesserProviderFactory.CreateProcesserProvider("Test Message Process 1");
    _processer.Start();
    _btnStart.Text = StopString;
} else {
    _processer.Stop();
    _btnStart.Text = StartString;
}
}
Bir sonra ki makalede konfigürasyon dosyasına kendi uygulama bloğumuzu elle eklemek yerine, ABSF kullanarak Visual Stidio icerisinde ki Enterprise Library Configuration tool ile nasıl eklenildiğini inceliyeceğiz.

Emre Coşkun

http://www.emrecoskun.net/