- خصیصههای ثابت: یعنی همه کاراکترهای A دارای یک شکل مشخص هستند. در واقع مشخصات ذاتی آبجکت میباشند.
- خصیصههای پویا: یعنی هر کاراکتر دارای فونت، سایز و رنگ خاص خود است. در واقع خصیصههایی که از یک آبجکت به آبجکت دیگر متفاوت هستند .
public interface IAlphabet { void Render(string font);//Define Extrinsic and non-static states for each object } public class A : IAlphabet { public void Render(string font) { Console.WriteLine(GetType().Name + " has font of type " + font); } } public class B : IAlphabet { public void Render(string font) { Console.WriteLine(GetType().Name + " has font of type " + font); } }
public class FlyWeightFactory { private readonly Dictionary<string, IAlphabet> _dictionary = new Dictionary<string, IAlphabet>(); public int Count { get { return _dictionary.Count; } } public IAlphabet GetObject(string name) { if (!_dictionary.ContainsKey(name)) switch (name) { case "A": _dictionary.Add(name, new A()); Console.WriteLine("New object created"); break; case "B": _dictionary.Add(name, new B()); Console.WriteLine("New object created"); break; default: throw new Exception("Factory can not create given object"); } else Console.WriteLine("Object reused"); return _dictionary[name]; } }
FlyWeightFactory flyWeightFactory = new FlyWeightFactory(); IAlphabet alphabet = flyWeightFactory.GetObject(typeof(A).Name); alphabet.Render("Arial"); Console.WriteLine(); alphabet = flyWeightFactory.GetObject(typeof(B).Name); alphabet.Render("Tahoma"); Console.WriteLine(); alphabet = flyWeightFactory.GetObject(typeof(A).Name); alphabet.Render("Time is New Roman"); Console.WriteLine(); alphabet = flyWeightFactory.GetObject(typeof(A).Name); alphabet.Render("B Nazanin"); Console.WriteLine(); Console.WriteLine("Total new alphabet count:" + flyWeightFactory.Count);
نکتهی قابل توجه این است که این الگو بصورت داخلی از الگوی Factory Method استفاده میکند. با توجه بیشتر به پیاده سازی Flyweight Factory شباهت هایی بین آن و Singleton Pattern میبینیم. کلاسهایی از این دست را Multiton می نامند. در Multiton نمونهها بصورت زوج کلیدهایی نگهداری میشوند و بر اساس Key دریافت شده نمونهی متناظر بازگردانده میشود. همچنین در Singleton تضمین میشود که از کلاس مربوطه فقط یک نمونه در کل Application وجود دارد. در Multiton Pattern تضمین میشود که برای هر Key تنها یک Instance وجود دارد.
public class Service { public int ServiceId { get; set; } public string ServiceName { get; set; } }
public interface ICoreService { Service LoadDefaultService(); }
An unhandled exception occurred while processing the request InvalidOperationException: Unable to resolve service for type 'WebApplication1.Models.ICoreService' while attempting to activate 'WebApplication1.Controllers.HomeController' Microsoft.Extensions.Internal.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
در نسخههای قدیمی MVC (منظور نسخههای قبل از 6)، برای تزریق وابستگیها از یک Controller Factory یا Dependency Resolver سفارشی استفاده میشد. اما در نسخه جدید MVC دیگری خبری از روشهای قدیمی نیست. چونکه یک سیستم تزریق وابستگی توکار، همراه با MVC یکپارچه شدهاست که عملیات تزریق وابستگیها را انجام میدهد. سیستم تزریق وابستگی پیش فرض، تنها از 4 حالت عملیاتی پشتیبانی میکند:
تیم Asp.Net برای فراهم آوردن امکان تزریق وابستگیها، تصمیم به انتزاعی کردن ویژگیهای مشترک محبوبترین Ioc Containerها و اجازه دادن به میان افزارها، جهت ارتباط با این اینترفیسها برای دستیابی به تزریق وابستگی بود.
namespace Microsoft.Extensions.DependencyInjection { // // Summary: // Specifies the lifetime of a service in an Microsoft.Extensions.DependencyInjection.IServiceCollection. public enum ServiceLifetime { // // Summary: // Specifies that a single instance of the service will be created. Singleton = 0, // // Summary: // Specifies that a new instance of the service will be created for each scope. // // Remarks: // In ASP.NET Core applications a scope is created around each server request. Scoped = 1, // // Summary: // Specifies that a new instance of the service will be created every time it is // requested. Transient = 2 } }
public void ConfigureServices(IServiceCollection services) { ServiceDescriptor descriptor = new ServiceDescriptor(typeof(ICoreService),typeof(CoreServise),ServiceLifetime.Transient); services.Add(descriptor); services.AddMvc(); }
ساخت یک Service Descriptor و اضافه کردن آن به سرویسها، فلسفه وجودی میان افزارها را زیر سوال میبرد. پس بجای ایجاد یک Service Descriptor، از متدهای الحاقی تدارک دیده شده استفاده میکنیم. مثلا بجای دو خط کد بالا میتوان از کد زیر استفاده نمود:
services.AddTransient<ICoreService,CoreServise>();
حال که یک دید کلی از نحوه کار مکانیزم تزریق وابستگی بدست آوردیم، میخواهیم این مکانیزم را با StructureMap جایگزین کنیم. بدین منظور ابتدا پکیج StructureMap را نصب میکنم.
در مرحله اول باید کلاسهایی را تدارک ببینیم که اینترفیسهای بالا را پیاده سازی نمایند. یعنی کلاسهای ما باید بتوانند همان کاری را انجام دهند که مکانیزم پیش فرض MVC انجام میدهد.
اولین مورد، کلاس StructureMapServiceProvider میباشد.
internal class StructureMapServiceProvider : IServiceProvider { private readonly IContainer _container; public StructureMapServiceProvider(IContainer container, bool scoped = false) { _container = container; } public object GetService(Type type) { try { return _container.GetInstance(type); } catch { return null; } } }
مورد دوم کلاس StructureMapServiceScope میباشد:
internal class StructureMapServiceScope : IServiceScope { private readonly IContainer _container; private readonly IContainer _childContainer; private IServiceProvider _provider; public StructureMapServiceScope(IContainer container) { _container = container; _childContainer = _container.GetNestedContainer(); _provider = new StructureMapServiceProvider(_childContainer, true); } public IServiceProvider ServiceProvider => _provider; public void Dispose() { _provider = null; if (_childContainer != null) _childContainer.Dispose(); } }
مورد سوم StructureMapServiceScopeFactory میباشد:
internal class StructureMapServiceScopeFactory : IServiceScopeFactory { private IContainer _container; public StructureMapServiceScopeFactory(IContainer container) { _container = container; } public IServiceScope CreateScope() { return new StructureMapServiceScope(_container); } }
مورد بعدی کلاس StructureMapPopulator میباشد. وظیفه این کلاس جمع آوری اطلاعات مربوط به سرویسها میباشد.
internal class StructureMapPopulator { private IContainer _container; public StructureMapPopulator(IContainer container) { _container = container; } public void Populate(IEnumerable<ServiceDescriptor> descriptors) { _container.Configure(c => { c.For<IServiceProvider>().Use(new StructureMapServiceProvider(_container)); c.For<IServiceScopeFactory>().Use<StructureMapServiceScopeFactory>(); foreach (var descriptor in descriptors) { switch (descriptor.Lifetime) { case ServiceLifetime.Singleton: Use(c.For(descriptor.ServiceType).Singleton(), descriptor); break; case ServiceLifetime.Transient: Use(c.For(descriptor.ServiceType), descriptor); break; case ServiceLifetime.Scoped: Use(c.For(descriptor.ServiceType), descriptor); break; } } }); } private static void Use(GenericFamilyExpression expression, ServiceDescriptor descriptor) { if (descriptor.ImplementationFactory != null) { expression.Use(Guid.NewGuid().ToString(), context => { return descriptor.ImplementationFactory(context.GetInstance<IServiceProvider>()); }); } else if (descriptor.ImplementationInstance != null) { expression.Use(descriptor.ImplementationInstance); } else if (descriptor.ImplementationType != null) { expression.Use(descriptor.ImplementationType); } else { throw new InvalidOperationException("IServiceDescriptor is invalid"); } } }
و در آخر کلاس StructureMapRegistration میباشد:
public static class StructureMapRegistration { public static void Populate(this IContainer container, IEnumerable<ServiceDescriptor> descriptors) { var populator = new StructureMapPopulator(container); populator.Populate(descriptors); } }
نهایتاً باید متد ConfigurationServices در کلاس StartUp را اندکی تغییر دهیم.
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); var container = new Container(); container.Configure(configure => { configure.For<ICoreService>().Use<CoreServise>(); }); container.Populate(services); return container.GetInstance<IServiceProvider>(); }
در کد بالا، متد ConfigurationServices به جای آنکه Void برگرداند، نمونهای از اینترفیس IServiceProvider را برمیگرداند. حال اگر برنامه را اجرا کنیم، وابستگیها توسط StructureMap تزریق شده و برنامه بدون هیچ مشکلی اجرا میشود.
EF Code First #12
- در اینجا متد جدید SaveAllChanges تعریف شد تا بدانید اگر خواستید پیش از SaveChanges اصلی، مثلا کار اعتبارسنجی دستی را انجام دهید (برای نمونه در برنامههای دسکتاپ مثلا)، چگونه میتوان به آن دسترسی داشت.
- DbContext در DataLayer قرار دارد. زمانیکه با یک ORM کار میکنید، خود ORM همان DataLayer شما است. لایهای که از آن استفاده میکند (لایه سرویس)، صرفا با اینترفیس IUnitOfWork کار میکند و تعاریف موجود در آن و نه چیزی بیشتر از آن.
- زمانیکه با IUnitOfWork کار میکنید، در هیچ قسمتی از برنامه قرار نیست مستقیما new MyContext مشاهده شود. تامین این وهله صرفا از طریق تزریق وابستگیها صورت میگیرد (کل بحث جاری یا همان پیاده سازی الگوی Context Per Request بر همین مبنا است؛ یک وهله از Context در طول یک درخواست. نه اینکه هر جایی علاقمند بودیم یک new MyContext دستی نوشته شود).
فرض کنید با مسئلهای مواجه هستید که باید ۲ میلیون گوئد (GUID) در ثانیه بسازید؛ و بسیار مهم است که این کار در کوتاهترین زمان ممکن انجام شود.
در مقاله زیر ابتدا سادهترین کد ممکن که حلقهای ساده برای انجام این کار است نوشته شده، و سپس قدم به قدم با استفاده از تکنیکهای مختلف آپتیمایز شده. کد سادهای که با آن کار شروع شده حدود ۲۱۲ میلیثانیه زمان میبرد و پس از اعمال آخرین آپتیمایزیشن این کار ۴۵ میلیثانیه طول میکشد.
EF Code First #12
پیاده سازی الگوی Context Per Request در برنامههای مبتنی بر EF Code first
در طراحی برنامههای چند لایه مبتنی بر EF مرسوم نیست که در هر کلاس و متدی که قرار است از امکانات آن استفاده کند، یکبار DbContext و کلاس مشتق شده از آن وهله سازی شوند؛ به این ترتیب امکان انجام امور مختلف در طی یک تراکنش از بین میرود. برای حل این مشکل الگویی مطرح شده است به نام Session/Context Per Request و یا به اشتراک گذاری یک Unit of work در لایههای مختلف برنامه در طی یک درخواست، که در ادامه یک پیاده سازی آنرا با هم مرور خواهیم کرد.
البته این سشن با سشن ASP.NET یکی نیست. در NHibernate معادل DbContextایی که در اینجا ملاحظه میکنید، Session نام دارد.
اهمیت بکارگیری الگوی Unit of work و به اشتراک گذاری آن در طی یک درخواست
در الگوی واحد کار یا همان DbContext در اینجا، تمام درخواستهای رسیده به آن، در صف قرار گرفته و تمام آنها در پایان کار، به بانک اطلاعاتی اعمال میشوند. برای مثال زمانیکه شیءایی را به یک وهله از DbContext اضافه/حذف میکنیم، یا در ادامه مقدار خاصیتی را تغییر میدهیم، هیچکدام از این تغییرات تا زمانیکه متد SaveChanges فراخوانی نشود، به بانک اطلاعاتی اعمال نخواهند شد. این مساله مزایای زیر را به همراه خواهد داشت:
الف) کارآیی بهتر
در اینجا از یک کانکشن باز شده، حداکثر استفاده صورت میگیرد. چندین و چند عملیات در طی یک batch به بانک اطلاعاتی اعمال میگردند؛ بجای اینکه برای اعمال هرکدام، یکبار اتصال جداگانهای به بانک اطلاعاتی باز شود.
ب) بررسی مسایل همزمانی
استفاده از یک الگوی واحد کار، امکان بررسی خودکار تمام تغییرات انجام شده بر روی یک موجودیت را در متدها و لایههای مختلف میسر کرده و به این ترتیب مسایل مرتبط با ConcurrencyMode عنوان شده در قسمتهای قبل به نحو بهتری قابل مدیریت خواهند بود.
ج) استفاده صحیح از تراکنشها
الگوی واحد کار به صورت خودکار از تراکنشها استفاده میکند. اگر در حین فراخوانی متد SaveChanges مشکلی رخ دهد، کل عملیات Rollback خواهد شد و تغییری در بانک اطلاعاتی رخ نخواهد داد. بنابراین استفاده از یک تراکنش در حین چند عملیات ناشی از لایههای مختلف برنامه، منطقیتر است تا اینکه هر کدام، در تراکنشی جدا مشغول به کار باشند.
کلاسهای مدل مثال جاری
در مثالی که در این قسمت بررسی خواهیم کرد، از کلاسهای مدل گروه محصولات کمک گرفته شده است:
using System.Collections.Generic;
namespace EF_Sample07.DomainClasses { public class Category { public int Id { get; set; } public virtual string Name { get; set; } public virtual string Title { get; set; } public virtual ICollection<Product> Products { get; set; } } }
using System.ComponentModel.DataAnnotations;
namespace EF_Sample07.DomainClasses { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; }
[ForeignKey("CategoryId")] public virtual Category Category { get; set; } public int CategoryId { get; set; } } }
در کلاس Product، یک خاصیت اضافی به نام CategoryId اضافه شده است که توسط ویژگی ForeignKey، به عنوان کلید خارجی جدول معرفی خواهد شد. از این خاصیت در برنامههای ASP.NET برای مقدار دهی یک کلید خارجی توسط یک DropDownList پر شده با لیست گروهها، استفاده خواهیم کرد.
پیاده سازی الگوی واحد کار
همانطور که در قسمت قبل نیز ذکر شد، DbContext در EF Code first بر اساس الگوی واحد کار تهیه شده است، اما برای به اشتراک گذاشتن آن بین لایههای مختلف برنامه نیاز است یک لایه انتزاعی را برای آن تهیه کنیم، تا بتوان آنرا به صورت خودکار توسط کتابخانههای Dependency Injection یا به اختصار DI در زمان نیاز به استفاده از آن، به کلاسهای استفاده کننده تزریق کنیم. کتابخانهی DI ایی که در این قسمت مورد استفاده قرار میگیرد، کتابخانه معروف StructureMap است. برای دریافت آن میتوانید از Nuget استفاده کنید؛ یا از صفحه اصلی آن در Github : (^).
اینترفیس پایه الگوی واحد کار ما به شرح زیر است:
using System.Data.Entity; using System;
namespace EF_Sample07.DataLayer.Context { public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveChanges(); } }
برای استفاده اولیه آن، تنها تغییری که در برنامه حاصل میشود به نحو زیر است:
using System.Data.Entity; using EF_Sample07.DomainClasses;
namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
#region IUnitOfWork Members public new IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } #endregion } }
توضیحات:
با کلاس Context در قسمتهای قبل آشنا شدهایم. در اینجا به معرفی کلاسهایی خواهیم پرداخت که در معرض دید EF Code first قرار خواهند گرفت.
DbSetها هم معرف الگوی Repository هستند. کلاس Sample07Context، معرفی الگوی واحد کار یا Unit of work برنامه است.
برای اینکه بتوانیم تعاریف کلاسهای سرویس برنامه را مستقل از تعریف کلاس Sample07Context کنیم، یک اینترفیس جدید را به نام IUnitOfWork به برنامه اضافه کردهایم.
در اینجا کلاس Sample07Context پیاده سازی کننده اینترفیس IUnitOfWork خواهد بود (اولین تغییر).
دومین تغییر هم استفاده از متد base.Set میباشد. به این ترتیب به سادگی میتوان به DbSetهای مختلف در حین کار با IUnitOfWork دسترسی پیدا کرد. به عبارتی ضرورتی ندارد به ازای تک تک DbSetها یکبار خاصیت جدیدی را به اینترفیس IUnitOfWork اضافه کرد. به کمک استفاده از امکانات Generics مهیا، اینبار
uow.Set<Product>
معادل همان db.Products سابق است؛ در حالتیکه از Sample07Context به صورت مستقیم استفاده شود.
همچنین نیازی به پیاده سازی متد SaveChanges نیست؛ زیرا پیاده سازی آن در کلاس DbContext قرار دارد.
استفاده از الگوی واحد کار در کلاسهای لایه سرویس برنامه
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface ICategoryService { void AddNewCategory(Category category); IList<Category> GetAllCategories(); } }
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface IProductService { void AddNewProduct(Product product); IList<Product> GetAllProducts(); } }
لایه سرویس برنامه را با دو اینترفیس جدید شروع میکنیم. هدف از این اینترفیسها، ارائه پیاده سازیهای متفاوت، به ازای ORMهای مختلف است. برای مثال در کلاسهای زیر که نام آنها با Ef شروع شده است، پیاده سازی خاص Ef Code first را تدارک خواهیم دید. این پیاده سازی، قابل انتقال به سایر ORMها نیست چون نه پیاده سازی یکسانی را از مباحث LINQ ارائه میدهند و نه متدهای الحاقی همانندی را به همراه دارند و نه اینکه مباحث نگاشت کلاسهای آنها به جداول مختلف یکی است:
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfCategoryService : ICategoryService { IUnitOfWork _uow; IDbSet<Category> _categories; public EfCategoryService(IUnitOfWork uow) { _uow = uow; _categories = _uow.Set<Category>(); }
public void AddNewCategory(Category category) { _categories.Add(category); }
public IList<Category> GetAllCategories() { return _categories.ToList(); } } }
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfProductService : IProductService { IUnitOfWork _uow; IDbSet<Product> _products; public EfProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); }
public void AddNewProduct(Product product) { _products.Add(product); }
public IList<Product> GetAllProducts() { return _products.Include(x => x.Category).ToList(); } } }
توضیحات:
همانطور که ملاحظه میکنید در هیچکدام از کلاسهای سرویس برنامه، وهله سازی مستقیمی از الگوی واحد کار وجود ندارد. این لایه از برنامه اصلا نمیداند که کلاسی به نام Sample07Context وجود خارجی دارد یا خیر.
همچنین لایه اضافی دیگری را به نام Repository جهت مخفی سازی سازوکار EF به برنامه اضافه نکردهایم. این لایه شاید در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نیست و سبب تحمیل سربار اضافی بی موردی به برنامه میشود؛ ORMها ویژگیهای یکسانی را ارائه نمیدهند. حتی در حالت استفاده از LINQ، پیاده سازیهای یکسانی را به همراه ندارند.
بنابراین اگر قرار است برنامه مستقل از ORM کار کند، نیاز است لایه استفاده کننده از سرویس برنامه، با دو اینترفیس IProductService و ICategoryService کار کند و نه به صورت مستقیم با پیاده سازی آنها. به این ترتیب هر زمان که لازم شد، فقط باید پیاده سازیهای کلاسهای سرویس را تغییر داد؛ باز هم برنامه نهایی بدون نیاز به تغییری کار خواهد کرد.
تا اینجا به معماری پیچیدهای نرسیدهایم و اصطلاحا over-engineering صورت نگرفته است. یک اینترفیس بسیار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه این اینترفیس به کلاسهای سرویس برنامه تزریق شده است (تزریق وابستگی در سازنده کلاس). کلاسهای سرویس ما «میدانند» که EF وجود خارجی دارد و سعی نکردهایم توسط لایه اضافی دیگری آنرا مخفی کنیم. شیوه کار با IDbSet تعریف شده دقیقا همانند روال متداولی است که با EF Code first کار میشود و بسیار طبیعی جلوه میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه کنسول ویندوزی
در ادامه برای وهله سازی اینترفیسهای سرویس و واحد کار برنامه، از کتابخانه StructureMap که یاد شد، استفاده خواهیم کرد. بنابراین، تمام برنامههای نهایی ارائه شده در این قسمت، ارجاعی را به اسمبلی StructureMap.dll نیاز خواهند داشت.
کدهای برنامه کنسول مثال جاری را در ادامه ملاحظه خواهید کرد:
using System.Collections.Generic; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07 { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>(); x.For<ICategoryService>().Use<EfCategoryService>(); });
var uow = ObjectFactory.GetInstance<IUnitOfWork>(); var categoryService = ObjectFactory.GetInstance<ICategoryService>();
var product1 = new Product { Name = "P100", Price = 100 }; var product2 = new Product { Name = "P200", Price = 200 }; var category1 = new Category { Name = "Cat100", Title = "Title100", Products = new List<Product> { product1, product2 } }; categoryService.AddNewCategory(category1); uow.SaveChanges(); } } }
در اینجا بیشتر هدف، معرفی نحوه استفاده از StructureMap است.
ابتدا توسط متد ObjectFactory.Initialize مشخص میکنیم که اگر برنامه نیاز به اینترفیس IUnitOfWork داشت، لطفا کلاس Sample07Context را وهله سازی کرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر باید از کلاس EfCategoryService تامین شود.
توسط ObjectFactory.GetInstance نیز میتوان به وهلهای از این کلاسها دست یافت و نهایتا با فراخوانی uow.SaveChanges میتوان اطلاعات را ذخیره کرد.
چند نکته:
- به کمک کتابخانه StructureMap، تزریق IUnitOfWork به سازنده کلاس EfCategoryService به صورت خودکار انجام میشود. اگر به کدهای فوق دقت کنید ما فقط با اینترفیسها مشغول به کار هستیم، اما وهلهسازیها در پشت صحنه انجام میشود.
- حین معرفی IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. این enum مقادیر زیر را میتواند بپذیرد:
public enum InstanceScope { PerRequest = 0, Singleton = 1, ThreadLocal = 2, HttpContext = 3, Hybrid = 4, HttpSession = 5, HybridHttpSession = 6, Unique = 7, Transient = 8, }
برای مثال اگر در برنامهای نیاز داشتید یک کلاس به صورت Singleton عمل کند، فقط کافی است نحوه کش شدن آنرا تغییر دهید.
حالت PerRequest در برنامههای وب کاربرد دارد (و حالت پیش فرض است). با انتخاب آن وهله سازی کلاس مورد نظر به ازای هر درخواست رسیده انجام خواهد شد.
در حالت ThreadLocal، به ازای هر Thread، وهلهای متفاوت در اختیار مصرف کننده قرار میگیرد.
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی میگردد.
حالت Hybrid ترکیبی است از حالتهای HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET MVC
یک برنامه خالی ASP.NET MVC را آغاز کنید. سپس یک HomeController جدید را نیز به آن اضافه نمائید و کدهای آنرا مطابق اطلاعات زیر تغییر دهید:
using System.Web.Mvc; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using EF_Sample07.DataLayer.Context; using System.Collections.Generic;
namespace EF_Sample07.MvcAppSample.Controllers { public class HomeController : Controller { IProductService _productService; ICategoryService _categoryService; IUnitOfWork _uow; public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService) { _productService = productService; _categoryService = categoryService; _uow = uow; }
[HttpGet] public ActionResult Index() { var list = _productService.GetAllProducts(); return View(list); }
[HttpGet] public ActionResult Create() { ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name"); return View(); }
[HttpPost] public ActionResult Create(Product product) { if (this.ModelState.IsValid) { _productService.AddNewProduct(product); _uow.SaveChanges(); }
return RedirectToAction("Index"); }
[HttpGet] public ActionResult CreateCategory() { return View(); }
[HttpPost] public ActionResult CreateCategory(Category category) { if (this.ModelState.IsValid) { _categoryService.AddNewCategory(category); _uow.SaveChanges(); }
return RedirectToAction("Index"); } } }
نکته مهم این کنترلر، تزریق وابستگیها در سازنده کلاس کنترلر است؛ به این ترتیب کنترلر جاری نمیداند که با کدام پیاده سازی خاصی از این اینترفیسها قرار است کار کند.
اگر برنامه را به همین نحو اجرا کنیم، موتور ASP.NET MVC ایراد خواهد گرفت که یک کنترلر باید دارای سازندهای بدون پارامتر باشد تا من بتوانم به صورت خودکار وهلهای از آنرا ایجاد کنم. برای رفع این مشکل از کتابخانه StructureMap برای تزریق خودکار وابستگیها کمک خواهیم گرفت:
using System; using System.Data.Entity; using System.Web.Mvc; using System.Web.Routing; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.MvcAppSample
{ // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
protected void Application_Start() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); initStructureMap(); }
private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>(); });
//Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); }
protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } }
توضیحات:
کدهای فوق متعلق به کلاس Global.asax.cs هستند. در اینجا در متد Application_Start، متد initStructureMap فراخوانی شده است.
با پیاده سازی ObjectFactory.Initialize در کدهای برنامه کنسول معرفی شده آشنا شدیم. اینبار فقط حالت کش شدن کلاس Context برنامه را HttpContextScoped قرار دادهایم تا به ازای هر درخواست رسیده یک بار الگوی واحد کار وهله سازی شود.
نکته مهمی که در اینجا اضافه شدهاست، استفاده از متد ControllerBuilder.Current.SetControllerFactory میباشد. این متد نیاز به وهلهای از نوع DefaultControllerFactory دارد که نمونهای از آنرا در کلاس StructureMapControllerFactory مشاهده میکنید. به این ترتیب در زمان وهله سازی خودکار یک کنترلر، اینبار StructureMap وارد عمل شده و وابستگیهای برنامه را مطابق تعاریف ObjectFactory.Initialize ذکر شده، به سازنده کلاس کنترلر تزریق میکند.
همچنین در متد Application_EndRequest با فراخوانی ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتی اتصالات به بانک اطلاعاتی جلوگیری خواهیم کرد. چون وهله الگوی کار برنامه HttpScoped تعریف شده، در پایان یک درخواست به صورت خودکار توسط StructureMap پاکسازی میشود و به نشتی منابع نخواهیم رسید.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET Web forms
در یک برنامه ASP.NET Web forms نیز میتوان این مباحث را پیاده سازی کرد:
using System; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class Global : System.Web.HttpApplication { private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
x.SetAllProperties(y=> { y.OfType<IUnitOfWork>(); y.OfType<ICategoryService>(); y.OfType<IProductService>(); }); }); }
void Application_Start(object sender, EventArgs e) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); initStructureMap(); }
void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); }
در اینجا کدهای کلاس Global.asax.cs را ملاحظه میکنید. توضیحات آن با قسمت ASP.NET MVC آنچنان تفاوتی ندارد و یکی است. البته منهای تعاریف SetAllProperties که جدید است و در ادامه به علت اضافه کردن آنها خواهیم رسید.
در ASP.NET Web forms برخلاف ASP.NET MVC نیاز است کار وهله سازی اینترفیسها را به صورت دستی انجام دهیم. برای این منظور و کاهش کدهای تکراری برنامه میتوان یک کلاس پایه را به نحو زیر تعریف کرد:
using System.Web.UI; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class BasePage : Page { public BasePage() { ObjectFactory.BuildUp(this); } } }
سپس برای استفاده از آن خواهیم داشت:
using System; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer;
namespace EF_Sample07.WebFormsAppSample { public partial class AddProduct : BasePage { public IUnitOfWork UoW { set; get; } public IProductService ProductService { set; get; } public ICategoryService CategoryService { set; get; }
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { bindToCategories(); } }
private void bindToCategories() { ddlCategories.DataTextField = "Name"; ddlCategories.DataValueField = "Id"; ddlCategories.DataSource = CategoryService.GetAllCategories(); ddlCategories.DataBind(); }
protected void btnAdd_Click(object sender, EventArgs e) { var product = new Product { Name = txtName.Text, Price = int.Parse(txtPrice.Text), CategoryId = int.Parse(ddlCategories.SelectedItem.Value) }; ProductService.AddNewProduct(product); UoW.SaveChanges(); Response.Redirect("~/Default.aspx"); } } }
اینبار وابستگیهای کلاس افزودن محصولات، به صورت خواصی عمومی تعریف شدهاند. این خواص عمومی توسط متد SetAllProperties که در فایل global.asax.cs معرفی شدند، باید یکبار تعریف شوند (مهم!).
سپس اگر دقت کرده باشید، اینبار کلاس AddProduct از BasePage ما ارث بری کرده است. در سازند کلاس BasePage، با فراخوانی متد ObjectFactory.BuildUp، تزریق وابستگیها به خواص عمومی کلاس جاری صورت میگیرد.
در ادامه نحوه استفاده از این اینترفیسها را جهت مقدار دهی یک DropDownList یا ذخیره سازی اطلاعات یک محصول مشاهده میکنید. در اینجا نیز کار با اینترفیسها انجام شده و کلاس جاری دقیقا نمیداند که با چه وهلهای مشغول به کار است. تنها در زمان اجرا است که توسط StructureMap ، به ازای هر اینترفیس معرفی شده، وهلهای مناسب بر اساس تعاریف فایل Global.asax.cs در اختیار برنامه قرار میگیرد.
کدهای کامل مثالهای این سری را از آدرس زیر هم میتوانید دریافت کنید: (^)
به روز رسانی
کدهای قسمت جاری را به روز شده جهت استفاده از EF 6 و StructureMap 3 در VS 2013، از اینجا میتوانید دریافت کنید:
EF_Sample07
الف) اگر از jQuery Ajax استفاده میکنید، حتما باید استفاده از Url.Action را لحاظ کنید
برای نمونه اگر قسمتی از عملیات Ajaxایی برنامه شما به نحو زیر تعریف شده است :
$.ajax({ type: "POST", url: "/Home/EmployeeInfo", ...
در این حالت برنامه شما تنها در زمانیکه در ریشه یک دومین قرار گرفته باشد کار خواهد کرد. اگر برنامه شما در مسیری مانند http://www.site.com/myNewApp نصب شود، کلیه فراخوانیهای Ajax ایی آن دیگر کار نخواهند کرد چون مسیر url فوق به ریشه سایت اشاره میکند و نه مسیر جاری برنامه شما (در یک sub domain جدید).
به همین جهت در یک چنین حالتی حتما باید به کمک Url.Action مسیر یک اکشن متد را معرفی کرد تا به صورت خودکار بر اساس محل قرارگیری برنامه و تعاریف مسیریابی آن، Url صحیحی تولید شود.
@Url.Action("EmployeeInfo", "Home")
ب) دریافت Url مطلق از یک Url.Action
Urlهای تولید شده توسط Url.Action به صورت پیش فرض نسبی هستند (نسبت به محل نصب و قرارگیری برنامه تعریف میشوند). اگر نیاز به دریافت یک مسیر مطلق که با http برای مثال شروع میشود دارید، باید به نحو زیر عمل کرد:
@Url.Action("About", "Home", null, "http")
ج) استفاده از Url.Action در یک کنترلر
فرض کنید قصد تولید یک فید RSS را در کنترلری دارید. یکی از آیتمهایی که باید ارائه دهید، لینک به مطلب مورد نظر است. این لینک باید مطلق باشد همچنین در یک View هم قرار نداریم که به کمک @ بلافاصله به متد کمکی Url.Action دسترسی پیدا کنیم.
در کنترلرها، وهله جاری کلاس به شیء Url و متد Action آن به نحو زیر دسترسی دارد و خروجی نهایی آن یک رشته است:
var url = this.Url.Action(actionName: "Index", controllerName: "Post", protocol: "http", routeValues: new { id = item.Id });
د) استفاده از Url.Action در کلاسهای کمکی برنامه خارج از یک کنترلر
فرض کنید قصد تهیه یک Html Helper سفارشی را به کمک کدنویسی در یک کلاس مجزا دارید. در اینجا نیز نباید Urlها را دستی تولید کرد. در Html Helperهای سفارشی نیز میتوان به کمک متد UrlHelper.GenerateUrl، به همان امکانات Url.Action دسترسی یافت:
public static class Extensions { public static string MyLink(this HtmlHelper html, ...) { string url = UrlHelper.GenerateUrl(null, "actionName", "controllerName", null, html.RouteCollection, html.ViewContext.RequestContext, includeImplicitMvcValues: true); //...
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام میشود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار میگیرد؛ بجای استفاده از پروتکلهای استاندارد وب سرویسها مانند Soap و امثال آن. پیاده سازیهای ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شدهاست. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامههای ASP.NET.
یافتن آدرس وب سرویس سایت پذیرای Pingback
اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه میشود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
<link rel="pingback" href="pingback server">
همانطور که ملاحظه میکنید، نیاز است Response header را آنالیز کنیم.
private Uri findPingbackServiceUri() { var request = (HttpWebRequest)WebRequest.Create(_targetUri); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Get; request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; using (var response = request.GetResponse() as HttpWebResponse) { if (response == null) return null; var url = extractPingbackServiceUriFormHeaders(response); if (url != null) return url; if (!isResponseHtml(response)) return null; using (var reader = new StreamReader(response.GetResponseStream())) { return extractPingbackServiceUriFormPage(reader.ReadToEnd()); } } } private static Uri extractPingbackServiceUriFormHeaders(WebResponse response) { var pingUrl = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) || header.Equals("pingback", StringComparison.OrdinalIgnoreCase)); return getValidAbsoluteUri(pingUrl); } private static Uri extractPingbackServiceUriFormPage(string content) { if (string.IsNullOrWhiteSpace(content)) return null; var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase); var match = regex.Match(content); return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value); } private static Uri getValidAbsoluteUri(string url) { Uri absoluteUri; return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri; } private static bool isResponseHtml(WebResponse response) { var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("content-type", StringComparison.OrdinalIgnoreCase)); return !string.IsNullOrWhiteSpace(contentTypeKey) && response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase); }
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شدهاست. زمانیکه این صفحه را درخواست میکنیم، response.Headers.AllKeys حاصل میتواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان مییابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شدهاست.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شدهاست. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.
ارسال Ping به آدرس سرویس Pingback
اکنون که آدرس سرویس pingback یک سایت را یافتهایم، کافی است ping ایی را به آن ارسال کنیم:
public void Send() { var pingUrl = findPingbackServiceUri(); if (pingUrl == null) throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host)); sendPing(pingUrl); } private void sendPing(Uri pingUrl) { var request = (HttpWebRequest)WebRequest.Create(pingUrl); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml"; request.ProtocolVersion = HttpVersion.Version11; makeXmlRpcRequest(request); using (var response = (HttpWebResponse)request.GetResponse()) { response.Close(); } } private void makeXmlRpcRequest(WebRequest request) { var stream = request.GetRequestStream(); using (var writer = new XmlTextWriter(stream, Encoding.ASCII)) { writer.WriteStartDocument(true); writer.WriteStartElement("methodCall"); writer.WriteElementString("methodName", "pingback.ping"); writer.WriteStartElement("params"); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); } }
در اینجا sourceUri آدرس صفحهای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شدهاست. در یک pinback، صرفا این دو آدرس به سرویس دریافت کنندهی pingback ارسال میشوند.
سپس سایت دریافت کنندهی ping، ابتدا sourceUri را دریافت میکند تا عنوان آنرا استخراج کند و همچنین بررسی میکند که آیا targetUri، در آن درج شدهاست یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs
نحوهی استفاده از کلاس Pingback تهیه شده
کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش میشوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینکهای خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینکهای یک محتوا را نیز میتوان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانهی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url); var linkTags = doc.DocumentNode.Descendants("link"); var linkedPages = doc.DocumentNode.Descendants("a") .Select(a => a.GetAttributeValue("href", null)) .Where(u => !String.IsNullOrEmpty(u));
[HttpPost, AjaxOnly, ValidateAntiForgeryToken, ValidateModelState] public virtual async Task<ActionResult> Create([Bind(Prefix = "Model")]MeetingCreateModel model) { await _service.CreateAsync(model).ConfigureAwait(false); return InformationNotification("Messages.Save.Success"); }