[HttpGet] public IAsyncEnumerable<Product> Get() => productsRepository.GetAllProducts();
// Old code: app.UseSignalR(routes => { routes.MapHub<SomeHub>("/path"); }); // New code: app.UseEndpoints(endpoints => { endpoints.MapHub<SomeHub>("/path"); });
// Old code: app.UseSignalR(routes => { routes.MapHub<SomeHub>("/path"); }); // New code: app.UseEndpoints(endpoints => { endpoints.MapHub<SomeHub>("/path"); });
معماری N-Tier چالشهای بخصوصی را برای قابلیتهای change-tracking در EF اضافه میکند. در ابتدا دادهها توسط یک آبجکت EF Context بارگذاری میشوند اما این آبجکت پس از ارسال دادهها به کلاینت از بین میرود. تغییراتی که در سمت کلاینت روی دادهها اعمال میشوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیتها نمیداند.
در نسخههای قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities میتوانستند تغییرات موجودیتها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی میشود، آبجکت DbContext از آن پشتیبانی نمیکند.
در این سری از مقالات روی عملیات پایه CRUD تمرکز میکنیم که در اکثر اپلیکیشنهای n-Tier استفاده میشوند. همچنین خواهیم دید چگونه میتوان تغییرات موجودیتها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیتهای منفصل (disconnected) توسط سرویسهای Web API نگاهی خواهیم داشت.
بروز رسانی موجودیتهای منفصل با Web API
سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده میشود. همچنین مدیریت دادهها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی میکند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژهها برای شبیه سازی یک محیط n-Tier انجام شده است.
فرض کنید مدلی مانند تصویر زیر داریم.
همانطور که میبینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی میکند. میخواهیم مدل و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده میکند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
- کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
- کلاس جدیدی با نام Order در پوشه مدلها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order { public int OrderId { get; set; } public string Product { get; set; } public int Quantity { get; set; } public string Status { get; set; } public byte[] TimeStamp { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext { public Recipe1Context() : base("Recipe1ConnectionString") { } public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>().ToTable("Orders"); // Following configuration enables timestamp to be concurrency token modelBuilder.Entity<Order>().Property(x => x.TimeStamp) .IsConcurrencyToken() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe1ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال میکند.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); ... }
- در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController { // GET api/order public IEnumerable<Order> Get() { using (var context = new Recipe1Context()) { return context.Orders.ToList(); } } // GET api/order/5 public Order Get(int id) { using (var context = new Recipe1Context()) { return context.Orders.FirstOrDefault(x => x.OrderId == id); } } // POST api/order public HttpResponseMessage Post(Order order) { // Cleanup data from previous requests Cleanup(); using (var context = new Recipe1Context()) { context.Orders.Add(order); context.SaveChanges(); // create HttpResponseMessage to wrap result, assigning Http Status code of 201, // which informs client that resource created successfully var response = Request.CreateResponse(HttpStatusCode.Created, order); // add location of newly-created resource to response header response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = order.OrderId })); return response; } } // PUT api/order/5 public HttpResponseMessage Put(Order order) { using (var context = new Recipe1Context()) { context.Entry(order).State = EntityState.Modified; context.SaveChanges(); // return Http Status code of 200, informing client that resouce updated successfully return Request.CreateResponse(HttpStatusCode.OK, order); } } // DELETE api/order/5 public HttpResponseMessage Delete(int id) { using (var context = new Recipe1Context()) { var order = context.Orders.FirstOrDefault(x => x.OrderId == id); context.Orders.Remove(order); context.SaveChanges(); // Return Http Status code of 200, informing client that resouce removed successfully return Request.CreateResponse(HttpStatusCode.OK); } } private void Cleanup() { using (var context = new Recipe1Context()) { context.Database.ExecuteSqlCommand("delete from [orders]"); } } }
در قدم بعدی اپلیکیشن کلاینت را میسازیم که از سرویس Web API استفاده میکند.
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
- کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.
نکته: قسمت هایی از اپلیکیشن که باید در لایههای مختلف مورد استفاده قرار گیرند - مانند کلاسهای موجودیتها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا میتوانید پروژه ای از نوع Class Library بسازید و تمام موجودیتها را در آن تعریف کنید. سپس لایههای مختلف این پروژه را ارجاع خواهند کرد.
فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.
private HttpClient _client; private Order _order; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { // create instance of the program class var program = new Program(); program.ServiceSetup(); program.CreateOrder(); // do not proceed until order is added await program.PostOrderAsync(); program.ChangeOrder(); // do not proceed until order is changed await program.PutOrderAsync(); // do not proceed until order is removed await program.RemoveOrderAsync(); } private void ServiceSetup() { // map URL for Web API cal _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") }; // add Accept Header to request Web API content // negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept. Add(new MediaTypeWithQualityHeaderValue("application/json")); } private void CreateOrder() { // Create new order _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" }; } private async Task PostOrderAsync() { // leverage Web API client side API to call service var response = await _client.PostAsJsonAsync("api/order", _order); Uri newOrderUri; if (response.IsSuccessStatusCode) { // Capture Uri of new resource newOrderUri = response.Headers.Location; // capture newly-created order returned from service, // which will now include the database-generated Id value _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully created order. Here is URL to new resource: {0}", newOrderUri); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private void ChangeOrder() { // update order _order.Quantity = 10; } private async Task PutOrderAsync() { // construct call to generate HttpPut verb and dispatch // to corresponding Put method in the Web API Service var response = await _client.PutAsJsonAsync("api/order", _order); if (response.IsSuccessStatusCode) { // capture updated order returned from service, which will include new quanity _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully updated order: {0}", response.StatusCode); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private async Task RemoveOrderAsync() { // remove order var uri = "api/order/" + _order.OrderId; var response = await _client.DeleteAsync(uri); if (response.IsSuccessStatusCode) Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode); else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); }
Successfully updated order: OK
Sucessfully deleted order: OK
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر Web API دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله اپلیکیشن در حال اجرا است و سرویسهای ما قابل دسترسی هستند.
حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی میکنیم. با این کار از سرویس مورد نظر درخواست میکنیم که دادهها را با فرمت JSON بازگرداند. سپس یک آبجکت Order میسازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال میکنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت میکند و آن را به لیست موجودیتها در Context جاری اضافه مینماید. این عمل باعث میشود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات میکند. در آخر با فراخوانی متد SaveChanges دادهها را ذخیره میکنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار میدهیم و به کلاینت ارسال میکنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینتها درخواستهای ایجاد رکورد جدید را بصورت POST ارسال میکنند. درخواستهای HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت میشوند.
در مرحله بعد عملیات بعدی را اجرا میکنیم، تعداد سفارش را تغییر میدهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال میکنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت میشود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر میدهیم، که این کار موجودیت را به Context جاری میچسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی میکنیم. در شمارههای بعدی این سری از مقالات، خواهیم دید چگونه میتوان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام میرسانیم.
در مرحله بعد، عملیات نهایی را اجرا میکنیم که موجودیت Order را از منبع داده حذف میکند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه میکنیم و متد DeleteAsync را فراخوانی میکنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی میکنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری میکند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد میشود.
در یک اپلیکیشن واقعی بهتر است کد دسترسی دادهها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.
{ "userIsAuthorizedForCourseTranscripts": false, "modules": [ { "title": "Course Overview", "clips": [ { "title": "Course Overview", "playerParameters": "author=scott-allen&name=aspdotnet-core-1-0-fundamentals-m0&mode=live&clip=0&course=aspdotnet-core-1-0-fundamentals", "transcripts": [ ] } ] }, { "title": "Building Your First ASP.NET Core Application", "clips": [ { "title": "Introduction", "playerParameters": "author=scott-allen&name=aspdotnet-core-1-0-fundamentals-m1&mode=live&clip=0&course=aspdotnet-core-1-0-fundamentals", "transcripts": [ { "displayTime": 0.0, "text": "Hi! This is Scott, and this course will help you build your first application with ASP.NET Core." }, { "displayTime": 7.0, "text": "In this course, we'll be using Visual Studio and the new ASP.NET Framework to build a web application that" } ] } ] } ] }
public class PluralsightCourse { public bool UserIsAuthorizedForCourseTranscripts { get; set; } public PluralsightCourseItems[] Modules { get; set; } } public class PluralsightCourseItems { public string Title { get; set; } public PluralsightCourseClip[] Clips { get; set; } } public class PluralsightCourseClip { public string Title { get; set; } public string PlayerParameters { get; set; } public PluralsightCourseClipTranscript[] Transcripts { get; set; } } public class PluralsightCourseClipTranscript { public float DisplayTime { get; set; } public string Text { get; set; } }
بارگذاری آن توسط کتابخانهی JSON.NET به صورت ذیل خواهد بود:
public static PluralsightCourse ProcessJsonFile(string jsonData) { return JsonConvert.DeserializeObject<PluralsightCourse>(jsonData); }
کدهای کامل این برنامه را از اینجا میتوانید دریافت کنید:
PluralsightJsonTranscripts.V1.0.7z
پیشنیازها
- آشنایی با مباحث Migrations در EF Code first
- آشنایی با مباحث الگوی واحد کار
- چگونه مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
- چگونه تنظیمات مدلهای EF را به صورت خودکار به Context اضافه کنیم؟
کدهایی را که در این قسمت مشاهده خواهید کرد، در حقیقت همان برنامهی توسعه یافته «آشنایی با مباحث الگوی واحد کار» است و از ذکر قسمتهای تکراری آن جهت طولانی نشدن مبحث، صرفنظر خواهد شد. برای مثال Context و مدلهای محصولات و گروههای آنها به همراه کلاسهای لایه سرویس برنامهی اصلی، دقیقا همان کدهای مطلب «آشنایی با مباحث الگوی واحد کار» است.
تعریف domain classes مخصوص افزونهها
در ادامهی پروژهی افزونه پذیر فعلی، پروژهی class library جدیدی را به نام MvcPluginMasterApp.Plugin1.DomainClasses اضافه خواهیم کرد. از آن جهت تعریف کلاسهای مدل افزونهی یک استفاده میکنیم. برای مثال کلاس News را به همراه تنظیمات Fluent آن به این پروژهی جدید اضافه کنید:
using System.Data.Entity.ModelConfiguration; namespace MvcPluginMasterApp.Plugin1.DomainClasses { public class News { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } public class NewsConfig : EntityTypeConfiguration<News> { public NewsConfig() { this.ToTable("Plugin1_News"); this.HasKey(news => news.Id); this.Property(news => news.Title).IsRequired().HasMaxLength(500); this.Property(news => news.Body).IsOptional().IsMaxLength(); } } }
PM> install-package EntityFramework
مشکل! برنامهی اصلی، همانند مطلب «آشنایی با مباحث الگوی واحد کار» دارای domain classes خاص خودش است به همراه تنظیمات Context ایی که صریحا در آن مدلهای متناظر با این پروژه در معرض دید EF قرار گرفتهاند:
public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
تغییرات اینترفیس Unit of work جهت افزونه پذیری
در ادامه، اینترفیس بهبود یافتهی IUnitOfWork را جهت پذیرش DbSetهای پویا و همچنین EntityTypeConfigurationهای پویا، ملاحظه میکنید:
namespace MvcPluginMasterApp.PluginsBase { public interface IUnitOfWork : IDisposable { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); void MarkAsChanged<TEntity>(TEntity entity) where TEntity : class; IList<T> GetRows<T>(string sql, params object[] parameters) where T : class; IEnumerable<TEntity> AddThisRange<TEntity>(IEnumerable<TEntity> entities) where TEntity : class; void SetDynamicEntities(Type[] dynamicTypes); void ForceDatabaseInitialize(); void SetConfigurationsAssemblies(Assembly[] assembly); } }
SetDynamicEntities : توسط این متد در ابتدای برنامه، نوعهای مدلهای جدید افزونهها به صورت خودکار به Context اضافه خواهند شد.
SetConfigurationsAssemblies : کار افزودن اسمبلیهای حاوی تعاریف EntityTypeConfigurationهای جدید و پویا را به عهده دارد.
ForceDatabaseInitialize: سبب خواهد شد تا مباحث migrations، پیش از شروع به کار برنامه، اعمال شوند.
در کلاس Context ذیل، نحوهی پیاده سازی این متدهای جدید را ملاحظه میکنید:
namespace MvcPluginMasterApp.DataLayer.Context { public class MvcPluginMasterAppContext : DbContext, IUnitOfWork { private readonly IList<Assembly> _configurationsAssemblies = new List<Assembly>(); private readonly IList<Type[]> _dynamicTypes = new List<Type[]>(); public void ForceDatabaseInitialize() { Database.Initialize(force: true); } public void SetConfigurationsAssemblies(Assembly[] assemblies) { if (assemblies == null) return; foreach (var assembly in assemblies) { _configurationsAssemblies.Add(assembly); } } public void SetDynamicEntities(Type[] dynamicTypes) { if (dynamicTypes == null) return; _dynamicTypes.Add(dynamicTypes); } protected override void OnModelCreating(DbModelBuilder modelBuilder) { addConfigurationsFromAssemblies(modelBuilder); addPluginsEntitiesDynamically(modelBuilder); base.OnModelCreating(modelBuilder); } private void addConfigurationsFromAssemblies(DbModelBuilder modelBuilder) { foreach (var assembly in _configurationsAssemblies) { modelBuilder.Configurations.AddFromAssembly(assembly); } } private void addPluginsEntitiesDynamically(DbModelBuilder modelBuilder) { foreach (var types in _dynamicTypes) { foreach (var type in types) { modelBuilder.RegisterEntityType(type); } } } } }
بهبود اینترفیس IPlugin جهت پذیرش نوعهای پویای EF
در قسمت اول، با اینترفیس IPlugin آشنا شدیم. هر افزونه باید دارای کلاسی باشد که این اینترفیس را پیاده سازی میکند. از آن جهت دریافت تنظیمات و یا ثبت تنظیمات مسیریابی و امثال آن استفاده میشود.
در اینجا متد GetEfBootstrapper آن کار دریافت تنظیمات EF هر افزونه را به عهد دارد.
namespace MvcPluginMasterApp.PluginsBase { public interface IPlugin { EfBootstrapper GetEfBootstrapper(); //...به همراه سایر متدهای مورد نیاز } public class EfBootstrapper { /// <summary> /// Assemblies containing EntityTypeConfiguration classes. /// </summary> public Assembly[] ConfigurationsAssemblies { get; set; } /// <summary> /// Domain classes. /// </summary> public Type[] DomainEntities { get; set; } /// <summary> /// Custom Seed method. /// </summary> public Action<IUnitOfWork> DatabaseSeeder { get; set; } } }
DomainEntities بیانگر لیست مدلها و موجودیتهای هر افزونه است.
DatabaseSeeder کار دریافت منطق متد Seed را بر عهده دارد. برای مثال اگر افزونهای نیاز است در آغاز کار تشکیل جداول آن، دیتای پیش فرض و خاصی را در بانک اطلاعاتی ثبت کند، میتوان از این متد استفاده کرد. اگر دقت کنید این Action یک وهله از IUnitOfWork را به افزونه ارسال میکند. بنابراین در این طراحی جدید، اینترفیس IUnitOfWork به پروژهی MvcPluginMasterApp.PluginsBase منتقل میشود. به این ترتیب دیگر نیازی نیست تا تک تک افزونهها ارجاع مستقیمی را به DataLayer پروژهی اصلی پیدا کنند.
تکمیل متد GetEfBootstrapper در افزونهها
اکنون جهت معرفی مدلها و تنظیمات EF آنها، تنها کافی است متد GetEfBootstrapper هر افزونه را تکمیل کنیم:
namespace MvcPluginMasterApp.Plugin1 { public class Plugin1 : IPlugin { public EfBootstrapper GetEfBootstrapper() { return new EfBootstrapper { DomainEntities = new[] { typeof(News) }, ConfigurationsAssemblies = new[] { typeof(NewsConfig).Assembly }, DatabaseSeeder = uow => { var news = uow.Set<News>(); if (news.Any()) { return; } news.Add(new News { Title = "News 1", Body = "news 1 news 1 news 1 ...." }); news.Add(new News { Title = "News 2", Body = "news 2 news 2 news 2 ...." }); } }; }
همچنین توسط delegate ایی به نام DatabaseSeeder، نحوهی دسترسی به متد Set واحد کار و سپس استفادهی از آن، برای تعریف متد Seed سفارشی نیز تکمیل شدهاست.
تدارک یک راه انداز EF، پیش از شروع به کار برنامه
در پوشهی App_Start پروژهی اصلی یا همان MvcPluginMasterApp، کلاس جدید EFBootstrapperStart را با کدهای ذیل اضافه کنید:
[assembly: PreApplicationStartMethod(typeof(EFBootstrapperStart), "Start")] namespace MvcPluginMasterApp { public static class EFBootstrapperStart { public static void Start() { var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList(); using (var uow = SmObjectFactory.Container.GetInstance<IUnitOfWork>()) { initDatabase(uow, plugins); runDatabaseSeeders(uow, plugins); } } private static void initDatabase(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null) continue; uow.SetDynamicEntities(efBootstrapper.DomainEntities); uow.SetConfigurationsAssemblies(efBootstrapper.ConfigurationsAssemblies); } Database.SetInitializer(new MigrateDatabaseToLatestVersion<MvcPluginMasterAppContext, Configuration>()); uow.ForceDatabaseInitialize(); } private static void runDatabaseSeeders(IUnitOfWork uow, IEnumerable<IPlugin> plugins) { foreach (var plugin in plugins) { var efBootstrapper = plugin.GetEfBootstrapper(); if (efBootstrapper == null || efBootstrapper.DatabaseSeeder == null) continue; efBootstrapper.DatabaseSeeder(uow); uow.SaveAllChanges(); } } } }
همانطور که ملاحظه میکنید، ابتدا لیست تمام افزونههای موجود، به کمک StructureMap دریافت میشوند. سپس میتوان در متد initDatabase به متد GetEfBootstrapper هر افزونه دسترسی یافت و توسط آن تنظیمات مدلها را یافته و به Context اصلی برنامه اضافه کرد. سپس با فراخوانی ForceDatabaseInitialize تمام این موارد به صورت خودکار به بانک اطلاعاتی اعمال خواهند شد.
کار متد runDatabaseSeeders، یافتن DatabaseSeeder هر افزونه، اجرای آنها و سپس فراخوانی متد SaveAllChanges در آخر کار است.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید:
MvcPlugin
سؤال: مرورگر چه زمانی از کش محلی خودش استفاده خواهد کرد (بدون ارسال درخواستی به سرور) و چه زمانی مجددا از سرور درخواست دریافت مجدد این عنصر کش شده را میکند؟
برای پاسخ دادن به این سؤال نیاز است با مفهومی به نام Conditional Requests (درخواستهای شرطی) آشنا شد که در ادامه به بررسی آن خواهیم پرداخت.
درخواستهای شرطی
مرورگرهای وب دو نوع درخواست شرطی و غیر شرطی را توسط پروتکل HTTP و HTTPS ارسال میکنند. دراینجا، زمانی یک درخواست غیرشرطی ارسال میشود که نسخهی ذخیره شدهی محلی منبع مورد نظر، مهیا نباشد. در این حالت، اگر منبع درخواستی در سرور موجود باشد، در پاسخ ارسالی خود وضعیت 200 یا HTTP/200 OK را باز میگرداند. اگر هدرهای دیگری نیز مانند کش کردن منبع در اینجا تنظیم شده باشند، مرورگر نتیجهی دریافتی را برای استفادهی بعدی ذخیره خواهد کرد.
در بار دومی که منبع مفروضی درخواست میگردد، مرورگر ابتدا به کش محلی خود نگاه خواهد کرد. همچنین در این حالت نیاز دارد که بداند این کش معتبر است یا خیر؟ برای بررسی این مورد ابتدا هدرهای ذخیره شده به همراه منبع، بررسی میشوند. پس از این بررسی اگر مرورگر به این نتیجه برسد که کش محلی معتبر است، دیگر درخواستی را به سرور ارسال نخواهد کرد.
اما در آینده اگر مدت زمان کش شدن تنظیم شده توسط هدرهای مرتبط، منقضی شده باشد (برای مثال با توجه به max-age هدر کش شدن منبع)، مرورگر هنوز هم درخواست کاملی را برای دریافت نسخهی جدید منبع مورد نیاز، به سرور ارسال نمیکند. در اینجا ابتدا یک conditional request را به وب سرور ارسال میکند (یک درخواست شرطی). این درخواست شرطی تنها دارای هدرهای If-Modified-Since و یا If-None-Match است و هدف از آن سؤال پرسیدن از وب سرور است که آیا این منبع خاص، در سمت سرور اخیرا تغییر کردهاست یا خیر؟ اگر پاسخ سرور خیر باشد، باز هم از همان کش محلی استفاده خواهد شد و مجددا درخواست کاملی برای دریافت نمونهی جدیدتر منبع مورد نیاز، به سرور ارسال نمیگردد.
پاسخی که سرور جهت مشخص سازی عدم تغییر منابع خود ارسال میکند، با هدر HTTP/304 Not Modified مشخص میگردد (این پاسخ هیچ body خاصی نداشته و فقط یک سری هدر است). اما اگر منبع درخواستی اخیرا تغییر کرده باشد، پاسخ HTTP/200 OK را در هدر بازگشت داده شده، به مرورگر بازخواهد گرداند (یعنی محتوا را مجددا دریافت کن).
چه زمانی مرورگر درخواستهای شرطی If-Modified-Since را به سرور ارسال میکند؟
اگر یکی از شرایط ذیل برقرار باشد، مرورگر حتی اگر تاریخ کش شدن منبع ویژهای به 10 سال بعد تنظیم شده باشد، مجددا یک درخواست شرطی را برای بررسی اعتبار کش محلی خود به سرور ارسال میکند:
الف) کش شدن بر اساس هدر خاصی به نام vary صورت گرفتهاست (برای مثال بر اساس id یا نام یک فایل).
ب) اگر نحوهی هدایت به صفحهی جاری از طریق META REFRESH باشد.
ج) اگر از طریق کدهای جاوا اسکریپتی، دستور reload صفحه صادر شود.
د) اگر کاربر دکمهی refresh را فشار دهد.
ه) اگر قسمتی از صفحه توسط پروتکل HTTP و قسمتی دیگر از آن توسط پروتکل HTTPS ارائه شود.
و ... اگر بر اساس هدر تاریخ مدت زمان کش شدن منبع، زمان منقضی شدن آن فرا رسیده باشد.
مدیریت درخواستهای شرطی در ASP.NET MVC
تا اینجا به این نکته رسیدیم که قرار دادن ویژگی Output cache بر روی یک اکشن متد، الزاما به معنای کش شدن آن تا مدت زمان تعیین شده نخواهد بود و مرورگر ممکن است (در یکی از 6 حالت ذکر شده فوق) توسط ارسال هدر If-Modified-Since ، سعی در تعیین اعتبار کش محلی خود کند و اگر پاسخ 304 را از سرور دریافت نکند، حتما نسبت به دریافت مجدد و کامل آن منبع اقدام خواهد کرد.
سؤال: چگونه میتوان هدر If-Modified-Since را در ASP.NET MVC مدیریت کرد؟
پاسخ: اگر از فیلتر OutputCache استفاده میکنید، به صورت خودکار هدر Last-Modified را اضافه میکند؛ اما این مورد کافی نیست.
در ادامه یک کنترلر و اکشن متد GetImage آنرا ملاحظه میکنید که تصویری را از مسیر app_data/images خوانده و بازگشت میدهد. همچنین این تصویر بازگشت داده شده را نیز با توجه به OutputCache آن به مدت یک ماه کش میکند.
using System.IO; using System.Web.Mvc; namespace MVC4Basic.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } const int AMonth = 30 * 86400; [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); } } }
<img src="@Url.Action("GetImage","Home", new { name = "test.png"})"/>
بار اول که صفحهی اول برنامه درخواست میشود، یک چنین هدرهایی رد و بدل خواهند شد (توسط ابزارهای توکار مرورگر وب کروم تهیه شدهاست؛ همان دکمهی F12 معروف):
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK Response Headers Cache-Control:public, max-age=2591916 Expires:Sat, 31 May 2014 12:45:55 GMT Last-Modified:Thu, 01 May 2014 12:45:55 GMT
در همین حال اگر صفحه را ریفرش کنیم (فشردن دکمهی F5)، اینبار هدرهای حاصل چنین شکلی را پیدا میکنند:
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:304 Not Modified Request Headers If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
در ادامه بجای اینکه صفحه را ریفرش کنیم، یکبار دیگر در نوار آدرس آن، دکمهی Enter را فشار خواهیم داد تا آدرس موجود در آن (ریشه سایت) مجددا در حالت معمولی دریافت شود.
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK (from cache)
مشکل! مرورگر را ببندید، تا کار دیباگ برنامه خاتمه یابد. مجددا برنامه را اجرا کنید. مشاهده خواهید کرد که ... اجرای برنامه در Break point قرار گرفته در سطر اول متد GetImage متوقف میشود. چرا؟! مگر قرار نبود تا یک ماه دیگر کش شود؟! هدر رد و بدل شده نیز Status Code:200 OK کامل است (که سبب دریافت کامل فایل میشود).
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:200 OK Request Headers If-Modified-Since:Thu, 01 May 2014 12:45:55 GMT
using System; using System.IO; using System.Net; using System.Web.Mvc; namespace MVC4Basic.Controllers { public class HomeController : Controller { public ActionResult Index() { return View(); } const int AMonth = 30 * 86400; [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var lastWriteTime = System.IO.File.GetLastWriteTime(path); this.Response.Cache.SetLastModified(lastWriteTime.ToUniversalTime()); var header = this.Request.Headers["If-Modified-Since"]; if (!string.IsNullOrWhiteSpace(header)) { DateTime isModifiedSince; if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime) { return new HttpStatusCodeResult(HttpStatusCode.NotModified); } } var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); } } }
برای امتحان آن همانطور که عنوان شد فقط کافی است یکبار مرورگر خود را کاملا بسته و مجددا برنامه را اجرا کنید.
Remote Address:127.0.0.1:5656 Request URL:http://localhost:10419/Home/GetImage?name=test.png Request Method:GET Status Code:304 Not Modified Request Headers If-Modified-Since:Thu, 01 May 2014 13:43:32 GMT
موارد کاربرد
اکثر فید خوانهای معروف نیز ابتدا هدر If-Modified-Since را ارسال میکنند و سپس (اگر چیزی تغییر کرده بود) محتوای فید شما را دریافت خواهند کرد. بنابراین برای کاهش بار برنامه و هچنین کاهش میزان انتقال دیتای سایت، مدیریت آن در حین ارائه محتوای پویای فیدها نیز بهتر است صورت گیرد. همچنین هر جایی که قرار است فایلی به صورت پویا به کاربران ارائه شود؛ مانند مثال فوق.
تبدیل این کدها به روش سازگار با ASP.NET MVC
ما در اینجا رسیدیم به یک سری کد تکراری if و else که باید در هر اکشن متدی که OutputCache دارد، تکرار شود. روش AOP وار آن در ASP.NET MVC، تبدیل این کدها به یک فیلتر با قابلیت استفادهی مجدد است:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public sealed class SetIfModifiedSinceAttribute : ActionFilterAttribute { public string Parameter { set; get; } public string BasePath { set; get; } public override void OnActionExecuting(ActionExecutingContext filterContext) { var response = filterContext.RequestContext.HttpContext.Response; var request = filterContext.RequestContext.HttpContext.Request; var path = getPath(filterContext); if (string.IsNullOrWhiteSpace(path)) { response.StatusCode = (int)HttpStatusCode.NotFound; filterContext.Result = new EmptyResult(); return; } var lastWriteTime = File.GetLastWriteTime(path); response.Cache.SetLastModified(lastWriteTime.ToUniversalTime()); var header = request.Headers["If-Modified-Since"]; if (string.IsNullOrWhiteSpace(header)) return; DateTime isModifiedSince; if (DateTime.TryParse(header, out isModifiedSince) && isModifiedSince > lastWriteTime) { response.StatusCode = (int)HttpStatusCode.NotModified; response.SuppressContent = true; filterContext.Result = new EmptyResult(); } } string getPath(ActionExecutingContext filterContext) { if (!filterContext.ActionParameters.ContainsKey(Parameter)) return string.Empty; var name = filterContext.ActionParameters[Parameter] as string; if (string.IsNullOrWhiteSpace(name)) return string.Empty; var path = Path.GetFileName(name); path = filterContext.HttpContext.Server.MapPath(string.Format("{0}/{1}", BasePath, path)); return !File.Exists(path) ? string.Empty : path; } }
و برای استفاده از آن خواهیم داشت:
[SetIfModifiedSince(Parameter = "name", BasePath = "~/app_data/images/")] [OutputCache(Duration = AMonth, VaryByParam = "name")] public ActionResult GetImage(string name) { name = Path.GetFileName(name); var path = Server.MapPath(string.Format("~/app_data/images/{0}", name)); var content = System.IO.File.ReadAllBytes(path); return File(content, "image/png", name); }
خلاصهی بحث
چون فیلتر OutputCache در ASP.NET MVC، هدر If-Modified-Since را پردازش نمیکند (از این جهت که پردازش آن برای نمونه در مثال فوق وابسته به منطق خاصی است و عمومی نیست)، اگر با هر بار گشودن سایت خود مشاهده کردید، تصاویر پویایی که قرار بوده یک ماه کش شوند، دوباره از سرور درخواست میشوند (البته به ازای هرباری که مرورگر از نو اجرا میشود و نه در دفعات بعدی که صفحات سایت با همان وهلهی ابتدایی مرور خواهند شد)، نیاز است خودتان دسترسی کار پردازش هدر If-Modified-Since را انجام داده و سپس status code 304 را در صورت نیاز، ارسال کنید.
و در حالت عمومی، طراحی سیستم caching محتوای پویای شما بدون پردازش هدر If-Modified-Since ناقص است (تفاوتی نمیکند که از کدام فناوری سمت سرور استفاده میکنید).
برای مطالعه بیشتر
Understanding Conditional Requests and Refresh
Use If-Modified-Since header in ASP.NET
Make your browser cache the output of an HttpHandler
304 Your images from a database
Conditional GET
Website Performance with ASP.NET - Part4 - Use Cache Headers
ASP.NET MVC 304 Not Modified Filter for Syndication Content
- کش کرنل به صورت پیش فرض بر روی صفحات ایستا فعال شده است؛ نه برای صفحاتی با محتوای پویا که البته این مورد قابل تغییر است که نحوه این تغییر را پایینتر توضیح خواهیم داد.
- اگر آدرس درخواستی شامل کوئری باشد صفحه کش نخواهد شد: http://www.site.info/postarchive.htm?id=25
- برای پاسخ ازمکانیزمهای فشرده سازی پویا استفاده شده باشد مثل gzip کش نخواهد شد
- صفحه درخواست شده صفحه اصلی سایت باشد کش نخواهد شد : http://www.dotnettip.info ولی اگر درخواست بدین صورت باشه http://www.domain.com/default.htm کش خواهد کرد.
- درخواست به صورت ناشناس anonymous نباشد و نیاز به authentication داشته باشد کش نخواهد شد (یعنی در هدر شامل گزینه authorization میباشد).
- درخواست باید از نوع نسخه http1 به بعد باشد.
- اگر درخواست شامل Entity-body باشد کش نخواهد کرد.
- درخواست شامل If-Range/Range header باشد کش نمیشود.
- کل حجم response بییشتر از اندازه تعیین شده باشد کش نخواهد گردید، این اندازه در کلید ریجستری UriMaxUriBytes قرار دارد. اطلاعات بیشتر
- اندازه هدر بیشتر از اندازه تعیین شده باشد که عموما اندازه تعیین شده یک کیلو بایت است.
- کش پر باشد، کش انجام نخواهد گرفت.
برای تعیین مقدار سایز کش response که در بالا اشاره کردیم میتوانید در همان پنجره، گزینه edit feature settings را انتخاب کنید.
این قسمت از مطلب که به نقل از مقاله آقای Karol Jarkovsky در این آدرس است یک سری تست هایی با نرم افزار(Web Capacity Analysis Tool (WCAT گرفته است که به نتایج زیر دست پیدا کرده است:
We recently published a BenchmarkDotNet-based benchmark on GitHub: https://github.com/DevExpress/XpoNetCoreDemos/tree/master/ORMBenchmark.
We used it to test the performance of the following Object-Relational Mapping (ORM) libraries for .NET Framework 4.6.1 and higher:
- Entity Framework 6.0.2 (EF 6);
- Entity Framework Core 2.0.3 (EF Core);
- eXpress Persistent Objects™ 18.1.3 (XPO).