using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web; using System.Web.Helpers; using System.Web.Http.Controllers; using System.Web.Http.Filters; using System.Web.Mvc; using ActionFilterAttribute = System.Web.Http.Filters.ActionFilterAttribute; namespace NgxAntiforgeryWebApi.Providers { public class XsrfCookieGeneratorAttribute : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { var xsrfTokenCookie = new HttpCookie("XSRF-TOKEN") { Value = ComputeXsrfTokenValue(), HttpOnly = false // Now JavaScript is able to read the cookie }; HttpContext.Current.Response.AppendCookie(xsrfTokenCookie); } private string ComputeXsrfTokenValue() { string cookieToken, formToken; AntiForgery.GetTokens(null, out cookieToken, out formToken); return $"{cookieToken}:{formToken}"; } } public class XsrfTokensValidationAttribute : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { IEnumerable<string> headerValues; if (!actionContext.Request.Headers.TryGetValues("X-XSRF-TOKEN", out headerValues)) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "X-XSRF-TOKEN header is missing." }; return; } if (headerValues == null) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "X-XSRF-TOKEN header value is empty." }; return; } var xsrfTokensValue = headerValues.FirstOrDefault(); if (string.IsNullOrEmpty(xsrfTokensValue) || !xsrfTokensValue.Contains(":")) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "X-XSRF-TOKEN header value is null." }; return; } var values = xsrfTokensValue.Split(':'); if (values.Length != 2) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = "X-XSRF-TOKEN header value is malformed." }; return; } var cookieToken = values[0]; var formToken = values[1]; try { AntiForgery.Validate(cookieToken, formToken); } catch (HttpAntiForgeryException ex) { actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest) { ReasonPhrase = ex.Message }; } } } }
تزریق خودکار وابستگیها در برنامههای ASP.NET MVC
The IControllerFactory 'Server.Helpers.StructureMapControllerFactory' did not return a controller for the name 'admin'
کد global.asax هم بصورت زیر است:
public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { Database.SetInitializer<EshoppingContext>(null); #region Mvc AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); MvcHandler.DisableMvcResponseHeader = true; #endregion initStructureMap(); } protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new EshoppingContext()); x.For<IProductCategoryService>().Use<ProductCategoryService>(); x.For<IProductService>().Use<ProductService>(); x.For<IPersonService>().Use<PersonService>(); x.For<IPersonAttachmentService>().Use<PersonAttachmentService>(); x.For<IPostService>().Use<PostService>(); x.For<IPostCategoryService>().Use<PostCategoryService>(); x.For<ISliderItemService>().Use<SliderItemService>(); }); ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); } }
public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = MVC.Home.Name, action = MVC.Home.ActionNames.Index, id = UrlParameter.Optional }, namespaces: new[] { "Server.Main.Controllers" } ); } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { if (controllerType == null) { return null; } return ObjectFactory.GetInstance(controllerType) as Controller; } }
ASP.NET MVC #19
مروری بر امکانات Caching اطلاعات در ASP.NET MVC
در برنامههای وب، بالاترین حد کارآیی برنامهها از طریق بهینه سازی الگوریتمها حاصل نمیشود، بلکه با بکارگیری امکانات Caching سبب خواهیم شد تا اصلا کدی اجرا نشود. در ASP.NET MVC این هدف از طریق بکارگیری فیلتری به نام OutputCache میسر میگردد:
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
}
}
همانطور که ملاحظه میکنید، OutputCache را به یک اکشن متد یا حتی به یک کنترلر نیز میتوان اعمال کرد. به این ترتیب HTML نهایی حاصل از View متناظر با اکشن متد جاری فراخوانی شده، Cache خواهد شد. سپس زمانیکه درخواست بعدی به سرور ارسال میشود، نتیجه دریافت شده، همان اطلاعات Cache شده قبلی است و عملا در سمت سرور کدی اجرا نخواهد شد. در اینجا توسط پارامتر Duration، مدت زمان معتبر بودن کش حاصل، برحسب ثانیه مشخص میشود. VaryByParam مشخص میکند که اگر متدی پارامتری را دریافت میکند، آیا باید به ازای هر مقدار دریافتی، مقادیر کش شده متفاوتی ذخیره شوند یا خیر. در اینجا چون متد Index پارامتری ندارد، از مقدار none استفاده شده است.
مثال یک
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس کنترلر جدید Home را نیز به آن اضافه نمائید:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
همچنین کدهای View متد Index را نیز به نحو زیر تغییر دهید:
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
در اینجا نمایش دو زمان دریافتی از کنترلر و زمان محاسبه شده در View را مشاهده میکنید. هدف این است که بررسی کنیم آیا فیلتر OutputCache بر روی این دو مقدار تاثیری دارد یا خیر.
برنامه را اجرا نمائید. سپس چند بار صفحه را Refresh کنید. مشاهده خواهید کرد که هر دو زمان یاد شده تا 60 ثانیه، تغییری نخواهند کرد و حاصل نهایی از Cache خواهنده میشود.
کاربرد یک چنین حالتی برای مثال نمایش اطلاعات بازدیدهای یک سایت است. نباید به ازای هر کاربر وارد شده به سایت، یکبار به بانک اطلاعاتی مراجعه کرد و آمار جدیدی را تهیه نمود. یا برای نمونه اگر جایی قرار است اطلاعات وضعیت آب و هوا نمایش داده شود، بهتر است این اطلاعات، مثلا هر نیم ساعت یکبار به روز شود و نه به ازای هر بازدید جدید از سایت، توسط صدها بازدید کننده همزمان. یا برای مثال کش کردن خروجی فید RSS یک بلاگ به مدت چند ساعت نیز ایده خوبی است. از این لحاظ که اگر اطلاعات بلاگ شما روزی یکبار به روز میشود، نیازی نیست تا به ازای هر برنامه فیدخوان، یکبار اطلاعات از بانک اطلاعاتی دریافت شده و پروسه رندر نهایی فید صورت گیرد. منوهای پویای یک سایت نیز در همین رده قرار میگیرند. دریافت اطلاعات منوهای پویای سایت به ازای هر درخواست رسیده کاربری جدید، کار اشتباهی است. این اطلاعات نیز باید کش شوند تا بار سرور کاهش یابد. البته تمام اینها زمانی میسر خواهند شد که اطلاعات سمت سرور کش شوند.
مثال دو
همان مثال قبلی را در اینجا جهت بررسی پارامتر VaryByParam به نحو زیر تغییر میدهیم:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index(string parameter)
{
ViewBag.Msg = parameter ?? string.Empty;
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}
در اینجا یک پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بین تگهای h2 نمایش داده خواهد شد. همچنین یک فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:
@{
ViewBag.Title = "Index";
}
<h2>@ViewBag.Msg</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>
@using (Html.BeginForm())
{
@Html.TextBox("parameter")
<input type="submit" />
}
اکنون برنامه را اجرا کنید. در TextBox نمایش داده شده یکبار مثلا بنویسید Test1 و فرم را به سرور ارسال نمائید. سپس مقدار Test2 را وارد کرده و ارسال نمائید. در بار دوم، خروجی صفحه همانند زمانی است که مقدار Test1 ارسال شده است. علت این است که مقدار VaryByParam به none تنظیم شده است و صرفنظر از ورودی کاربر، همان اطلاعات کش شده قبلی بازگشت داده خواهد شد. برای رفع این مشکل، متد Index را به نحو زیر تغییر دهید، به طوریکه مقدار VaryByParam به نام پارامتر متد جاری اشاره کند:
[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)
در ادامه مجددا برنامه را اجرا کنید. اکنون یکبار مقدار Test1 را به سرور ارسال کنید. سپس مقدار Test2 را ارسال نمائید. مجددا همین دو مرحله را با مقادیر Test1 و Test2 تکرار کنید. مشاهده خواهید کرد که اینبار اطلاعات بر اساس مقدار پارامتر ارسالی کش شده است.
تنظیمات متفاوت OutputCache
الف) VaryByParam : اگر مساوی none قرار گیرد، همواره همان مقدار کش شده قبلی نمایش داده میشود. اگر مقدار آن به نام پارامتر خاصی تنظیم شود، اطلاعات کش شده بر اساس مقادیر متفاوت پارامتر دریافتی، متفاوت خواهند بود. در اینجا پارامترهای متفاوت را با یک «,» میتوان از هم جدا ساخت. اگر تعداد پارامترها زیاد است میتوان مقدار VaryByParam را مساوی با * قرار داد. در این حالت به ازای مقادیر متفاوت دریافتی پارامترهای مختلف، اطلاعات مجزایی در کش قرار خواهد گرفت. این روش آخر آنچنان توصیه نمیشود چون سربار بالایی دارد و حجم بالایی از اطلاعات بر اساس پارامترهای مختلف، باید در کش قرار گیرند.
ب) Location : مکان قرارگیری اطلاعات کش شده را مشخص میکند. مقدار آن نیز بر اساس یک enum به نام OutputCacheLocation مشخص میگردد. در این حالت برای مثال میتوان مکانهای Server، Client و ServerAndClient را مقدار دهی نمود. مقدار Downstream به معنای کش شدن اطلاعات بر روی پروکسی سرورهای بین راه و یا مرورگرها است. پیش فرض آن Any است که ترکیبی از Server و Downstream میباشد.
اگر قرار است اطلاعات یکسانی به تمام کاربران نمایش داده شود، مثلا محتوای لیست یک منوی پویا، محل قرارگیری اطلاعات کش باید سمت سرور باشد. اگر نیاز است به ازای هر کاربر محتوای اطلاعات کش شده متفاوت باشد، بهتر است محل سمت کلاینت را مقدار دهی نمود.
ج) VaryByHeader : اطلاعات، بر اساس هدرهای مشخص شده، کش میشوند. برای مثال مرسوم است که از Accept-Language در اینجا استفاده شود تا اطلاعات مثلا فرانسوی کش شده، به کاربر آلمانی تحویل داده نشود.
د) VaryByCustom : در این حالت نام یک متد استاتیک تعریف شده در فایل global.asax.cs باید مشخص گردد. توسط این متد کلید رشتهای اطلاعاتی که قرار است کش شود، بازگشت داده خواهد شد.
ه) SqlDependency : در این حالت اطلاعات تا زمانیکه تغییری در جداول بانک اطلاعاتی SQL Server صورت نگیرد، کش خواهد شد.
و) Nostore : به پروکسی سرورهای بین راه و همچنین مرورگرها اطلاع میدهد که اطلاعات را نباید کش کنند. اگر قسمت اعتبار سنجی این سری را به خاطر داشته باشید، چنین تعریفی در قسمت Remote validation بکارگرفته شد:
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
و یا میتوان برای اینکار یک فیلتر سفارشی را نیز تهیه کرد:
using System;
using System.Web.Mvc;
namespace MvcApplication16.Helper
{
/// <summary>
/// Adds "Cache-Control: private, max-age=0" header,
/// ensuring that the responses are not cached by the user's browser.
/// </summary>
public class NoCachingAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
filterContext.HttpContext.Response.CacheControl = "private";
filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
}
}
}
کار این فیلتر اضافه کردن هدر «Cache-Control: private, max-age=0» به Response است.
استفاده از فایل Web.Config برای معرفی تنظیمات Caching
یکی دیگر از تنظیمات ویژگی OutputCache، پارامتر CacheProfile است که امکان تنظیم آن در فایل web.config نیز وجود دارد. برای نمونه تنظیمات زیر را به قسمت system.web فایل وب کانفیگ برنامه اضافه کنید:
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Aggressive" location="ServerAndClient" duration="300"/>
<add name="Mild" duration="100" location="Server" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
سپس مثلا برای استفاده از پروفایلی به نام Aggressive، خواهیم داشت:
[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)
استفاده از ویژگی به نام donut caching
تا اینجا به این نتیجه رسیدیم که OutputCache، کل خروجی یک View را بر اساس پارامترهای مختلفی که دریافت میکند، کش خواهد کرد. در این بین اگر بخواهیم تنها قسمت کوچکی از صفحه کش نشود چه باید کرد؟ برای حل این مشکل قابلیتی به نام cache substitution که به donut caching هم معروف است (چون آنرا میتوان به شکل یک donut تصور کرد!) در ASP.NET MVC قابل استفاده است.
@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }
همانطور که ملاحظه میکنید برای تعریف یک چنین اطلاعاتی باید از متد Response.WriteSubstitution در یک view استفاده کرد. در این مثال، نمایش زمان جاری معرفی شده، صرف نظر از وضعیت کش صفحه جاری، کش نخواهد شد.
عکس آن هم ممکن است. فرض کنید که صفحه جاری شما از سه partial view تشکیل شده است. هر کدام از این partial viewها نیز مزین به OutpuCache هستند. اما صفحه اصلی درج کننده اطلاعات این سه partial view فاقد ویژگی Output کش است. در این حالت تنها اطلاعات این partial viewها کش خواهند شد و سایر قسمتهای صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جدید به روز خواهند شد. حالت توصیه شده نیز همین مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومی درنظر داشته باشید.
استفاده از امکانات Data Caching به صورت مستقیم
مطالبی که تا اینجا عنوان شدند به کش کردن اطلاعات Response اختصاص داشتند. اما امکانات Caching موجود، به این مورد خلاصه نشده و میتوان اطلاعات و اشیاء را نیز کش کرد. برای مثال اطلاعات «با سطح دسترسی عمومی» دریافتی از بانک اطلاعاتی توسط یک کوئری را نیز میتوان کش کرد. جهت انجام اینکار میتوان از متدهای HttpRuntime.Cache.Insert و یا HttpContext.Cache.Insert استفاده کرد. استفاده از HttpContext.Cache.Insert حین نوشتن Unit tests دردسر کمتری دارد و mocking آن ساده است؛ از این جهت که بر اساس HttpContextBase تعریف شدهاست.
در ادامه یک کلاس کمکی نوشتن اطلاعات در cache و سپس بازیابی آنرا ملاحظه میکنید:
using System;
using System.Web;
using System.Web.Caching;
namespace MvcApplication16.Helper
{
public static class CacheManager
{
public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
{
if (data == null) return;
httpContext.Cache.Add(
key,
data,
null,
DateTime.Now.AddMinutes(durationMinutes),
TimeSpan.Zero,
CacheItemPriority.AboveNormal,
null);
}
public static T CacheRead<T>(this HttpContextBase httpContext, string key)
{
var data = httpContext.Cache[key];
if (data != null)
return (T)data;
return default(T);
}
public static void InvalidateCache(this HttpContextBase httpContext, string key)
{
httpContext.Cache.Remove(key);
}
}
}
و برای استفاده از آن در یک اکشن متد، ابتدا نیاز است فضای نام این کلاس تعریف شود و سپس برای نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext یکی از خواص تعریف شده در شیء کنترلر است که با ارث بری کنترلرها از آن، همواره در دسترس میباشد.
در اینجا برای نمونه اطلاعات یک لیست جنریک دریافتی از بانک اطلاعاتی را مثلا 10 دقیقه (بسته به پارامتر durationMinutes آن) میتوان کش کرد و سپس توسط متد CacheRead آنرا دریافت نمود. اگر متد CacheRead نال برگرداند به معنای خالی بودن کش است. بنابراین یکبار اطلاعات را از بانک اطلاعاتی دریافت نموده و سپس آنرا کش خواهیم کردیم.
البته هستند ORMهایی که یک چنین کارهایی را به صورت توکار پشتیبانی کنند. به مکانیزم آن، Second level cache هم گفته میشود؛ به علاوه امکان استفاده از پروایدرهای دیگری را بجز کش IIS برای ذخیره سازی موقتی اطلاعات نیز فراهم میکنند.
همچنین باید دقت داشت این اعداد مدت زمان، هیچگونه ضمانتی ندارند. اگر IIS احساس کند که با کمبود منابع مواجه شده است، به سادگی شروع به حذف اطلاعات موجود در کش خواهد کرد.
نکته امنیتی مهم!
به هیچ عنوان از OutputCache در صفحاتی که نیاز به اعتبار سنجی دارند، استفاده نکنید و به همین جهت در قسمت کش کردن اطلاعات، بر روی «اطلاعاتی با سطح دسترسی عمومی» تاکید شد.
فرض کنید کارمندی به صفحه مشاهده فیش حقوقی خودش مراجعه کرده است. این ماه هم اضافه حقوق آنچنانی داشته است. شما هم این صفحه را به مدت سه ساعت کش کردهاید. آیا میتوانید تصور کنید اگر همین گزارش کش شده با این اطلاعات، به سایر کارمندان نمایش داده شود چه قشقرقی به پا خواهد شد؟!
بنابراین هیچگاه اطلاعات مخصوص به یک کاربر اعتبار سنجی شده را کش نکنید و «تنها» اطلاعاتی نیاز به کش شدن دارند که عمومی باشند. برای مثال لیست آخرین اخبار سایت؛ لیست آخرین مدخلهای فید RSS سایت؛ لیست اطلاعات منوی عمومی سایت؛ لیست تعداد کاربران مراجعه کننده به سایت در طول یک روز؛ گزارش آب و هوا و کلیه اطلاعاتی با سطح دسترسی عمومی که کش شدن آنها مشکل ساز نباشد.
به صورت خلاصه هیچگاه در کدهای شما چنین تعریفی نباید مشاهده شود:
[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()
مراحل فعال سازی آپلود فایلها در ASP.NET Core
مرحلهی اول فعال سازی آپلود فایلها در ASP.NET Core، شامل افزودن ویژگی "enctype="multipart/form-data به یک فرم تعریف شدهاست:
<form method="post" asp-action="Index" asp-controller="TestFileUpload" enctype="multipart/form-data"> <input type="file" name="files" multiple /> <input type="submit" value="Upload" /> </form>
در سمت سرور، امضای اکشن متد دریافت کنندهی این فایلها به صورت ذیل خواهد بود:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files)
یافتن جایگزینی برای Server.MapPath در ASP.NET Core
زمانیکه فایل ارسالی، در سمت سرور دریافت شد، مرحلهی بعد، ذخیره سازی آن بر روی سرور است و از آنجائیکه ما دقیقا نمیدانیم ریشهی سایت در کدام پوشهی سرور واقع شدهاست، میشد از متد Server.MapPath برای یافتن دقیق آن کمک گرفت. با حذف این متد در ASP.NET Core، روش یافتن ریشهی سایت یا همان پوشهی wwwroot در اینجا شامل مراحل ذیل است:
public class TestFileUploadController : Controller { private readonly IHostingEnvironment _environment; public TestFileUploadController(IHostingEnvironment environment) { _environment = environment; }
پس از آن خاصیت environment.WebRootPath_ به ریشهی پوشهی wwwroot برنامه، بر روی سرور اشاره میکند. به این ترتیب میتوان مسیر دقیقی را جهت ذخیره سازی فایلهای رسیده، مشخص کرد.
امکان ذخیره سازی async فایلها در ASP.NET Core
عملیات کار با فایلها، عملیاتی است که از مرزهای IO سیستم عبور میکند. به همین جهت یکی از بهترین مثالهای پیاده سازی async، جهت رها سازی تردهای برنامه و بالا بردن میزان پاسخدهی آن با بالا بردن تعداد تردهای آزاد بیشتر است. در ASP.NET Core، نوشتن async محتوای فایل رسیده در یک stream پشتیبانی میشود و این stream میتواند یک FileStream و یا MemoryStream باشد. در ذیل نحوهی کار async با یک FileStream را مشاهده میکنید:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Index(IList<IFormFile> files) { var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } foreach (var file in files) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await file.CopyToAsync(fileStream).ConfigureAwait(false); } } return View(); }
چون برنامههای ASP.NET Core قابلیت اجرای بر روی لینوکس را نیز دارند، تا حد امکان باید از Path.Combine جهت جمع زدن اجزای مختلف یک میسر، استفاده کرد. از این جهت که در لینوکس، جداکنندهی اجزای مسیرها، / است بجای \ در ویندوز و متد Path.Combine به صورت خودکار این مسایل را لحاظ خواهد کرد.
در آخر با استفاده از متد file.CopyToAsync کار نوشتن غیرهمزمان محتوای فایل دریافتی در یک FileStream انجام میشود؛ به همین جهت در امضای متد فوق، <async Task<IActionResult را نیز ملاحظه میکنید.
پشتیبانی کامل از Model Binding آپلود فایلها در ASP.NET Core
در ASP.NET MVC 5.x اگر ویژگی Required را بر روی یک خاصیت از نوع HttpPostedFileBase قرار دهید ... کار نمیکند و در سمت کلاینت تاثیری را به همراه نخواهد داشت؛ مگر اینکه تنظیمات سمت کلاینت آنرا به صورت دستی انجام دهیم. این مشکلات در ASP.NET Core، کاملا برطرف شدهاند:
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
@model UserViewModel <form method="post" asp-action="UploadPhoto" asp-controller="TestFileUpload" enctype="multipart/form-data"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input asp-for="Photo" /> <span asp-validation-for="Photo" class="text-danger"></span> <input type="submit" value="Upload"/> </form>
اینبار جهت فعال سازی و استفادهی از قابلیتهای Model Binding میتوان از ModelState نیز بهره گرفت:
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> UploadPhoto(UserViewModel userViewModel) { if (ModelState.IsValid) { var formFile = userViewModel.Photo; if (formFile == null || formFile.Length == 0) { ModelState.AddModelError("", "Uploaded file is empty or null."); return View(viewName: "Index"); } var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } var filePath = Path.Combine(uploadsRootFolder, formFile.FileName); using (var fileStream = new FileStream(filePath, FileMode.Create)) { await formFile.CopyToAsync(fileStream).ConfigureAwait(false); } RedirectToAction("Index"); } return View(viewName: "Index"); }
بررسی پسوند فایلهای رسیدهی به سرور
ASP.NET Core دارای ویژگی است به نام FileExtensions که ... هیچ ارتباطی به خاصیتهایی از نوع IFormFile ندارد:
[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")]
در ادامه جهت بررسی پسوندهای فایلهای رسیده، میتوان یک ویژگی اعتبارسنجی سمت سرور جدید را طراحی کرد:
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public class UploadFileExtensionsAttribute : ValidationAttribute { private readonly IList<string> _allowedExtensions; public UploadFileExtensionsAttribute(string fileExtensions) { _allowedExtensions = fileExtensions.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList(); } public override bool IsValid(object value) { var file = value as IFormFile; if (file != null) { return isValidFile(file); } var files = value as IList<IFormFile>; if (files == null) { return false; } foreach (var postedFile in files) { if (!isValidFile(postedFile)) return false; } return true; } private bool isValidFile(IFormFile file) { if (file == null || file.Length == 0) { return false; } var fileExtension = Path.GetExtension(file.FileName); return !string.IsNullOrWhiteSpace(fileExtension) && _allowedExtensions.Any(ext => fileExtension.Equals(ext, StringComparison.OrdinalIgnoreCase)); } }
public class UserViewModel { [Required(ErrorMessage = "Please select a file.")] //`FileExtensions` needs to be applied to a string property. It doesn't work on IFormFile properties, and definitely not on IEnumerable<IFormFile> properties. //[FileExtensions(Extensions = ".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [UploadFileExtensions(".png,.jpg,.jpeg,.gif", ErrorMessage = "Please upload an image file.")] [DataType(DataType.Upload)] public IFormFile Photo { get; set; } }
الف) در متدهای لایه جاری خود واژههای کلیدی new و همچنین کلیه فراخوانیهای استاتیک را بیابید.
ب) وهله سازی اینها را به یک سطح بالاتر (نقطه آغازین برنامه) منتقل کنید. اینکار باید بر اساس اتکای به Abstraction و برای مثال استفاده از اینترفیسها صورت گیرد.
ج) اینکار را آنقدر تکرار کنید تا دیگر در کدهای لایه جاری خود واژه کلیدی new یا فراخوانی متدهای استاتیک مشاهده نشود.
د) در آخر وهله سازی object graphهای مورد نیاز را به یک IoC Container محول کنید.
یک مثال: ابتدا بررسی یک قطعه کد متداول
using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Web.Mvc; namespace DI06.Controllers { public class HomeController : Controller { public ActionResult Index() { string result = string.Empty; using (var client = new WebClient { Encoding = Encoding.UTF8 }) { result = client.DownloadString("https://www.dntips.ir/"); } var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(result); var title = match.Groups[1].Value.Trim(); ViewBag.PageTitle = title; return View(); } } }
مشکلات کد فوق:
الف) قرار گرفتن منطق تجاری پیاده سازی کدها مستقیما داخل کدهای یک اکشن متد؛ این مساله در دراز مدت به تکرار شدید کدها منجر خواهد شد که نهایتا قابلیت نگهداری آنرا کاهش میدهند.
ب) در این کد حداقل دو بار واژه کلیدی new ذکر شده است. مورد اول یا new WebClient، از همه مهمتر است؛ از این جهت که نوشتن آزمون واحد را برای این کنترلر بسیار مشکل میکند. آزمونهای واحد باید سریع و بدون نیاز به منابع خارجی، قابل اجرا باشند. تعویض آن هم مطابق کدهای تدارک دیده شده کار سادهای نیست.
بهبود کیفیت قطعه کد متداول فوق با استفاده از الگوی معکوس سازی وابستگیها
در اصل معکوس سازی وابستگیها عنوان کردیم لایه بالایی سیستم نباید مستقیما به لایههای زیرین در حال استفاده از آن، وابسته باشد. این وابستگی باید معکوس شده و همچنین بر اساس Abstraction یا برای مثال استفاده از اینترفیسها صورت گیرد.
به همین منظور یک پروژه دیگر را از نوع Class library، مثلا به نام DI06.Services به Solution جاری اضافه میکنیم.
namespace DI06.Services { public interface IWebClientServices { string FetchUrl(string url); string GetWebPageTitle(string url); } } using System.Net; using System.Text; using System.Text.RegularExpressions; namespace DI06.Services { public class WebClientServices : IWebClientServices { public string FetchUrl(string url) { using (var client = new WebClient { Encoding = Encoding.UTF8 }) { return client.DownloadString(url); } } public string GetWebPageTitle(string url) { var html = FetchUrl(url); var match = new Regex(@"(?s)<title>(.+?)</title>", RegexOptions.IgnoreCase).Match(html); return match.Groups[1].Value.Trim(); } } }
هنوز کار معکوس سازی وابستگیها رخ نداده است. صرفا اندکی تمیزکاری و انتقال پیاده سازی منطق تجاری به یک سری کلاسهایی با قابلیت استفاده مجدد صورت گرفته است. به این ترتیب اگر باگی در این کدها وجود داشته باشد و همچنین از آن در چندین نقطه برنامه استفاده شده باشد، اصلاح این کلاس مرکزی، به یکباره تمامی قسمتهای مختلف برنامه را تحت تاثیر مثبت قرار داده و از تکرار کدها و فراموشی احتمالی بهبود قسمتهای مشابه جلوگیری میکند.
کار معکوس سازی وابستگیها در یک لایه بالاتر صورت خواهد گرفت:
using System.Web.Mvc; using DI06.Services; namespace DI06.Controllers { public class HomeController : Controller { readonly IWebClientServices _webClientServices; public HomeController(IWebClientServices webClientServices) { _webClientServices = webClientServices; } public ActionResult Index() { ViewBag.PageTitle = _webClientServices.GetWebPageTitle("https://www.dntips.ir/"); return View(); } } }
در مورد نحوه تنظیمات اولیه یک IoC Container و یا پیشنیازهای ASP.NET MVC جهت آماده شدن برای تزریق خودکار وابستگیها در سازنده کنترلرها، پیشتر مطالبی را در این سری مطالعه کردهاید؛ در اینجا نیز اصول مورد استفاده یکی است و تفاوتی نمیکند.
فرض کنید قصد دارید یک متد async از نوع IEnumerable را که تعدادی yield return به تاخیر افتاده را به همراه دارد (yield returnها فقط زمانی اجرا میشوند که بر روی آنها متدهایی مانند ToList و یا حلقهی foreach اجرا شوند) و همچنین توسط await Task.Delay، دریافت اطلاعات به صورت async را نیز شبیه سازی میکند، تهیه کنید:
public struct Statement { public int Id { get; } public string Description { get; } public Statement(int id, string description) => (Id, Description) = (id, description); public override string ToString() => Description; } public static async Task<IEnumerable<Statement>> GetStatements(bool error) { if (error) { throw new Exception("Oops, we messed up 😬"); } await Task.Delay(1000); //Simulate waiting for data to come through. yield return new Statement(1, "C# is cool!"); yield return new Statement(2, "C# orginally named COOL."); yield return new Statement(3, "More examples..."); }
The body of 'AsyncStreams.GetStatements(bool)' cannot be an iterator block because 'Task<IEnumerable<AsyncStreams.Statement>>' is not an iterator interface type (CS1624)
برای رفع یک چنین مشکلی، اکنون در C# 8.0 میتوان از اینترفیس جدید IAsyncEnumerable بجای Task IEnumerable استفاده کرد. به این ترتیب تنها تغییری که در قطعه کد فوق نیاز است، تغییر امضای آن به صورت زیر است:
static async IAsyncEnumerable<Statement> GetStatements(bool error)
امکان تعریف حلقههای async در C# 8.0
مرحلهی بعد، ایجاد حلقهای بر روی متد GetStatements است. اکنون مشکل دیگری وجود دارد: حلقهی foreach به خودی خود، یک حلقهی synchronous است و اگر از آن برای کار با یک استریم async استفاده شود، هربار که اطلاعاتی از آن بازگشت داده میشود، پایان یک Task نیز گزارش داده خواهد شد که میتوان سبب خاتمهی حلقه شود. بنابراین انجام اینکار نیز پیش از C# 8.0 میسر نبود که اکنون با امکان تعریف await پیش از یک حلقهی foreach، ممکن شدهاست:
static async IAsyncEnumerable<Statement> GetStatementsAsync(bool error) { await foreach (var statement in GetStatements(error)) { await Task.Delay(1000); yield return statement; } }
اینترفیس IAsyncEnumerable چگونه تعریف شدهاست؟
اینترفیس IAsyncEnumerable متد GetAsyncEnumerator را تعریف میکند که یک IAsyncEnumerator را بازگشت میدهد و آن نیز به همراه متد MoveNextAsync است. اگر دقت کنید در این حالت از نگارش async اینترفیس IDisposable به نام IAsyncDisposable استفاده کردهاست:
using System.Threading; namespace System.Collections.Generic { public interface IAsyncEnumerable<out T> { IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default); } public interface IAsyncEnumerator<out T> : IAsyncDisposable { T Current { get; } ValueTask<bool> MoveNextAsync(); } } namespace System { public interface IAsyncDisposable { ValueTask DisposeAsync(); } }
مثالی از IAsyncDisposable و روش Dispose خودکار آن
با معرفی IAsyncDisposable، اگر یک مثال ساده از پیاده سازی آن به صورت زیر باشد:
public class AwaitUsingTest : IAsyncDisposable { public async ValueTask DisposeAsync() { await Task.CompletedTask; } public void Dummy() { } }
async Task FooBar() { await using var test = new AwaitUsingTest(); test.Dummy(); }
EF Code First #12
من چند روز پیش مطلبی رو راجع به ساخت attribute برای compare validation در روش code first عنوان کردم(البته در asp.net web form).که در واقع اصلیترین کاربردش همون کاربرد معروف مقایسه رمز عبور و تکرار رمز عبور هست. یه سری کارایی در این زمینه انجام دادم و خواستم در مورد مشکل مربوطه و در کل ، صحیح یا غلط بودن این روش کمکم کنید.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = true, Inherited = true)] public class CompareAttribute : ValidationAttribute { public CompareAttribute(string originalProperty, string confirmProperty) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } public override bool IsValid(object value) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value); return String.Equals(((User)value).Password, ((User)value).RepeatPassword); } }
//[CompareAttribute("Password", "RepeatPassword", ErrorMessage = "Not Compare!")] public class User { public Int64 UserId { get; set; }
[Required(ErrorMessageResourceName = "Password", ErrorMessageResourceType = typeof(ErrorMessageResource))] public String Password { get; set; }
[NotMapped] [Required(ErrorMessageResourceName = "RepeatPassword", ErrorMessageResourceType = typeof(ErrorMessageResource))] //[CompareAttribute("Password","RepeatPassword" , ErrorMessage = "Not Compare!")] public String RepeatPassword { get; set; } }
وقتی اونو بالای کلاس User میزارم کار میکنه.ولی خب این یه کار خیلی زشتیه! چون به ازای همه propertyها اجرا میشه.ولی وقتی اونو تو جای اصلیش که همون بالای repeat password هست میزارم کار نمیکنه.
یه جورایی مشکل از اینه که نمیشه انگار مقدار property رو به این سمت پاس داد.ولی در حالتی که بالای کلاس میزارمش چون خود کلاس رو پاس میده ، طبیعتاٌ
اسم propertyها رو هم پیدا میکنه.
ممنون میشم راهنماییم کنید .
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
/// <summary> /// Validate IR National Code /// </summary> /// <param name="nationalcode">National Code</param> /// <param name="lastNumber">Last Number Of National Code</param> /// <returns></returns> public static bool IsValidNationalCode(this string nationalcode, out int lastNumber) { lastNumber = -1; if (!nationalcode.IsItNumber()) return false; var invalid = new[] { "0000000000", "1111111111", "2222222222", "3333333333", "4444444444", "5555555555", "6666666666", "7777777777", "8888888888", "9999999999" }; if (invalid.Contains(nationalcode)) return false; var array = nationalcode.ToCharArray(); if (array.Length != 10) return false; var j = 10; var sum = 0; for (var i = 0; i < array.Length - 1; i++) { sum += Int32.Parse(array[i].ToString(CultureInfo.InvariantCulture)) * j; j--; } var diff = sum % 11; if (diff < 2) { lastNumber = diff; return diff == Int32.Parse(array[9].ToString(CultureInfo.InvariantCulture)); } var temp = Math.Abs(diff - 11); lastNumber = temp; return temp == Int32.Parse(array[9].ToString(CultureInfo.InvariantCulture)); }
یک نکتهی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئریهای خود ارجاعی
همانطور که در مطلب «مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجهی آن، بسیار ساده میشود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسیهای ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت میتوان به صورت زیر عمل کرد:
الف) تشکیل یک کلاس پایه برای تعریف سادهتر و مشخص رابطههای خود ارجاعی
public abstract class BaseEntity { public int Id { get; set; } } public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity where TSelfEntity : BaseEntity { public virtual TSelfEntity? Reply { set; get; } public int? ReplyId { get; set; } public virtual ICollection<TSelfEntity>? Children { get; set; } }
که ساختار معرفی شدهی در اینجا، با توضیحات موجود در متن، انطباق دارد.
ب) پر کردن درخت نهایی حاصل به صورت دستی:
چون دیگر EF این درخت را برای ما تشکیل نمیدهد، اکنون باید خودمان کار تشکیل آنرا به صورت زیر انجام دهیم:
public static class SelfReferencingExtensions { public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList) where TEntity : BaseSelfReferencingEntity<TEntity> { var results = new List<TEntity>(); if (originalList is null || originalList.Count == 0) { return results; } foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue)) { results.Add(rootItem); AppendChildren(originalList, rootItem); } return results; } private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem) where TEntity : BaseSelfReferencingEntity<TEntity> { foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id)) { parentItem.Children ??= new List<TEntity>(); parentItem.Children.Add(kid); AppendChildren(originalList, kid); } } }
در اینجا کار تشکیل درخت نهایی، با استفاده از یک متد بازگشتی، انجام میشود.
پس از این مقدمات، نحوهی استفاده از آن به صورت زیر است:
var comments = await _comments.AsNoTracking() .Where(x => x.ParentId == postId) .OrderBy(x => x.Id) .Take(count) .ToListAsync(); var commentsTree = comments.ToSelfReferencingTree();
کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکتهی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده میشوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخها را به صورت خودکار انجام میدهد.
معرفی الگوی Repository
روش متداول کار با فناوریهای مختلف دسترسی به دادهها عموما بدین شکل است:
الف) یافتن رشته اتصالی رمزنگاری شده به دیتابیس از یک فایل کانفیگ (در یک برنامه اصولی البته!)
ب) باز کردن یک اتصال به دیتابیس
ج) ایجاد اشیاء Command برای انجام عملیات مورد نظر
د) اجرا و فراخوانی اشیاء مراحل قبل
ه) بستن اتصال به دیتابیس و آزاد سازی اشیاء
اگر در برنامههای یک تازه کار به هر محلی از برنامه او دقت کنید این 5 مرحله را میتوانید مشاهده کنید. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمایش و ...
مشکلات این روش:
1- حجم کارهای تکراری انجام شده بالا است. اگر قسمتی از فناوری دسترسی به دادهها را به اشتباه درک کرده باشد، پس از مطالعه بیشتر و مشخص شدن نحوهی رفع مشکل، قسمت عمدهای از برنامه را باید اصلاح کند (زیرا کدهای تکراری همه جای آن پراکندهاند).
2- برنامه نویس هر بار باید این مراحل را به درستی انجام دهد. اگر در یک برنامه بزرگ تنها قسمت آخر در یکی از مراحل کاری فراموش شود دیر یا زود برنامه تحت فشار کاری بالا از کار خواهد افتاد (و متاسفانه این مساله بسیار شایع است).
3- برنامه منحصرا برای یک نوع دیتابیس خاص تهیه خواهد شد و تغییر این رویه جهت استفاده از دیتابیسی دیگر (مثلا کوچ برنامه از اکسس به اس کیوال سرور)، نیازمند بازنویسی کل برنامه میباشد.
و ...
همین برنامه نویس پس از مدتی کار به این نتیجه میرسد که باید برای اینکارهای متداول، یک لایه و کلاس دسترسی به دادهها را تشکیل دهد. اکنون هر قسمتی از برنامه برای کار با دیتابیس باید با این کلاس مرکزی که انجام کارهای متداول با دیتابیس را خلاصه میکند، کار کند. به این صورت کد نویسی یک نواختی با حذف کدهای تکراری از سطح برنامه و همچنین بدون فراموش شدن قسمت مهمی از مراحل کاری، حاصل میگردد. در اینجا اگر روزی قرار شد از یک دیتابیس دیگر استفاده شود فقط کافی است یک کلاس برنامه تغییر کند و نیازی به بازنویسی کل برنامه نخواهد بود.
این روزها تشکیل این لایه دسترسی به دادهها (data access layer یا DAL) نیز مزموم است! و دلایل آن در مباحث چرا به یک ORM نیازمندیم برشمرده شده است. جهت کار با ORM ها نیز نیازمند یک لایه دیگر میباشیم تا یک سری اعمال متداول با آنهارا کپسوله کرده و از حجم کارهای تکراری خود بکاهیم. برای این منظور قبل از اینکه دست به اختراع بزنیم، بهتر است به الگوهای طراحی برنامه نویسی شیء گرا رجوع کرد و از رهنمودهای آن استفاده نمود.
الگوی Repository یکی از الگوهای برنامه نویسی با مقیاس سازمانی است. با کمک این الگو لایهای بر روی لایه نگاشت اشیاء برنامه به دیتابیس تشکیل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه میکند. به این صورت هم از تشکیل یک سری کدهای تکراری در سطح برنامه جلوگیری شده و هم از وابستگی بین مدل برنامه و لایه دسترسی به دادهها (که در اینجا همان NHibernate میباشد) جلوگیری میشود. الگوی Repository (مخزن)، کار ثبت، حذف، جستجو و به روز رسانی دادهها را با ترجمه آنها به روشهای بومی مورد استفاده توسط ORM مورد نظر، کپسوله میکند. به این شکل شما میتوانید یک الگوی مخزن عمومی را برای کارهای خود تهیه کرده و به سادگی از یک ORM به ORM دیگر کوچ کنید؛ زیرا کدهای برنامه شما به هیچ ORM خاصی گره نخورده و این عملیات بومی کار با ORM توسط لایهای که توسط الگوی مخزن تشکیل شده، صورت گرفته است.
طراحی کلاس مخزن باید شرایط زیر را برآورده سازد:
الف) باید یک طراحی عمومی داشته باشد و بتواند در پروژههای متعددی مورد استفاده مجدد قرار گیرد.
ب) باید با سیستمی از نوع اول طراحی و کد نویسی و بعد کار با دیتابیس، سازگاری داشته باشد.
ج) باید امکان انجام آزمایشات واحد را سهولت بخشد.
د) باید وابستگی کلاسهای دومین برنامه را به زیر ساخت ORM مورد استفاده قطع کند (اگر سال بعد به این نتیجه رسیدید که ORM ایی به نام XYZ برای کار شما بهتر است، فقط پیاده سازی این کلاس باید تغییر کند و نه کل برنامه).
ه) باید استفاده از کوئریهایی از نوع strongly typed را ترویج کند (مثل کوئریهایی از نوع LINQ).
بررسی مدل برنامه
مدل این قسمت (برنامه NHSample4 از نوع کنسول با همان ارجاعات متداول ذکر شده در قسمتهای قبل)، از نوع many-to-many میباشد. در اینجا یک واحد درسی توسط چندین دانشجو میتواند اخذ شود یا یک دانشجو میتواند چندین واحد درسی را اخذ نماید که برای نمونه کلاس دیاگرام و کلاسهای متشکل آن به شکل زیر خواهند بود:
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }
public Course()
{
Students = new List<Student>();
}
}
}
using System.Collections.Generic;
namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }
public Student()
{
Courses = new List<Course>();
}
}
}
کلاس کانفیگ برنامه جهت ایجاد نگاشتها و سپس ساخت دیتابیس متناظر
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}
public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
الف) با توجه به اینکه برنامه از نوع ویندوزی است، برای مدیریت صحیح کانکشن استرینگ، فایل App.Config را به برنامه افروده و محتویات آنرا به شکل زیر تنظیم میکنیم (تا کلید DbConnectionString توسط متد GetConfig مورد استفاده قرارگیرد ):
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>
ب) در NHibernate سنتی (!) کار ساخت نگاشتها توسط یک سری فایل xml صورت میگیرد که با معرفی فریم ورک Fluent NHibernate و استفاده از قابلیتهای Auto Mapping آن، اینکار با سهولت و دقت هر چه تمامتر قابل انجام است که توضیحات نحوهی انجام آنرا در قسمتهای قبل مطالعه فرمودید. اگر نیاز بود تا این فایلهای XML نیز جهت بررسی شخصی ایجاد شوند، تنها کافی است از متد ExportTo آن همانگونه که در متد GetConfig استفاده شده، کمک گرفته شود. به این صورت پس از ایجاد خودکار نگاشتها، فایلهای XML متناظر نیز در مسیری که به عنوان آرگومان متد ExportTo مشخص گردیده است، تولید خواهند شد (دو فایل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشهای که محل اجرای برنامه است خواهید یافت).
با فراخوانی متد CreateDb این کلاس، پس از ساخت خودکار نگاشتها، database schema متناظر، در دیتابیسی که توسط کانکشن استرینگ برنامه مشخص شده، ایجاد خواهد شد که دیتابیس دیاگرام آنرا در شکل ذیل مشاهده مینمائید (جداول دانشجویان و واحدها هر کدام به صورت موجودیتی مستقل ایجاد شده که ارجاعات آنها در جدولی سوم نگهداری میشود).
پیاده سازی الگوی مخزن
اینترفیس عمومی الگوی مخزن به شکل زیر میتواند باشد:
using System;
using System.Linq;
using System.Linq.Expressions;
namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);
T Save(T entity);
T Update(T entity);
void Delete(T entity);
IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}
سپس پیاده سازی آن با توجه به کلاس SingletonCore ایی که در قسمت قبل تهیه کردیم (جهت مدیریت صحیح سشن فکتوری)، به صورت زیر خواهد بود.
این کلاس کار آغاز و پایان تراکنشها را نیز مدیریت کرده و جهت سهولت کار اینترفیس IDisposable را نیز پیاده سازی میکند :
using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;
namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;
public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}
~Repository()
{
Dispose(false);
}
public T Get(object key)
{
if (!isSessionSafe) return default(T);
return _session.Get<T>(key);
}
public T Save(T entity)
{
if (!isSessionSafe) return default(T);
_session.Save(entity);
return entity;
}
public T Update(T entity)
{
if (!isSessionSafe) return default(T);
_session.Update(entity);
return entity;
}
public void Delete(T entity)
{
if (!isSessionSafe) return;
_session.Delete(entity);
}
public IQueryable<T> Find()
{
if (!isSessionSafe) return null;
return _session.Linq<T>();
}
public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;
return Find().Where(predicate);
}
void Commit()
{
if (!isSessionSafe) return;
if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}
void Rollback()
{
if (!isSessionSafe) return;
if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}
private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}
void BeginTransaction()
{
if (!isSessionSafe) return;
_session.BeginTransaction();
}
public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;
try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}
_disposed = true;
}
}
}
using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;
namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ایجاد دیتابیس در صورت نیاز
//NHSessionManager.Config.CreateDb();
//ابتدا یک دانشجو را اضافه میکنیم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}
//سپس یک واحد را اضافه میکنیم
using (var courseRepo = new Repository<Course>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}
//اکنون یک واحد را به دانشجو انتساب میدهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}
//سپس شماره دروس استادی خاص را نمایش میدهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");
foreach (var course in query)
Console.WriteLine(course.Id);
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور که ملاحظه میکنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوهی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنشها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت میکند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن میتوان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.
دریافت سورس برنامه قسمت هشتم
ادامه دارد ...