public class Post { public int Id { get; set; } public string Title { get; set; } public DateTime dt { get; set; } } static void Main(string[] args) { List<Post> ListOfPost = new List<Post>(); DateTime dt = DateTime.Now; PersianCalendar pc = new PersianCalendar(); int day = pc.GetDayOfMonth(dt); int month = pc.GetMonth(dt); int year = pc.GetYear(dt); int DaysInMonth = pc.GetDaysInMonth(year, month); DateTime FirstDayOfCurrentMonth = dt.AddDays(-day).Date; DateTime LastDayOfCurrentMonth = FirstDayOfCurrentMonth.AddDays(DaysInMonth); var query = ListOfPost .Where(x => x.dt.Date > FirstDayOfCurrentMonth.Date) .Where(x => x.dt.Date <= LastDayOfCurrentMonth.Date) .ToList(); }
Lazy loading در تزریق وابستگیها به کمک StructureMap
public interface IUnitOfWork { Lazy<IDbSet<TEntity>> LazySet<TEntity>() where TEntity : class; int SaveChanges(); }
Lazy <IDbSet<TEntity>> LazySet<TEntity>() where TEntity : class
برای نمونه متد زیر را درنظر بگیرید:
public static string CurrentInvariantMonthName() { var month = DateTimeOffset.UtcNow.Month; return CultureInfo .InvariantCulture .DateTimeFormat .GetMonthName(month); }
برای حل این مشکل، با استفاده از ویژگی global using statements و compiler directives، میتوان دو مفهوم متفاوت را برای زمان ارائه داد:
#if MOCK_TIME global using DateTimeOffset = StubDateTimeOffset; #else global using DateTimeOffset = System.DateTimeOffset; #endif public static class StubDateTimeOffset { private static System.DateTimeOffset? value; public static System.DateTimeOffset UtcNow => value ?? System.DateTimeOffset.UtcNow ; public static void Set(System.DateTimeOffset dateTimeOffset) { value = dateTimeOffset; } public static void Reset() => value = null; }
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <DefineConstants>MOCK_TIME</DefineConstants> </PropertyGroup>
// set time StubDateTimeOffset.Set(new(new(2022, 7, 1)));
تولید پویای کد در زمان اجرا توسط Reflection.Emit
Reflection چیست؟
Reflection چیزهایی هستند که با نگاه در یک آینه قابل مشاهدهاند. در این حالت شخص میتواند قسمتهای مختلف ظاهر خود را برانداز کرده یا قسمتی را تغییر دهید. اما این مساله چه ربطی به دنیای دات نت دارد؟ در دات نت با استفاده از Reflection میتوان به اطلاعات اشیاء یک برنامهی در حال اجرا دسترسی یافت. برای مثال نام کلاسهای مختلف آن چیست یا درون کلاسی خاص، چه متدهایی قرار دارند. همچنین با استفاده از Reflection میتوان رفتارهای جدیدی را نیز به کلاسها و اشیاء افزود یا آنها را تغییر داد.
همواره عنوان میشود که از Reflection به دلیل سربار بالای آن پرهیز کنید و تنها از آن به عنوان آخرین راه حل موجود استفاده نمائید و این دقیقا موردی است که در مباحث جاری بیشتر از آن استفاده خواهد شد: ساخت اشیاء جدید در زمان اجرا به کمک کدهای IL و امکانات Reflection
نگاهی به امکانات متداول Reflection
در مثال بعد، نگاهی خواهیم داشت به امکانات متداول Reflection، مانند دسترسی به متدها و خواص یک کلاس و تعویض مقدار یا فراخوانی آنها:
using System; namespace FastReflectionTests { class Person { public string Name { set; get; } public string Speak() { return string.Format("Hello, my name is {0}.", this.Name); } } class Program { static void Main(string[] args) { //روش متداول var vahid = new Person { Name = "Vahid" }; Console.WriteLine(vahid.Speak()); var type = vahid.GetType(); //نمایش متدهای یک کلاس var methods = type.GetMethods(); foreach (var method in methods) { Console.WriteLine(method.Name); } //تغییر مقدار یک خاصیت var setNameMethod = type.GetMethod("set_Name"); setNameMethod.Invoke(obj: vahid, parameters: new[] { "Ali" }); //فراخوانی یک متد var speakMethod = type.GetMethod("Speak"); var result = speakMethod.Invoke(obj: vahid, parameters: null); Console.WriteLine(result); } } }
Hello, my name is Vahid. set_Name get_Name Speak ToString Equals GetHashCode GetType Hello, my name is Ali.
در اینجا یک کلاس شخص با خاصیت نام او تعریف شده است؛ به همراه متدی که رشتهای را نمایش خواهد داد.
در متد Main برنامه، ابتدا یک وهله جدید از این شخص ایجاد شده و سپس به روش متداول، متد Speak آن فراخوانی گردیده است. در ادامه کار از امکانات Reflection برای انجام همین امور کمک گرفته شده است.
کار با دریافت نوع یک وهله شروع میشود. برای نمونه در اینجا توسط vahid.GetType به نوع وهله ساخته شده دسترسی یافتهایم. سپس با داشتن این type، میتوان به کلیه امکانات Reflection دسترسی یافت. برای مثال توسط GetMethods، لیست کلیه متدهای موجود در کلاس شخص بازگشت داده میشود.
اگر به خروجی فوق دقت کنید، پس از سطر اول، 7 سطر بعدی نمایانگر متدهای موجود در کلاس شخص هستند. شاید عنوان کنید که این کلاس به نظر یک متد بیشتر ندارد. اما در دات نت اشیاء از شیء Object مشتق میشوند و چهار متد ToString، Equals، GetHashCode و GetType متعلق به آن هستند. همچنین خواص تعریف شده نیز در اصل به دو متد set و get به صورت خودکار در کدهای IL برنامه ترجمه خواهند شد. از همین متد set_Name در ادامه برای مقدار دهی خاصیت نام وهله ایجاد شده استفاده شده است.
همانطور که ملاحظه میکنید برای فراخوانی یک وهله از طریق Reflection، ابتدا توسط متد type.GetMethod میتوان به آن دسترسی یافت و سپس با فراخوانی متد Invoke، میتوان متد مدنظر را بر روی یک شیء مهیا با پارامترهایی که ذکر میکنیم، فراخوانی کرد. اگر این متد پارامتری ندارد، آنرا نال قرار خواهیم داد.
تا اینجا مقدمهای را ملاحظه نمودید که بیشتر جهت تکمیل بحث، حفظ روابط منطقی قسمتهای مختلف آن و یادآوری مباحث مرتبط با Reflection ذکر شدند.
ایجاد اشیاء در زمان اجرای برنامه
یکی از کلاسهای مهم Reflection که در منابع مختلف کمتر به آن پرداخته شده است، کلاس DynamicMethod آن است که از آن میتوان برای ایجاد اشیاء و یا متدهایی پویا در زمان اجرا استفاده کرد. این کلاس قرار گرفته در فضای نام System.Reflection.Emit، دارای یک ILGenerator است که میتوان به آن OpCodeهایی را اضافه کرد. زمانیکه کار ایجاد این متدپویا به پایان رسید، با استفاده از Delegates امکان دسترسی و اجرای این متد پویا وجود خواهد داشت.
یک مثال کامل را در این زمینه در ادامه ملاحظه مینمائید:
using System; using System.Reflection.Emit; namespace FastReflectionTests { class Program { static double Divider(int a, int b) { return a / b; } delegate double DividerDelegate(int a, int b); static void Main(string[] args) { //روش متداول Console.WriteLine(Divider(10, 2)); //تعریف امضای متد var myMethod = new DynamicMethod( name: "DividerMethod", returnType: typeof(double), parameterTypes: new[] { typeof(int), typeof(int) }, m: typeof(Program).Module); //تعریف بدنه متد var il = myMethod.GetILGenerator(); il.Emit(opcode: OpCodes.Ldarg_0); //بارگذاری پارامتر اول بر روی پشته ارزیابی il.Emit(opcode: OpCodes.Ldarg_1); //بارگذاری پارامتر دوم بر روی پشته ارزیابی il.Emit(opcode: OpCodes.Div); // دو پارامتر از پشته ارزیابی دریافت و تقسیم خواهند شد il.Emit(opcode: OpCodes.Ret); // دریافت نتیجه نهایی از پشته ارزیابی و بازگشت آن //فراخوانی متد پویا //روش اول var result = myMethod.Invoke(obj: null, parameters: new object[] { 10, 2 }); Console.WriteLine(result); //روش دوم var method = (DividerDelegate)myMethod.CreateDelegate(delegateType: typeof(DividerDelegate)); Console.WriteLine(method(10, 2)); } } }
در ابتدای این مثال جدید یک متد متداول تقسیم کننده دو عدد را ملاحظه میکنید. در ادامه قصد داریم overload دیگری از این متد را توسط کدهای MSIL در زمان اجرا ایجاد کنیم که دو پارامتر int را قبول میکند.
کار با وهله سازی کلاس DynamicMethod موجود در فضای نام System.Reflection.Emit شروع میشود. در اینجا کار تعریف امضای متد جدید باید صورت گیرد. برای مثال نام آن چیست، نوع خروجی آن کدام است. نوع پارامترهای آن چیست و نهایتا این متدی که قرار است به صورت پویا به برنامه اضافه شود، باید در کجا قرار گیرد. برای اینکار از Module خود کلاس Program برنامه استفاده شده است.
پس از تعریف امضای متد پویا، نوبت به تعریف بدنهی آن میرسد. کار با دریافت یک ILGenerator که میتوان در آن کدهای IL را وارد کرد شروع میشود. مابقی آن تعریف کدهای IL توسط متد Emit است و پیشتر با مقدمات اسمبلی دات نت در قسمتهای قبلی مبحث جاری آشنا شدهایم. ابتدا دو Ldarg فراخوانی شدهاند تا دو پارامتر ورودی متد را دریافت کنند. سپس Div بر روی آنها صورت گرفته و نهایتا نتیجه بازگشت داده شده است.
خوب؛ تا اینجا موفق شدیم اولین متد پویای خود را ایجاد نمائیم. برای اجرا آن حداقل دو روش وجود دارد:
الف) فراخوانی متد Invoke بر روی آن. با توجه به اینکه قرار نیست این متد بر روی وهلهی خاصی اجرا شود، اولین پارامتر آن null وارد شده است و سپس پارامترهای این متد پویا توسط آرگومان دوم متد Invoke وارد شدهاند.
ب) میتوان این عملیات را اندکی شکیلتر کرد. برای اینکار پیش از متد Main برنامه یک delegate به نام DividerDelegate تعریف شده است. سپس با استفاده از متد CreateDelegate، خروجی این متد پویا را تبدیل به یک delegate کردهایم. اینبار فراخوانی متد پویا بسیار شبیه به متدهای معمولی میشود.
<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>http://www.example.com/</loc> <lastmod>2005-01-01</lastmod> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> </urlset>
که یک فایل XML متشکل از یک تگ urlset است و این تگ نیز حاوی یک یا چند تگ url میباشد. با توجه به تعاریف بالا به یک چنین کلاسی خواهیم رسید:
public enum ChangeFreq { Always, Hourly, Daily, Weekly, Monthly, Yearly, Never } [XmlElement("loc")] public string Url { get; set; } [XmlElement("lastmod")] public DateTime? LastModified { get; set; } public bool ShouldSerializeLastModified() { return LastModified.HasValue; } [XmlElement("changefreq")] public ChangeFreq? ChangeFrequency { get; set; } public bool ShouldSerializeChangeFrequency() { return ChangeFrequency.HasValue; } [XmlElement("priority")] public float? Priority { get; set; } public bool ShouldSerializePriority() { return Priority.HasValue; } }
دقت داشته باشید که چون پروپرتیهای LastModified ، ChangeFrequency و Priority از نوع Nullable تعریف شدهاند، پس باید کاری کنیم در صورتیکه این پروپرتیها نال بودند سریالیز نشوند. بدین منظور از تابع ShouldSerialize[MemberName] استفاده میشود. این تابع جزئی از دات نت است. کافی است بعد از ShouldSerialize نام پروپرتی را ذکر کنید. حال به کلاس دیگری نیاز داریم تا لیستی از کلاس فوق را دربر داشته باشد.
[XmlRoot("urlset",Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")] public class SiteMp { private readonly List<Location> _locations; public SiteMp() { _locations = new List<Location>(); } [XmlElement("url")] public List<Location> Locations { get { return _locations; } set { foreach (var location in value) { Add(location); } } } public void Add(Location location) { _locations.Add(location); } }
حال برای پردازش کلاس بالا لازم است ActionResultی را طراحی کنیم تا خروجی Response را به فرمت XML پردازش کند:
public class XmlResult : ActionResult { private readonly object _objectToSerialize; public XmlResult(object objectToSerialize) { _objectToSerialize = objectToSerialize; } public override void ExecuteResult(ControllerContext context) { if (_objectToSerialize == null) return; context.HttpContext.Response.Clear(); var xmlSerializer = new XmlSerializer(_objectToSerialize.GetType()); context.HttpContext.Response.ContentType = "text/xml"; xmlSerializer.Serialize(context.HttpContext.Response.Output, _objectToSerialize); } }
و در آخر یک کنترلر ساخته و به صورت زیر از آن استفاده میکنیم:
public class SiteMapController : Controller { // GET: SiteMap public ActionResult Index() { SiteMp siteMap = new SiteMp(); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/Index" }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/NewRequest", ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/FindRequest", ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); siteMap.Add(new Location { Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/ContactUs/Index", ChangeFrequency = Location.ChangeFreq.Daily, LastModified = DateTime.UtcNow, Priority = 0.5f }); return new XmlResult(siteMap); }
اگر دقت کنید لینکهای ثابت باید به صورت دستی اضافه شوند. سناریویی را تصور کنید که لینکها زیاد باشند(جدای از لینک هایی که از دیتابیس لود میشوند) این کار کمی ناجور به نظر میرسد. در اینجا میخواهیم از طریق امکانات ،Reflection عمل اضافه کردن لینک به صورت خودکار انجام شود.
public class ControllerScanner { public static List<string> ScanAllControllers(HttpRequestBase requestBase) { Assembly asm = Assembly.GetAssembly(typeof(MvcApplication)); var controllerActionlist = asm.GetTypes() .Where(type => typeof (Controller).IsAssignableFrom(type)) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public)) .Where((returnType => returnType.ReturnType == (typeof(ViewResult)) || returnType.ReturnType==(typeof(ActionResult)))) .Select( x => new { Controller = x.DeclaringType.Name, Action = x.Name, ReturnType = x.ReturnType.Name }) .OrderBy(x => x.Controller).ThenBy(x => x.Action).Distinct().ToList(); if (requestBase.Url == null) return null; var url = requestBase.Url.GetLeftPart(UriPartial.Authority); return controllerActionlist.Select(controller => $"{url}/{controller.Controller}/{controller.Action}").ToList(); } }
حال از کلاس بالا در کنترلر SiteMap به صورت زیر استفاده میکنیم :
public class SiteMapController : Controller { // GET: SiteMap public ActionResult Index() { var siteMap = new SiteMap(); var controllers = ControllerScanner.ScanAllControllers(Request); foreach (var controller in controllers) { siteMap.Add(new Location { Url = controller, ChangeFrequency = Location.ChangeFreq.Always, LastModified = DateTime.UtcNow, Priority = 0.5f }); } return new XmlResult(siteMap); } }
در آخر نیز سطر زیر را به سیستم مسیریابی اضافه نمایید تا در صورت درخواست فایل sitemap.xml اکشن Index از کنترلر SiteMap فراخوانی شود.
routes.MapRoute( "SiteMap", // Route name "sitemap.xml", // URL with parameters new { controller = "Sitemap", action = "Index", name = UrlParameter.Optional, area = "" } );
در این قسمت یک مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمتهای قبل با هم مرور خواهیم کرد. برای سادگی کار از یک برنامه Console استفاده خواهد شد (هر چند مرسوم شده است که برای نوشتن آزمایشات از آزمونهای واحد بجای این نوع پروژهها استفاده شود). همچنین فرض هم بر این است که database schema برنامه را مطابق قسمت قبل در اس کیوال سرور ایجاد کرده اید (نکته آخر بحث قسمت سوم).
یک پروژه جدید از نوع کنسول را به solution برنامه (همان NHSample1 که در قسمتهای قبل ایجاد شد)، اضافه نمائید.
سپس ارجاعاتی را به اسمبلیهای زیر به آن اضافه کنید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمتهای قبل تعاریف موجودیتها و نگاشت آنها را در این پروژه class library ایجاد کرده بودیم و اکنون قصد استفاده از آن را داریم.
اگر دیتابیس قسمت قبل را هنوز ایجاد نکردهاید، کلاس CDb را به برنامه افزوده و سپس متد CreateDb آنرا به برنامه اضافه نمائید.
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;
namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);
PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);
var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());
در ادامه یک کلاس جدید به نام Config را به برنامه کنسول ایجاد شده اضافه کنید:
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;
namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اکنون سورس کامل مثال برنامه را در نظر بگیرید:
کلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:
using System;
using NHibernate;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;
public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}
public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};
Console.WriteLine("Saving a customer...");
session.Save(vahid);
session.Flush();//چندین عملیات با هم و بعد
transaction.Commit();
return vahid.Id;
}
}
}
public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);
Console.WriteLine("Deleting a customer...");
session.Delete(customer);
session.Flush();//چندین عملیات با هم و بعد
transaction.Commit();
}
}
}
}
}
using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;
//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
نیاز است تا ISessionFactory را برای ساخت سشنهای دسترسی به دیتابیس ذکر شده در تنظمیات آن جهت استفاده در تمام تردهای برنامه، ایجاد نمائیم. لازم به ذکر است که تا قبل از فراخوانی BuildSessionFactory این تنظیمات باید معرفی شده باشند و پس از آن دیگر اثری نخواهند داشت.
ایجاد شیء ISessionFactory هزینه بر است و گاهی بر اساس تعداد کلاسهایی که باید مپ شوند، ممکن است تا چند ثانیه به طول انجامد. به همین جهت نیاز است تا یکبار ایجاد شده و بارها مورد استفاده قرار گیرد. در برنامه به کرات از using استفاده شده تا اشیاء IDisposable را به صورت خودکار و حتمی، معدوم نماید.
بررسی متد AddNewCustomer :
در ابتدا یک سشن را از ISessionFactory موجود درخواست میکنیم. سپس یکی از بهترین تمرینهای کاری جهت کار با دیتابیسها ایجاد یک تراکنش جدید است تا اگر در حین اجرای کوئریها مشکلی در سیستم، سخت افزار و غیره پدید آمد، دیتابیسی ناهماهنگ حاصل نشود. زمانیکه از تراکنش استفاده شود، تا هنگامیکه دستور transaction.Commit آن با موفقیت به پایان نرسیده باشد، اطلاعاتی در دیتابیس تغییر نخواهد کرد و از این لحاظ استفاده از تراکنشها جزو الزامات یک برنامه اصولی است.
در ادامه یک وهله از شیء Customer را ایجاد کرده و آنرا مقدار دهی میکنیم (این شیء در قسمتهای قبل ایجاد گردید). سپس با استفاده از session.Save دستور ثبت را صادر کرده، اما تا زمانیکه transaction.Commit فراخوانی و به پایان نرسیده باشد، اطلاعاتی در دیتابیس ثبت نخواهد شد.
نیازی به ذکر سطر فلاش در این مثال نبود و NHibernate اینکار را به صورت خودکار انجام میدهد و فقط از این جهت عنوان گردید که اگر چندین عملیات را با هم معرفی کردید، استفاده از session.Flush سبب خواهد شد که رفت و برگشتها به دیتابیس حداقل شود و فقط یکبار صورت گیرد.
در پایان این متد، Id ثبت شده در دیتابیس بازگشت داده میشود.
چون در متد CreateSessionFactory ، متد ShowSql را نیز ذکر کرده بودیم، هنگام اجرای برنامه، عبارات SQL ایی که در پشت صحنه توسط NHibernate تولید میشوند را نیز میتوان مشاهده نمود:
بررسی متد DeleteCustomer :
ایجاد سشن و آغاز تراکنش آن همانند متد AddNewCustomer است. سپس در این سشن، یک شیء از نوع Customer با Id ایی مشخص load خواهد گردید. برای نمونه، نام این مشتری نیز در کنسول نمایش داده میشود. سپس این شیء مشخص و بارگذاری شده را به متد session.Delete ارسال کرده و پس از فراخوانی transaction.Commit ، این مشتری از دیتابیس حذف میشود.
برای نمونه خروجی SQL پشت صحنه این عملیات که توسط NHibernate مدیریت میشود، به صورت زیر است:
Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
NHibernate: INSERT INTO [Customer] (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
فرض کنید از هفته آینده قرار شده است که نسخه سبک و تک کاربرهای از برنامه ما تهیه شود. بدیهی است SQL server برای این منظور انتخاب مناسبی نیست (هزینه بالا برای یک مشتری، مشکلات نصب، مشکلات نگهداری و امثال آن برای یک کاربر نهایی و نه یک سازمان بزرگ که حتما ادمینی برای این مسایل در نظر گرفته میشود).
اکنون چه باید کرد؟ باید برنامه را از صفر بازنویسی کرد یا قسمت دسترسی به دادههای آنرا کلا مورد باز بینی قرار داد؟ اگر برنامه اسپاگتی ما اصلا لایه دسترسی به دادهها را نداشت چه؟! همه جای برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از یک دیتابیس دیگر یعنی باز نویسی کل برنامه.
همانطور که ملاحظه میکنید، زمانیکه با NHibernate کار شود، مدیریت لایه دسترسی به دادهها به این فریم ورک محول میشود و اکنون برای استفاده از دیتابیس SQLite تنها باید تغییرات زیر صورت گیرد:
ابتدا ارجاعی را به اسمبلی System.Data.SQLite.dll اضافه نمائید (تمام این اسمبلیهای ذکر شده به همراه مجموعه FluentNHibernate ارائه میشوند). سپس:
الف) ایجاد یک دیتابیس خام بر اساس کلاسهای domain و mapping تعریف شده در قسمتهای قبل به صورت خودکار
CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...
دریافت سورس برنامه تا این قسمت
نکته:
در سه قسمت قبل، تمام خواص پابلیک کلاسهای پوشه domain را به صورت معمولی و متداول معرفی کردیم. اگر نیاز به lazy loading در برنامه وجود داشت، باید تمامی کلاسها را ویرایش کرده و واژه کلیدی virtual را به کلیه خواص پابلیک آنها اضافه کرد. علت هم این است که برای عملیات lazy loading ، فریم ورک NHibernate باید یک سری پروکسی را به صورت خودکار جهت کلاسهای برنامه ایجاد نماید و برای این امر نیاز است تا بتواند این خواص را تحریف (override) کند. به همین جهت باید آنها را به صورت virtual تعریف کرد. همچنین تمام سطرهای Not.LazyLoad نیز باید حذف شوند.
ادامه دارد ...
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider
public class Startup
{
public void ConfigureServices(IServiceCollections services)
{
services.AddMvc(); // موجب فعال شدن «صفحات» و کنترلرها میشود
}
public void Configure(IApplicationBuilder app)
{
app.UseMvc();
}
}
@page @{ var message = "Hello, World!"; } <html> <body> <p>@message</p> </body> </html>
URL متناظر | نام فایل و مسیر آن |
/ یا /Index | /Pages/Index.cshtml |
/Contact | /Pages/Store/Contact.cshtml |
/Store/Contact | /Pages/Store/Contact.cshtml |
using System.ComponentModel.DataAnnotations; namespace MyApp { public class Contact { [Required] public string Name { get; set; } [Required] public string Email { get; set; } } }
@page @using MyApp @using Microsoft.AspNetCore.Mvc.RazorPages @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @inject ApplicationDbContext Db @functions { [BindProperty] public Contact Contact { get; set; } public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { Db.Contacts.Add(Contact); await Db.SaveChangesAsync(); return RedirectToPage(); } return Page(); } } <html> <body> <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند</p> <div asp-validation-summary="All"></div> <form method="POST"> <div>Name: <input asp-for="Contact.Name" /></div> <div>Email: <input asp-for="Contact.Email" /></div> <input type="submit" /> </form> </body> </html>
- اگر خطایی نبود، اطلاعات ذخیره شده و به صفحۀ دیگر ریدایرکت میشود.
- درغیراینصورت، صفحه را دوباره بههمراه خطاهای اعتبار سنجی نمایش میدهد.
- اگر اطلاعات موفقتآمیز وارد شوند، آنگاه متد هندلر OnPostAsync، هلپر RedirctToPage را برای برگرداندن نمونهای از RedirectToPageResult فراخوانی میکند. این یک نوع جدید بازگشتی برای اکشن متد است که شبیه RedirectToAction یا RedirectToRoute است؛ با این تفاوت که مخصوص صفحات طراحی شده است.
@page @using MyApp @using MyApp.Pages @using Microsoft.AspNetCore.Mvc.RazorPages @addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" @model ContactModel <html> <body> <p>فرم زیر را پر کنید تا در اسرع وقت، کارشناسان ما با شما بگیرند </p> <div asp-validation-summary="All"></div> <form method="POST"> <div>Name: <input asp-for="Contact.Name" /></div> <div>Email: <input asp-for="Contact.Email" /></div> <input type="submit" /> </form> </body> </html>
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace MyApp.Pages { public class ContactModel : PageModel { public ContactModel(ApplicationDbContext db) { Db = db; } [BindProperty] public Contact Contact { get; set; } private ApplicationDbContext Db { get; } public async Task<IActionResult> OnPostAsync() { if (ModelState.IsValid) { Db.Contacts.Add(Contact); await Db.SaveChangesAsync(); return RedirectToPage(); } return Page(); } } }
نگاهی به نحوهی عملکرد سرویسها و تزریق وابستگیها در AngularJS 2.0
فرض کنید کلاس سرویسی، به نحو ذیل تعریف شدهاست:
export class MyService {}
let svc = new MyService();
همچنین در این حالت، mocking این سرویس برای نوشتن unit tests نیز مشکل میباشد.
راه بهتر و توصیه شدهی در اینجا، ثبت و معرفی این سرویسها به AngularJS 2.0 است. سپس AngularJS 2.0 به ازای هر کلاس سرویس معرفی شدهی به آن، یک وهله/نمونه را ایجاد میکند. بنابراین طول عمر سرویسهای ایجاد شدهی در این حالت، singleton است (یکبار ایجاد شده و تا پایان طول عمر برنامه زنده نگه داشته میشوند).
پس از آن میتوان از تزریق کنندههای توکار AngularJS 2.0، جهت تزریق وهلههای این سرویسها استفاده کرد.
اکنون اگر کلاسی، نیاز به این سرویس داشته باشد، نیاز خود را به صورت یک وابستگی تعریف شدهی در سازندهی کلاس اعلام میکند:
constructor(private _myService: MyService){}
به این فرآیند اصطلاحا dependency injection و یا تزریق وابستگیها میگویند. در فرآیند تزریق وابستگیها، یک کلاس، وهلههای کلاسهای دیگر مورد نیاز خودش را بجای وهله سازی مستقیم، از یک تزریق کننده دریافت میکند. بنابراین بجای نوشتن newها در کلاس جاری، آنها را به صورت وابستگیهایی در سازندهی کلاس تعریف میکنیم تا توسط AngularJS 2.0 تامین شوند.
با توجه به اینکه طول عمر این وابستگیها singleton است و این طول عمر توسط AngularJS 2.0 مدیریت میشود، اطلاعات وهلههای سرویسهای مختلف و تغییرات صورت گرفتهی در آنها، بین تمام کامپوننتها به صورت یکسانی به اشتراک گذاشته میشوند.
به علاوه اکنون امکان mocking سرویسها با توجه به عدم وهله سازی آنها در داخل کلاسها به صورت مستقیم، سادهتر از قبل میسر است.
مراحل ساخت یک سرویس در AngularJS 2.0
ساخت یک سرویس در AngularJS 2.0، با ایجاد یک کلاس جدید شروع میشود. سپس متادیتای آن افزوده شده و در آخر موارد مورد نیاز آن import خواهند شد. با این موارد پیشتر در حین ساختن یک کامپوننت جدید و یا یک Pipe جدید آشنا شدهاید و این طراحی یک دست را در سراسر AngularJS 2.0 میتوان مشاهده کرد.
اولین سرویس خود را با افزودن فایل جدید product.service.ts به پوشهی app\products آغاز میکنیم؛ با این محتوا:
import { Injectable } from 'angular2/core'; import { IProduct } from './product'; @Injectable() export class ProductService { getProducts(): IProduct[] { return [ { "productId": 2, "productName": "Garden Cart", "productCode": "GDN-0023", "releaseDate": "March 18, 2016", "description": "15 gallon capacity rolling garden cart", "price": 32.99, "starRating": 4.2, "imageUrl": "app/assets/images/garden_cart.png" }, { "productId": 5, "productName": "Hammer", "productCode": "TBX-0048", "releaseDate": "May 21, 2016", "description": "Curved claw steel hammer", "price": 8.9, "starRating": 4.8, "imageUrl": "app/assets/images/rejon_Hammer.png" } ]; } }
همانند سایر ماژولهای تعریف شده، در اینجا نیز باید کلاس تعریف شده export شود تا در قسمتهای دیگر قابل استفاده و دسترسی گردد.
سپس در این سرویس، یک متد برای بازگشت لیست محصولات ایجاد شدهاست.
در ادامه یک decorator جدید به نام ()Injectable@ به بالای این کلاس اضافه شدهاست. این متادیتا است که مشخص میکند کلاس جاری، یک سرویس AngularJS 2.0 است.
البته باید دقت داشت که این مزین کننده تنها زمانی نیاز است حتما قید شود که کلاس تعریف شده، دارای وابستگیهای تزریق شدهای باشد. اما توصیه شدهاست که بهتر است هر کلاس سرویسی (حتی اگر دارای وابستگیهای تزریق شدهای هم نبود) به این decorator ویژه، مزین شود تا بتوان طراحی یک دستی را در سراسر برنامه شاهد بود.
در آخر هم موارد مورد نیاز، import میشوند. برای مثال Injectable در ماژول angular2/core تعریف شدهاست.
هدف از تعریف این سرویس، دور کردن وظیفهی تامین داده، از کلاس کامپوننت لیست محصولات است؛ جهت رسیدن به یک طراحی SOLID.
در قسمت بعدی این سری، این لیست را بجای یک آرایهی از پیش تعریف شده، از یک سرور HTTP دریافت خواهیم کرد.
ثبت و معرفی سرویس جدید ProductService به AngularJS 2.0 Injector
مرحلهی اول استفاده از سرویسهای تعریف شده، ثبت و معرفی آنها به AngularJS 2.0 Injector است. سپس این Injector است که تک وهلهی سرویس ثبت شدهی در آنرا در اختیار هر کامپوننتی که آنرا درخواست کند، قرار میدهد.
مرحلهی ثبت این سرویس، معرفی نام این کلاس، به خاصیتی آرایهای، به نام providers است که یکی از خواص decorator ویژهی Component است. بدیهی است هر کامپوننتی که در برنامه وجود داشته باشد، توانایی ثبت این سرویس را نیز دارد؛ اما باید از کدامیک استفاده کرد؟
اگر سرویس خود را در کامپوننت لیست محصولات رجیستر کنیم، تک وهلهی این سرویس تنها در این کامپوننت و زیر کامپوننتهای آن در دسترس خواهند بود و اگر این سرویس را در بیش از یک کامپوننت ثبت کنیم، آنگاه دیگر هدف اصلی طول عمر singleton یک سرویس مفهومی نداشته و برنامه هم اکنون دارای چندین وهله از سرویس تعریف شدهی ما میگردد و دیگر نمیتوان اطلاعات یکسانی را بین کامپوننتها به اشتراک گذاشت.
بنابراین توصیه شدهاست که از خاصیت providers کامپوننتهای غیر ریشهای، صرفنظر کرده و سرویسهای خود را تنها در بالاترین سطح کامپوننتهای تعریف شده، یعنی در فایل app.component.ts ثبت و معرفی کنید. به این ترتیب تک وهلهی ایجاد شدهی در اینجا، در این کامپوننت ریشهای و تمام زیر کامپوننتهای آن (یعنی تمام کامپوننتهای دیگر برنامه) به صورت یکسانی در دسترس قرار میگیرد.
به همین جهت فایل app.component.ts را گشوده و تغییرات ذیل را به آن اعمال کنید:
import { Component } from 'angular2/core'; import { ProductListComponent } from './products/product-list.component'; import { ProductService } from './products/product.service'; @Component({ selector: 'pm-app', template:` <div><h1>{{pageTitle}}</h1> <pm-products></pm-products> </div> `, directives: [ProductListComponent], providers: [ProductService] }) export class AppComponent { pageTitle: string = "DNT AngularJS 2.0 APP"; }
الف) خاصیت providers که آرایهای از سرویسها را قبول میکند، با ProductService مقدار دهی شدهاست.
ب) در ابتدای فایل، ProductService، از ماژول آن import گردیدهاست.
تزریق سرویسها به کامپوننتها
تا اینجا یک سرویس جدید را ایجاد کردیم و سپس آنرا به AngularJS 2.0 Injector معرفی نمودیم. اکنون نوبت به استفاده و تزریق آن، به کلاسی است که به این وابستگی نیاز دارد. در TypeScript، تزریق وابستگیها در سازندهی یک کلاس صورت میگیرند. هر کلاس، دارای متد سازندهای است که در زمان وهله سازی آن، اجرا میشود. اگر نیاز به تزریق وابستگیها باشد، تعریف این سازنده به صورت صریح، ضروری است. باید دقت داشت که هدف اصلی از متد سازنده، آغاز و مقدار دهی متغیرها و وابستگیهای مورد نیاز یک کلاس است و باید تا حد امکان از منطقهای طولانی عاری باشد.
در ادامه فایل product-list.component.ts را گشوده و سپس سازندهی ذیل را به آن اضافه کنید:
import { ProductService } from './product.service'; export class ProductListComponent implements OnInit { pageTitle: string = 'Product List'; imageWidth: number = 50; imageMargin: number = 2; showImage: boolean = false; listFilter: string = 'cart'; constructor(private _productService: ProductService) { }
روش خلاصه شدهای که در اینجا جهت تعریف سازندهی کلاس و متغیر تعریف شدهی در آن بکار گرفته شده، معادل قطعه کد متداول ذیل است و هر دو حالت ذکر شده، در TypeScript یکی میباشند:
private _productService: ProductService; constructor(productService: ProductService) { _productService = productService; }
این وابستگی در اولین باری که کلاس کامپوننت، توسط AngularJS 2.0 وهله سازی میشود، از لیست providers ثبت شدهی در کامپوننت ریشهی سایت، تامین خواهد شد.
اکنون نوبت به استفادهی از این سرویس تزریق شدهاست. به همین جهت ابتدا لیست عناصر آرایهی خاصیت products را حذف میکنیم (برای اینکه قرار است این سرویس، کار تامین اطلاعات را انجام دهد و نه کلاس کامپوننت).
products: IProduct[];
this.products = _productService.getProducts();
به همین جهت روش صحیح انجام این مقدار دهی، با پیاده سازی life cycle hook ویژهای به نام OnInit است که در قسمت پنجم آنرا معرفی کردیم:
export class ProductListComponent implements OnInit { products: IProduct[]; constructor(private _productService: ProductService) { } ngOnInit(): void { //console.log('In OnInit'); this.products = this._productService.getProducts(); }
در اینجا اکنون خاصیت products عاری است از ذکر صریح عناصر تشکیل دهندهی آن. سپس وابستگی مورد نیاز، در سازندهی کلاس تزریق شدهاست و در آخر، در رویداد چرخهی حیات ngOnInit، با استفاده از این وابستگی تزریقی، لیست محصولات دریافت و به خاصیت عمومی products نسبت داده شدهاست.
در ادامه برنامه را اجرا کنید. باید هنوز هم مطابق قبل، لیست محصولات قابل مشاهده باشد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: MVC5Angular2.part7.zip
خلاصهی بحث
فرآیند کلی تعریف یک سرویس AngularJS 2.0، تفاوتی با ساخت یک کامپوننت یا Pipe سفارشی ندارد. پس از تعریف کلاسی که نام آن ختم شدهی به Service است، آنرا مزین به ()Injectable@ میکنیم. سپس این سرویس را در بالاترین سطح کامپوننتهای موجود یا همان کامپوننت ریشهی سایت، ثبت و معرفی میکنیم؛ تا تنها یک وهله از آن توسط AngularJS 2.0 Injector ایجاد شده و در اختیار تمام کامپوننتهای برنامه قرار گیرد. البته اگر این سرویس تنها در یک کامپوننت استفاده میشود و قصد به اشتراک گذاری اطلاعات آنرا نداریم، میتوان سطح سلسله مراتب دسترسی به آنرا نیز کاهش داد. برای مثال این سرویس را در لیست providers همان کامپوننت ویژه، ثبت و معرفی کرد. به این ترتیب تنها این کامپوننت خاص و فرزندان آن دسترسی به امکانات سرویس مدنظر را مییابند و نه تمام کامپوننتهای دیگر تعریف شدهی در برنامه.
در ادامه هر کلاسی که به این سرویس نیاز دارد (با توجه به سلسه مراتب دسترسی ذکر شده)، تنها کافی است در سازندهی خود، این وابستگی را اعلام کند تا توسط AngularJS 2.0 Injector تامین گردد.
معرفی مدل ارسالی برنامه سمت کلاینت
فرض کنید مطابق شکل فوق، قرار است اطلاعات یک کاربر، به همراه تعدادی تصویر از او، به سمت Web API ارسال شوند. برای نمونه، مدل اشتراکی کاربر را به صورت زیر تعریف کردهایم:
using System.ComponentModel.DataAnnotations; namespace BlazorWasmUpload.Shared { public class User { [Required] public string Name { get; set; } [Required] [Range(18, 90)] public int Age { get; set; } } }
ساختار کنترلر Web API دریافت کنندهی مدل برنامه
در این حالت امضای اکشن متد CreateUser واقع در کنترلر Files که قرار است این اطلاعات را دریافت کند، به صورت زیر است:
namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { [HttpPost] public async Task<IActionResult> CreateUser( [FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null)
ایجاد سرویسی در سمت کلاینت، برای آپلود اطلاعات یک مدل به همراه فایلهای انتخابی کاربر
کدهای کامل سرویسی که میتواند انتظارات یاد شده را در سمت کلاینت برآورده کند، به صورت زیر است:
using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Components.Forms; namespace BlazorWasmUpload.Client.Services { public interface IFilesManagerService { Task<HttpResponseMessage> PostModelWithFilesAsync<T>(string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName); } public class FilesManagerService : IFilesManagerService { private readonly HttpClient _httpClient; public FilesManagerService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<HttpResponseMessage> PostModelWithFilesAsync<T>( string requestUri, IEnumerable<IBrowserFile> browserFiles, string fileParameterName, T model, string modelParameterName) { var requestContent = new MultipartFormDataContent(); requestContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data"); if (browserFiles?.Any() == true) { foreach (var file in browserFiles) { var stream = file.OpenReadStream(maxAllowedSize: 512000 * 1000); requestContent.Add(content: new StreamContent(stream, (int)file.Size), name: fileParameterName, fileName: file.Name); } } requestContent.Add( content: new StringContent(JsonSerializer.Serialize(model), Encoding.UTF8, "application/json"), name: modelParameterName); var result = await _httpClient.PostAsync(requestUri, requestContent); result.EnsureSuccessStatusCode(); return result; } } }
- کامپوننت استاندارد InputFiles در Blazor Wasm، میتواند لیستی از IBrowserFileهای انتخابی توسط کاربر را در اختیار ما قرار دهد.
- fileParameterName، همان نام پارامتر "inputFiles" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- model جنریک، برای نمونه وهلهای از شیء User است که به یک فرم Blazor متصل است.
- modelParameterName، همان نام پارامتر "userModel" در اکشن متد سمت سرور مثال جاری است که به صورت متغیر قابل تنظیم شدهاست.
- در ادامه یک MultipartFormDataContent را تشکیل دادهایم. توسط این ساختار میتوان فایلها و اطلاعات یک مدل را به صورت یکجا جمع آوری و به سمت سرور ارسال کرد. به این content ویژه، ابتدای لیستی از new StreamContentها را اضافه میکنیم. این streamها توسط متد OpenReadStream هر IBrowserFile دریافتی از کامپوننت InputFile، تشکیل میشوند. متد OpenReadStream به صورت پیشفرض فقط فایلهایی تا حجم 500 کیلوبایت را پردازش میکند و اگر فایلی حجیمتر را به آن معرفی کنیم، یک استثناء را صادر خواهد کرد. به همین جهت میتوان توسط پارامتر maxAllowedSize آن، این مقدار پیشفرض را تغییر داد.
- در اینجا مدل برنامه به صورت JSON به عنوان یک new StringContent اضافه شدهاست. مزیت کار کردن با JsonSerializer.Serialize استاندارد، ساده شدن برنامه و عدم درگیری با مباحث Reflection و خواندن پویای اطلاعات مدل جنریک است. اما در ادامه مشکلی را پدید خواهد آورد! این رشتهی ارسالی به سمت سرور، به صورت خودکار به یک مدل، Bind نخواهد شد و باید برای آن یک model-binder سفارشی را بنویسیم. یعنی این رشتهی new StringContent را در سمت سرور دقیقا به صورت یک رشته معمولی میتوان دریافت کرد و نه حالت دیگری و مهم نیست که اکنون به صورت JSON ارسال میشود؛ چون MultipartFormDataContent ویژهای را داریم، model-binder پیشفرض ASP.NET Core، انتظار یک شیء خاص را در این بین ندارد.
- تنظیم "form-data" را هم به عنوان Headers.ContentDisposition مشاهده میکنید. بدون وجود آن، ویژگی [FromForm] سمت Web API، از پردازش درخواست جلوگیری خواهد کرد.
- در آخر توسط متد PostAsync، این اطلاعات جمع آوری شده، به سمت سرور ارسال خواهند شد.
پس از تهیهی سرویس ویژهی فوق که میتواند اطلاعات فایلها و یک مدل را به صورت یکجا به سمت سرور ارسال کند، اکنون نوبت به ثبت و معرفی آن به سیستم تزریق وابستگیها در فایل Program.cs برنامهی کلاینت است:
namespace BlazorWasmUpload.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IFilesManagerService, FilesManagerService>(); // ... } } }
تکمیل فرم ارسال اطلاعات مدل و فایلهای همراه آن در برنامهی Blazor WASM
در ادامه پس از تشکیل IFilesManagerService، نوبت به استفادهی از آن است. به همین جهت همان کامپوننت Index برنامه را به صورت زیر تغییر میدهیم:
@code { IReadOnlyList<IBrowserFile> SelectedFiles; User UserModel = new User(); bool isProcessing; string UploadErrorMessage;
- SelectedFiles همان لیست فایلهای انتخابی توسط کاربر است.
- UserModel شیءای است که به EditForm جاری متصل خواهد شد.
- توسط isProcessing ابتدا و انتهای آپلود به سرور را مشخص میکنیم.
- UploadErrorMessage، خطای احتمالی انتخاب فایلها مانند «فقط تصاویر را انتخاب کنید» را تعریف میکند.
بر این اساس، فرمی را که در تصویر ابتدای بحث مشاهده کردید، به صورت زیر تشکیل میدهیم:
@page "/" @using System.IO @using BlazorWasmUpload.Shared @using BlazorWasmUpload.Client.Services @inject IFilesManagerService FilesManagerService <h3>Post a model with files</h3> <EditForm Model="UserModel" OnValidSubmit="CreateUserAsync"> <DataAnnotationsValidator /> <div> <label>Name</label> <InputText @bind-Value="UserModel.Name"></InputText> <ValidationMessage For="()=>UserModel.Name"></ValidationMessage> </div> <div> <label>Age</label> <InputNumber @bind-Value="UserModel.Age"></InputNumber> <ValidationMessage For="()=>UserModel.Age"></ValidationMessage> </div> <div> <label>Photos</label> <InputFile multiple disabled="@isProcessing" OnChange="OnInputFileChange" /> @if (!string.IsNullOrWhiteSpace(UploadErrorMessage)) { <div> @UploadErrorMessage </div> } @if (SelectedFiles?.Count > 0) { <table> <thead> <tr> <th>Name</th> <th>Size (bytes)</th> <th>Last Modified</th> <th>Type</th> </tr> </thead> <tbody> @foreach (var selectedFile in SelectedFiles) { <tr> <td>@selectedFile.Name</td> <td>@selectedFile.Size</td> <td>@selectedFile.LastModified</td> <td>@selectedFile.ContentType</td> </tr> } </tbody> </table> } </div> <div> <button disabled="@isProcessing">Create user</button> </div> </EditForm>
- UserModel که وهلهی از شیء اشتراکی User است، به EditForm متصل شدهاست.
- سپس توسط یک InputText و InputNumber، مقادیر خواص نام و سن کاربر را دریافت میکنیم.
- InputFile دارای ویژگی multiple هم امکان دریافت چندین فایل را توسط کاربر میسر میکند. پس از انتخاب فایلها، رویداد OnChange آن، توسط متد OnInputFileChange مدیریت خواهد شد:
private void OnInputFileChange(InputFileChangeEventArgs args) { var files = args.GetMultipleFiles(maximumFileCount: 15); if (args.FileCount == 0 || files.Count == 0) { UploadErrorMessage = "Please select a file."; return; } var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" }; if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase))) { UploadErrorMessage = "Please select .jpg/.jpeg/.png files only."; return; } SelectedFiles = files; UploadErrorMessage = string.Empty; }
- در ادامه اگر فایلی انتخاب نشده باشد، یا فایل انتخابی، تصویری نباشد، با مقدار دهی UploadErrorMessage، خطایی را به کاربر نمایش میدهیم.
- در پایان این متد، لیست فایلهای دریافتی را به فیلد SelectedFiles انتساب میدهیم تا در ذیل InputFile، به صورت یک جدول نمایش داده شوند.
مرحلهی آخر تکمیل این فرم، تدارک متد رویدادگردان OnValidSubmit فرم برنامه است:
private async Task CreateUserAsync() { try { isProcessing = true; await FilesManagerService.PostModelWithFilesAsync( requestUri: "api/Files/CreateUser", browserFiles: SelectedFiles, fileParameterName: "inputFiles", model: UserModel, modelParameterName: "userModel"); UserModel = new User(); } finally { isProcessing = false; SelectedFiles = null; } }
- سپس روش استفادهی از متد PostModelWithFilesAsync سرویس FilesManagerService را مشاهده میکنید که اطلاعات فایلها و مدل برنامه را به سمت اکشن متد api/Files/CreateUser ارسال میکند.
- در آخر با وهله سازی مجدد UserModel، به صورت خودکار فرم برنامه را پاک کرده و آمادهی دریافت اطلاعات بعدی میکنیم.
تکمیل کنترلر Web API دریافت کنندهی مدل برنامه
در ابتدای بحث، ساختار ابتدایی کنترلر Web API دریافت کنندهی اطلاعات FilesManagerService.PostModelWithFilesAsync فوق را معرفی کردیم. در ادامه کدهای کامل آنرا مشاهده میکنید:
using System.IO; using Microsoft.AspNetCore.Mvc; using BlazorWasmUpload.Shared; using Microsoft.AspNetCore.Hosting; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using System.Collections.Generic; using Microsoft.Extensions.Logging; using System.Text.Json; using BlazorWasmUpload.Server.Utils; using System.Linq; namespace BlazorWasmUpload.Server.Controllers { [ApiController] [Route("api/[controller]/[action]")] public class FilesController : ControllerBase { private const int MaxBufferSize = 0x10000; private readonly IWebHostEnvironment _webHostEnvironment; private readonly ILogger<FilesController> _logger; public FilesController( IWebHostEnvironment webHostEnvironment, ILogger<FilesController> logger) { _webHostEnvironment = webHostEnvironment; _logger = logger; } [HttpPost] public async Task<IActionResult> CreateUser( //[FromForm] string userModel, // <-- this is the actual form of the posted model [ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel, [FromForm] IList<IFormFile> inputFiles = null) { /*var user = JsonSerializer.Deserialize<User>(userModel); _logger.LogInformation($"userModel.Name: {user.Name}"); _logger.LogInformation($"userModel.Age: {user.Age}");*/ _logger.LogInformation($"userModel.Name: {userModel.Name}"); _logger.LogInformation($"userModel.Age: {userModel.Age}"); var uploadsRootFolder = Path.Combine(_webHostEnvironment.WebRootPath, "Files"); if (!Directory.Exists(uploadsRootFolder)) { Directory.CreateDirectory(uploadsRootFolder); } if (inputFiles?.Any() == true) { foreach (var file in inputFiles) { if (file == null || file.Length == 0) { continue; } var filePath = Path.Combine(uploadsRootFolder, file.FileName); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, MaxBufferSize, useAsync: true); await file.CopyToAsync(fileStream); _logger.LogInformation($"Saved file: {filePath}"); } } return Ok(); } } }
[ModelBinder(BinderType = typeof(JsonModelBinder)), FromForm] User userModel,
using System; using System.Text.Json; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.ModelBinding; namespace BlazorWasmUpload.Server.Utils { public class JsonModelBinder : IModelBinder { public Task BindModelAsync(ModelBindingContext bindingContext) { if (bindingContext == null) { throw new ArgumentNullException(nameof(bindingContext)); } var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName); if (valueProviderResult != ValueProviderResult.None) { bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult); var valueAsString = valueProviderResult.FirstValue; var result = JsonSerializer.Deserialize(valueAsString, bindingContext.ModelType); if (result != null) { bindingContext.Result = ModelBindingResult.Success(result); return Task.CompletedTask; } } return Task.CompletedTask; } } }
[HttpPost] public async Task<IActionResult> CreateUser( [FromForm] string userModel, // <-- this is the actual form of the posted model [FromForm] IList<IFormFile> inputFiles = null) { var user = JsonSerializer.Deserialize<User>(userModel);
یک نکته تکمیلی: در Blazor 5x، از نمایش درصد پیشرفت آپلود، پشتیبانی نمیشود؛ از این جهت که HttpClient طراحی شده، در اصل به fetch API استاندارد مرورگر ترجمه میشود و این API استاندارد، هنوز از streaming پشتیبانی نمیکند . حتی ممکن است با کمی جستجو به راهحلهایی که سعی کردهاند بر اساس HttpClient و نوشتن بایت به بایت اطلاعات در آن، درصد پیشرفت آپلود را محاسبه کرده باشند، برسید. این راهحلها تنها کاری را که انجام میدهند، بافر کردن اطلاعات، جهت fetch API و سپس ارسال تمام آن است. به همین جهت درصدی که نمایش داده میشود، درصد بافر شدن اطلاعات در خود مرورگر است (پیش از ارسال آن به سرور) و سپس تحویل آن به fetch API جهت ارسال نهایی به سمت سرور.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorWasmUpload.zip