class Parent { public IChild child {get;set;} public Parent(Ichild child) { this.child =child; } }
function timeDiff(current, prev) { const millisecond = current - prev; const second = millisecond / 1000; const minute = second / 60; const hour = minute / 60; const day = hour / 24; const week = day / 7; const month = week / 4.3; const year = month / 12; const quarter = year / 4; const unitValues = { millisecond, second, minute, hour, day, week, month, year, quarter }; return function diffValueByUnit(unitKey) { return unitValues[unitKey]; }; } const from = new Date(); from.setDate(from.getDate() - 2); console.log(from); const to = new Date(Date.now()); console.log(to); const diff = timeDiff(from, to); const result = parseFloat(Math.round(diff("hour"))); console.log(result); const rtf = new Intl.RelativeTimeFormat("fa"); console.log(rtf.format(result, "hour"));
> Wed Feb 19 2020 02:24:36 GMT+0330 (Iran Standard Time) > Fri Feb 21 2020 02:24:36 GMT+0330 (Iran Standard Time) > -48 > "۴۸ ساعت پیش"
private enum VisiteType { FirstVisite = 2, Reviste = 4, }
if (httpContext.Cache[ipAddress] != null) { hits++; if (hits == (int)VisiteType.FirstVisite) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; }
using System; using System.Net; using System.Web; using System.Web.Caching; namespace IpBlocker { public class IpBlocker : IHttpModule { private int hits = 0; /// <summary> /// Define enum to specify visite type /// </summary> private enum VisiteType { FirstVisite = 2, Reviste = 4, } public void Init(HttpApplication context) { context.BeginRequest += OnBeginRequest; } public void Dispose() { } public void OnBeginRequest(object sender, EventArgs e) { var httpApplication = sender as HttpApplication; var httpContext = httpApplication?.Context; ProcessRequest(httpApplication, httpContext); } private void ProcessRequest(HttpApplication application, HttpContext httpContext) { //Checke if browser is a search engine web crawler if (httpContext.Request.Browser.Crawler) return; var ipAddress = application.Context.Request.UserHostAddress; if (httpContext.Cache[ipAddress] == null) { // Reset hits for new request after blocking ip if (hits > 0) hits = 0; hits++; httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } else { if (httpContext.Cache[ipAddress] != null) { hits++; if (hits == (int)VisiteType.FirstVisite) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } if (hits == (int)VisiteType.Reviste) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddSeconds(1), slidingExpiration: Cache.NoSlidingExpiration); return; } if (hits > (int)VisiteType.Reviste) { httpContext.Cache.Insert(key: ipAddress, value: hits, dependencies: null, absoluteExpiration: DateTime.UtcNow.AddMinutes(1), slidingExpiration: Cache.NoSlidingExpiration); httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; httpContext.Response.SuppressContent = true; httpContext.Response.End(); } } } } } }
اعمال کنترل دسترسی پویا در پروژههای ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
در مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاستهای دسترسی پویا» به طور مفصل به قضیه کنترل دسترسی پویا در ASP.NET Core Identity پرداخته شدهاست؛ در این مطلب روش دیگری را بررسی خواهیم کرد.
مشخص میباشد که بدون وابستگی به روش خاصی، خیلی ساده میتوان به شکل زیر عمل کرد:
services.AddAuthorization(options => { options.AddPolicy("View Projects", policy => policy.RequireClaim(CustomClaimTypes.Permission, "projects.view")); });
[Authorize("View Projects")] public IActionResult Index(int siteId) { return View(); }
Using a large range of policies (for different room numbers or ages, for example), so it doesn’t make sense to add each individual authorization policy with an AuthorizationOptions.AddPolicy call.
کار با پیاده سازی واسط IAuthorizationPolicyProvider شروع میشود؛ یا شاید ارث بری از DefaultAuthorizationPolicyProvider رجیستر شدهی در سیستم DI و توسعه آن هم کافی باشد.
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider { public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) { } public override Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { if (!policyName.StartsWith(PermissionAuthorizeAttribute.PolicyPrefix, StringComparison.OrdinalIgnoreCase)) { return base.GetPolicyAsync(policyName); } var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(','); var policy = new AuthorizationPolicyBuilder() .RequireClaim(CustomClaimTypes.Permission, permissionNames) .Build(); return Task.FromResult(policy); } }
متد GetPolicyAsync موظف به یافتن و بازگشت یک Policy ثبت شده میباشد؛ با این حال میتوان با بازنویسی آن و با استفاده از وهلهای از AuthorizationPolicyBuilder، فرآیند تعریف سیاست درخواست شده را که احتمالا در تنظیمات آغازین پروژه تعریف نشده و پیشوند مدنظر را نیز دارد، خوکار کرد. در اینجا امکان ترکیب کردن چندین دسترسی را هم خواهیم داشت که برای این منظور میتوان دسترسیهای مختلف را به صورت comma separated به سیستم معرفی کرد.
نکتهی مهم در تکه کد بالا مربوط است به PolicyPrefix که با استفاده از آن مشخص کردهایم که برای هر سیاست درخواستی، این فرآیند را طی نکند و موجب اختلال در سیستم نشود.
پس از پیاده سازی واسط مطرح شده، لازم است این پیاده سازی جدید را به سیستم DI هم معرفی کنید:
services.AddSingleton<IAuthorizationPolicyProvider, AuthorizationPolicyProvider>();
خوب، تا اینجا فرآیند تعریف سیاستها به صورت خودکار انجام شد. در ادامه نیاز است با تعریف یک فیلتر Authorization، بتوان لیست دسترسیهای مورد نظر برای اکشنی خاص را نیز مشخص کرد تا در متد GetPolicyAsync فوق، کار ثبت خودکار سیاست دسترسی متناظر با آنرا توسط فراخوانی متد policyBuilder.RequireClaim، انجام دهد تا دیگر نیازی به تعریف دستی و جداگانهی آن، در کلاس آغازین برنامه نباشد. برای این منظور به شکل زیر عمل خواهیم کرد:
public class PermissionAuthorizeAttribute : AuthorizeAttribute { internal const string PolicyPrefix = "PERMISSION:"; /// <summary> /// Creates a new instance of <see cref="AuthorizeAttribute"/> class. /// </summary> /// <param name="permissions">A list of permissions to authorize</param> public PermissionAuthorizeAttribute(params string[] permissions) { Policy = $"{PolicyPrefix}{string.Join(",", permissions)}"; } }
همانطور که مشخص میباشد، رشته PERMISSION به عنوان پیشوند رشته تولیدی از لیست اسامی دسترسیها، استفاده شدهاست و در پراپرتی Policy قرار داده شدهاست. این بار برای کنترل دسترسی میتوان به شکل زیر عمل کرد:
[PermissionAuthorize(PermissionNames.Projects_View)] public IActionResult Get(FilteredQueryModel query) { //... } [PermissionAuthorize(PermissionNames.Projects_Create)] public IActionResult Post(ProjectModel model) { //... }
برای مثال در اولین فراخوانی فیلتر PermissionAuthorize فوق، مقدار ثابت PermissionNames.Projects_View به عنوان یک Policy جدید به متد GetPolicyAsync کلاس AuthorizationPolicyProvider سفارشی ما ارسال میشود. چون دارای پیشوند «:PERMISSION» است، مورد پردازش قرار گرفته و توسط متد policyBuilder.RequireClaim به صورت خودکار به سیستم معرفی و ثبت خواهد شد.
همچنین راه حل مطرح شده برای مدیریت دسترسیهای پویا، در gist به اشتراک گذاشته شده «موجودیتهای مرتبط با مدیریت دسترسیهای پویا» را نیز مد نظر قرار دهید.
حال اگر بخواهیم همین پروژه را به صورت سورس باز ارائه دهیم، استفاده کنندگان نهایی به مشکل برخواهند خورد؛ زیرا فایل pfx حاصل، توسط کلمه عبور محافظت میشود و در سایر سیستمها بدون درنظر گرفتن این ملاحظات قابل استفاده نخواهد بود.
معادل فایلهای pfx، فایلهایی هستند با پسوند snk که تنها تفاوت مهم آنها با فایلهای pfx، عدم محافظت توسط کلمه عبور است و ... برای کارهای خصوصا سورس باز انتخاب مناسبی به شمار میروند. اگر دقت کنید، اکثر پروژههای سورس باز دات نتی موجود در وب (مانند NHibernate، لوسین، iTextSharp و غیره) از فایلهای snk برای اضافه کردن امضای دیجیتال به کتابخانه نهایی تولیدی استفاده میکنند و نه فایلهای pfx محافظت شده.
در اینجا اگر فایل pfx ایی دارید و میخواهید معادل snk آنرا تولید کنید، قطعه کد زیر چنین امکانی را مهیا میسازد:
using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace PfxToSnk { class Program { /// <summary> /// Converts .pfx file to .snk file. /// </summary> /// <param name="pfxData">.pfx file data.</param> /// <param name="pfxPassword">.pfx file password.</param> /// <returns>.snk file data.</returns> public static byte[] Pfx2Snk(byte[] pfxData, string pfxPassword) { var cert = new X509Certificate2(pfxData, pfxPassword, X509KeyStorageFlags.Exportable); var privateKey = (RSACryptoServiceProvider)cert.PrivateKey; return privateKey.ExportCspBlob(true); } static void Main(string[] args) { var pfxFileData = File.ReadAllBytes(@"D:\Key.pfx"); var snkFileData = Pfx2Snk(pfxFileData, "my-pass"); File.WriteAllBytes(@"D:\Key.snk", snkFileData); } } }
کنترل DatePicker شمسی مخصوص Silverlight 4
datePicker.SelectedPersianDate = "1391/7/23"
ردیابی تغییرات در سمت کلاینت توسط Web API
فرض کنید میخواهیم از سرویسهای REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین میخواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیتها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی دادهها توسط مدل Code-First انجام میشود.
در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویسهای ارائه شده توسط پروژه Web API را فراخوانی میکند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی میکنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه میکند. میخواهیم مدلها و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
- کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
- کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیتها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه میکند. کلاینتها هنگام ویرایش آبجکت موجودیتها باید این فیلد را مقدار دهی کنند. همانطور که میبینید این خاصیت از نوع TrackingState enum مشتق میشود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیتها بدین روش، وابستگیهای EF را برای کلاینت از بین میبریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگیهای بخصوصی معرفی میشدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور میدهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity { protected BaseEntity() { TrackingState = TrackingState.Nochange; } public TrackingState TrackingState { get; set; } } public enum TrackingState { Nochange, Add, Update, Remove, }
- کلاسهای موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity { public int CustomerId { get; set; } public string Name { get; set; } public string Company { get; set; } public virtual ICollection<Phone> Phones { get; set; } } public class Phone : BaseEntity { public int PhoneId { get; set; } public string Number { get; set; } public string PhoneType { get; set; } public int CustomerId { get; set; } public virtual Customer Customer { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیتهای جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی میکنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext { public Recipe4Context() : base("Recipe4ConnectionString") { } public DbSet<Customer> Customers { get; set; } public DbSet<Phone> Phones { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Do not persist TrackingState property to data store // This property is used internally to track state of // disconnected entities across service boundaries. // Leverage the Custom Code First Conventions features from Entity Framework 6. // Define a convention that performs a configuration for every entity // that derives from a base entity class. modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState)); modelBuilder.Entity<Customer>().ToTable("Customers"); modelBuilder.Entity<Phone>().ToTable("Phones"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe4ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال میکند و به JSON serializer دستور میدهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیتهای Customer و PhoneNumber بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینتها ارائه میشود را به مقادیر متناظر کامپوننتهای ردیابی EF تبدیل میکند.
public static EntityState Set(TrackingState trackingState) { switch (trackingState) { case TrackingState.Add: return EntityState.Added; case TrackingState.Update: return EntityState.Modified; case TrackingState.Remove: return EntityState.Deleted; default: return EntityState.Unchanged; } }
- در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController { // GET api/customer public IEnumerable<Customer> Get() { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).ToList(); } } // GET api/customer/5 public Customer Get(int id) { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id); } } [ActionName("Update")] public HttpResponseMessage UpdateCustomer(Customer customer) { using (var context = new Recipe4Context()) { // Add object graph to context setting default state of 'Added'. // Adding parent to context automatically attaches entire graph // (parent and child entities) to context and sets state to 'Added' // for all entities. context.Customers.Add(customer); foreach (var entry in context.ChangeTracker.Entries<BaseEntity>()) { entry.State = EntityStateFactory.Set(entry.Entity.TrackingState); if (entry.State == EntityState.Modified) { // For entity updates, we fetch a current copy of the entity // from the database and assign the values to the orginal values // property from the Entry object. OriginalValues wrap a dictionary // that represents the values of the entity before applying changes. // The Entity Framework change tracker will detect // differences between the current and original values and mark // each property and the entity as modified. Start by setting // the state for the entity as 'Unchanged'. entry.State = EntityState.Unchanged; var databaseValues = entry.GetDatabaseValues(); entry.OriginalValues.SetValues(databaseValues); } } context.SaveChanges(); } return Request.CreateResponse(HttpStatusCode.OK, customer); } [HttpDelete] [ActionName("Cleanup")] public HttpResponseMessage Cleanup() { using (var context = new Recipe4Context()) { context.Database.ExecuteSqlCommand("delete from phones"); context.Database.ExecuteSqlCommand("delete from customers"); return Request.CreateResponse(HttpStatusCode.OK); } } }
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program { private HttpClient _client; private Customer _bush, _obama; private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up completes await program.CleanupAsync(); program.CreateFirstCustomer(); // do not proceed until customer is added await program.AddCustomerAsync(); program.CreateSecondCustomer(); // do not proceed until customer is added await program.AddSecondCustomerAsync(); // do not proceed until customer is removed await program.RemoveFirstCustomerAsync(); // do not proceed until customers are fetched await program.FetchCustomersAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") }; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue ("application/json")); } private async Task CleanupAsync() { // call the cleanup method from the service _response = await _client.DeleteAsync("api/customer/cleanup/"); } private void CreateFirstCustomer() { // create customer #1 and two phone numbers _bush = new Customer { Name = "George Bush", Company = "Ex President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _whiteHousePhone = new Phone { Number = "212 222-2222", PhoneType = "White House Red Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bushMobilePhone = new Phone { Number = "212 333-3333", PhoneType = "Bush Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bush.Phones.Add(_whiteHousePhone); _bush.Phones.Add(_bushMobilePhone); } private async Task AddCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _bush = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _bush.Name, _bush.Phones.Count); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private void CreateSecondCustomer() { // create customer #2 and phone numbers _obama = new Customer { Name = "Barack Obama", Company = "President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _obamaMobilePhone = new Phone { Number = "212 444-4444", PhoneType = "Obama Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; // set tracking state to 'Modifed' to generate a SQL Update statement _whiteHousePhone.TrackingState = TrackingState.Update; _obama.Phones.Add(_obamaMobilePhone); _obama.Phones.Add(_whiteHousePhone); } private async Task AddSecondCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _obama = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _obama.Name, _obama.Phones.Count); foreach (var phoneType in _obama.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private async Task RemoveFirstCustomerAsync() { // remove George Bush from underlying data store. // first, fetch George Bush entity, demonstrating a call to the // get action method on the service while passing a parameter var query = "api/customer/" + _bush.CustomerId; _response = _client.GetAsync(query).Result; if (_response.IsSuccessStatusCode) { _bush = await _response.Content.ReadAsAsync<Customer>(); // set tracking state to 'Remove' to generate a SQL Delete statement _bush.TrackingState = TrackingState.Remove; // must also remove bush's mobile number -- must delete child before removing parent foreach (var phoneType in _bush.Phones) { // set tracking state to 'Remove' to generate a SQL Delete statement phoneType.TrackingState = TrackingState.Remove; } // construct call to remove Bush from underlying database table _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { Console.WriteLine("Removed {0} from database", _bush.Name); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Remove {0} from data store", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } private async Task FetchCustomersAsync() { // finally, return remaining customers from underlying data store _response = await _client.GetAsync("api/customer/"); if (_response.IsSuccessStatusCode) { var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>(); foreach (var customer in customers) { Console.WriteLine("Customer {0} has {1} Phone Numbers(s)", customer.Name, customer.Phones.Count()); foreach (var phoneType in customer.Phones) { Console.WriteLine("Phone Type: {0}", phoneType.PhoneType); } } } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } }
- در آخر کلاسهای Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایههای مختلف اپلیکیشن به اشتراک گذاشته شوند.
اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت میکنیم و از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی میکنیم. این فراخوانی تمام دادههای پیشین را حذف میکند.
در قدم بعدی یک مشتری بهمراه دو شماره تماس میسازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی میکنیم تا کامپوننتهای Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.
سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی میکنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت میکند و آن را به context جاری اضافه مینماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه میشود و EF شروع به ردیابی تغییرات آن میکند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.
قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده میکنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه میکند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه میدهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیتها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل میکنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام میشود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر میدهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت میکنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت میکند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص میدهیم. پشت پرده، کامپوننتهای EF Change-tracking بصورت خودکار تفاوتهای مقادیر اصلی و مقادیر ارسالی را تشخیص میدهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری میکنند. فراخوانیهای بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.
در اپلیکیشن کلاینت عملیات افزودن، بروز رسانی و حذف موجودیتها توسط مقداردهی خاصیت TrackingState را نمایش داده ایم.
متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل میکند و آبجکتها را به موتور change-tracking ارسال میکند که نهایتا منجر به تولید دستورات لازم SQL میشود.
نکته: در اپلیکیشنهای واقعی بهتر است کد دسترسی دادهها و مدلهای دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت میتواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی میتوانید برای تمام موجودیتها استفاده کنید.
تا قبل از دات نت 7، متدهای Parse و TryParse جزو استاندارد اغلب نوعها در دات نت بودند؛ اما امکان استفادهی جنریک از آنها وجود نداشت. این مشکل به لطف وجود اعضای استاتیک اینترفیسها در دات نت 7 و C# 11 برطرف شدهاست. برای این منظور دو اینترفیس جدید System.IParsable و System.ISpanParsable به دات نت 7 اضافه شدهاند که امکان دسترسی به متد T.Parse را میسر میکنند.
دو نمونه مثال از نحوهی استفادهی از این API جدید را در ادامه مشاهده میکنید:
public static T ParseIt<T>(string content, IFormatProvider? provider) where T : IParsable<T> { return T.Parse(content, provider); } public IEnumerable<T> ParseCsvRow<T>(string content, IFormatProvider? provider) where T : IParsable<T> { return content.Split(',').Select(str => T.Parse(str, provider)); }
public static T ParseIt<T>(string content, IFormatProvider? provider) { var type = typeof(T); var method = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public, new[] { typeof(string), typeof(IFormatProvider) }); return (T)method!.Invoke(null, new object?[] { content, provider })!; }
واژهی استثناء یا exception کوتاه شدهی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ میدهد و در نتیجه، جریان عادی برنامه را مختل میکند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی دربارهی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته میشود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر میکند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامهی شما باشد و باید این لیست را درون برنامهی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامهی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامهتان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی میتوانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناءها بستگی به حالتهای مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه میتواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts() { using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml"))) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
در مثال دوم میدانیم که ممکن است فایل از قبل موجود نباشد. بنابراین میتوانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts() { var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml"); if (!File.Exists(fullPath)) return new Product[0]; using (var stream = File.Read(fullPath)) { var serializer = new XmlSerializer(); return (IEnumerable<Product>)serializer.Deserialize(stream); } }
چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر میگردانند ایجاد کرد.
اجازه دهید دوباره از مثالهای فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
IEnumerable<Product> GetCachedProducts()
در واقع استثناها حالتهایی هستند که غیرقابل پیشبینی هستند. این حالتها میتوانند یک خطای منطقی از طرف برنامهنویس و یا چیزی خارج کنترل برنامهنویس باشند (مانند خطاهای سیستمعامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمیتوان مدیریت کرد.
اگر میخواهید استثناءها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.
چه استثناءهایی باید مدیریت شوند و کدامها خیر؟
مدیریت صحیح استثناءها میتواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ میدهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری میکند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناءهایی را catch کنید که بتوانید برای آن راهحلی ارائه دهید.
به عنوان مثال اگر در لایهی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، میتوانیم آن را catch کرده و درون یک استثناء عمومیتر قرار دهیم:
public class UserRepository : IUserRepository { public IList<User> Search(string value) { try { return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value)); } catch (SqlException err) { var msg = String.Format("Ohh no! Failed to search after users with '{0}' as search string", value); throw new DataSourceException(msg, err); } } }
اگر مطمئن نیستید که تمام استثناءها توسط شما مدیریت شدهاند، میتوانید در حالتهای زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: میتوانید Aplication_Error را پیادهسازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException
WCF: پیادهسازی اینترفیس IErrorHandler
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI
چه زمانهایی باید یک استثناء صادر شود؟
صادر کردن یک استثناء به تنهایی کار سادهایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه دادههایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.
اعتبارسنجی آرگومانها
سادهترین مثال، آرگومانهای مورد انتظار یک متد است:
public void PrintName(string name) { Console.WriteLine(name); }
مشکل فوق را میتوانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); Console.WriteLine(name); }
public void PrintName(string name) { if (name == null) throw new ArgumentNullException("name"); if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long"); if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics"); Console.WriteLine(name); }
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحثبرانگیز متدی با امضای زیر است:
public User GetUser(int id) { }
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId); if (user == null) throw new InvalidOperationException("Failed to find user: " + userId); // actual logic here
public User GetUser(int id) { if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?"); var user = db.Execute<User>("WHERE Id = ?", id); if (user == null) throw new EntityNotFoundException("Failed to find user with id " + id); return user; }
خطاهای متداول حین کار با استثناءها
- صدور مجدد استثناء و از بین بردن stacktrace
کد زیر را در نظر بگیرید:
try { FutileAttemptToResist(); } catch (BorgException err) { _myDearLog.Error("I'm in da cube! Ohh no!", err); throw err; }
- اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید
یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:
try { GreaseTinMan(); } catch (InvalidOperationException err) { throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود }
- ارائه ندادن context information
در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:
try { socket.Connect("somethingawful.com", 80); } catch (SocketException err) { throw new InvalidOperationException("Socket failed", err); }
void IncreaseStatusForUser(int userId, int newStatus) { try { var user = _repository.Get(userId); if (user == null) throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus)); user.Status = newStatus; _repository.Save(user); } catch (DataSourceException err) { var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus); throw new UpdateException(errMsg, err); }
نحوهی طراحی استثناءها
برای ایجاد یک استثناء سفارشی میتوانید از کلاس Exception ارثبری کنید و چهار سازندهی آن را اضافه کنید:
public NewException() public NewException(string description ) public NewException(string description, Exception inner) protected or private NewException(SerializationInfo info, StreamingContext context)
سازندهی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کردهاید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
“The key ‘abrakadabra’ was not present in the dictionary.”
سازندهی سوم شبیه به سازندهی قبلی عمل میکند با این تفاوت که توسط پارامتر دوم میتوانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازندهی سوم زمانی مورد استفاده قرار میگیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیرهی استثناءها درون فایل و...)
خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception { public SampleException(string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); } public SampleException(string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); } public SampleException(SerializationInfo info, StreamingContext context) : base(info, context) { } }
اجباری کردن ارائهی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازندهها را اینگونه تعریف کنیم:
public class HttpException : Exception { System.Net.HttpStatusCode _statusCode; public HttpException(System.Net.HttpStatusCode statusCode, string description) : base(description) { if (description == null) throw new ArgumentNullException("description"); _statusCode = statusCode; } public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner) : base(description, inner) { if (description == null) throw new ArgumentNullException("description"); if (inner == null) throw new ArgumentNullException("inner"); _statusCode = statusCode; } public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { } public System.Net.HttpStatusCode StatusCode { get; private set; } }
public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } }
public class HttpException : Exception { // [...] public HttpException(SerializationInfo info, StreamingContext context) : base(info, context) { // this is new StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode"); } public HttpStatusCode StatusCode { get; private set; } public override string Message { get { return base.Message + "\r\nStatus code: " + StatusCode; } } // this is new public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); info.AddValue("HttpStatusCode", (int) StatusCode); } }
در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را میتوان ارائه داد، این مورد در یافتن راهحل خیلی کمک خواهد کرد.
طراحی پیامهای مناسب
پیامهای exception مختص به توسعهدهندگان است نه کاربران نهایی.
نوشتن این نوع پیامها برای برنامهنویس کار خستهکنندهایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType"); throw new Exception("Unecpected workingDirectory");
توسعهدهندگانی که exception message را در اولویت قرار میدهند، معتقد هستند که از لحاظ تجربهی کاربری پیامها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیشتر عنوان گردید این نوع پیامها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایههای زیرین. همچنین پیامهایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامهنویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیامها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیامها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوهی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیامهای استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیامها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناءهایی که توسط Base Class Library و دیگر کتابخانههای thirt-party صادر میشوند چیست؟ اینها انگلیسی هستند.
در تمامی حالتهایی که عنوان شد فرض بر این است که شما در حال نوشتن این نوع پیامها برای یک سیستم خاص هستید. اما اگر هدف نوشتن یک کتابخانه باشد چطور؟ در این حالت نمیدانید که کتابخانهی شما در کجا استفاده میشود.
اگر هدف نوشتن یک کتابخانه نباشد این نوع پیامهایی که برای کاربران نهایی باشند، وابستگیها را در سیستم افزایش خواهند داد، زیرا در این حالت پیامها به یک رابط کاربری خاص گره خواهند خورد.
خب اگر پیامها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام میتواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیامهای زیر مناسب هستند:
"Unecpected workingDirectory"
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory. "The invalid value was: "fllobdedy"."
همیشه برای نوشتن پیامهای مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوهی رفع مشکل را توضیح دهید
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
public class SendEmailsTask : ScheduledTaskTemplate
using System; namespace DNTScheduler.TestWebApplication.WebTasks { public class SendEmailsTask : ScheduledTaskTemplate { /// <summary> /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آنها را مشخص خواهد کرد /// </summary> public override int Order { get { return 1; } } public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Minute % 2 == 0 && now.Second == 1; } public override void Run() { if (this.IsShuttingDown || this.Pause) return; System.Diagnostics.Trace.WriteLine("Running Send Emails"); } public override string Name { get { return "ارسال ایمیل"; } } } }
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Hour == 23 && now.Minute == 33 && now.Second == 1; }
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System; using System.Net; namespace DNTScheduler.TestWebApplication.WebTasks { public static class ScheduledTasksRegistry { public static void Init() { ScheduledTasksCoordinator.Current.AddScheduledTasks( new SendEmailsTask(), new DoBackupTask()); ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) => { //todo: log the exception. System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message); }; ScheduledTasksCoordinator.Current.Start(); } public static void End() { ScheduledTasksCoordinator.Current.Dispose(); } public static void WakeUp(string pageUrl) { try { using (var client = new WebClient()) { client.Credentials = CredentialCache.DefaultNetworkCredentials; client.Headers.Add("User-Agent", "ScheduledTasks 1.0"); client.DownloadData(pageUrl); } } catch (Exception ex) { //todo: log ex System.Diagnostics.Trace.WriteLine(ex.Message); } } } }
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System; using System.Configuration; using DNTScheduler.TestWebApplication.WebTasks; namespace DNTScheduler.TestWebApplication { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { ScheduledTasksRegistry.Init(); } protected void Application_End() { ScheduledTasksRegistry.End(); //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]); } } }
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
<?xml version="1.0"?> <configuration> <appSettings> <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" /> </appSettings> </configuration>
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new { TaskName = x.Name, LastRunTime = x.LastRun, LastRunWasSuccessful = x.IsLastRunSuccessful, IsPaused = x.Pause, }).ToList();