اشتراکها
از EF Core 2.1 به بعد، قابلیت جدیدی تحت عنوان «تبدیلگرهای مقدار»، به آن اضافه شدهاست. برای مثال در EF Core، زمانیکه اطلاعات Enums، در بانک اطلاعاتی ذخیره میشوند، معادل عددی آنها درج خواهند شد. اگر علاقمند باشید تا بجای این مقادیر عددی دقیقا همان رشتهی تعریف کنندهی Enum درج شود، میتوان یک «تبدیلگر مقدار» را برای آن نوشت. برای مثال در موجودیت Rider زیر، خاصیت Mount از نوع یک enum است.
برای اینکه در حین درج رکوردهای Rider در بانک اطلاعاتی دقیقا از مقادیر رشتهای EquineBeast استفاده شود، میتوان به صورت زیر عمل کرد:
در اینجا در حین تعریف جزئیات نگاشت یک مدل میتوان متد جدید HasConversion را نیز فراخوانی کرد. پارامتر اول آن، روش تبدیل مقدار enum را به یک رشته، جهت درج در بانک اطلاعاتی و پارامتر دوم آن، روش تبدیل مقدار رشتهای خوانده شدهی از بانک اطلاعاتی را جهت وهله سازی یک Rider داری خاصیت enum، مشخص میکند.
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
مزیت اینکار این است که اگر قرار شد برای چندین خاصیت از تبدیلگر مقدار مشابهی استفاده کنیم، میتوان از یک converter تعریف شده بجای تکرار کدهای آن استفاده کرد.
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
نکته: تمام تبدیل کنندههای مقدار توکار EF Core، بدون حالت هستند. بنابراین میتوان یک تک وهلهی از آنها را بین چندین خاصیت به اشتراک گذاشت.
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
برای نمونه در اینجا با ذکر نوع رشته، تبدیل enum به string به صورت خودکار انجام خواهد شد. معادل اینکار، تعریف نوع سمت بانک اطلاعاتی این خاصیت است:
در این حالت حتی نیازی به تعریف HasConversion هم نیست.
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
در اینجا معکوس کردن رشتهها به عنوان الگوریتم سادهی رمزنگاری اطلاعات انتخاب شدهاست. نحوهی اعمال این ValueConverter جدید را نیز ملاحظه میکنید.
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
هدف از این Attribute خالی، صرفا نشانه گذاری خاصیتهایی است که قرار است به صورت رمزنگاری شده در بانک اطلاعاتی ذخیره شوند؛ مانند خاصیت Value زیر:
پس از آن، متد OnModelCreating را به صورت زیر اصلاح میکنیم تا به کمک Reflection و اطلاعات موجودیتهای ثبت شدهی در سیستم، متد SetValueConverter را بر روی خواصی که دارای EncryptedAttribute هستند، به صورت خودکار فراخوانی کند:
تاثیر ValueConverterها بر روی اعمال متداول کار با بانک اطلاعاتی
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
و یا کوئری زیر
به این نحو ترجمه خواهد شد:
یعنی نیازی نیست تا مقداری را که در حال جستجوی آن هستیم، خودمان به صورت دستی رمزنگاری کرده و سپس در کوئری قرار دهیم. اینکار به صورت خودکار انجام میشود.
public class Rider { public int Id { get; set; } public EquineBeast Mount { get; set; } } public enum EquineBeast { Donkey, Mule, Horse, Unicorn }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); }
نکته 1: مقادیر نال، هیچگاه به تبدیلگرهای مقدار، ارسال نمیشوند. اینکار پیاده سازی آنها را سادهتر میکند و همچنین میتوان آنها را بین خواص نالپذیر و نالنپذیر، به اشتراک گذاشت. بنابراین برای مقادیر نال نمیتوان تبدیلگر نوشت.
نکته 2: کاری که در متد HasConversion فوق انجام شدهاست، در حقیقت وهله سازی ضمنی یک ValueConverter و استفاده از آن است. میتوان اینکار را به صورت صریح نیز انجام داد:
var converter = new ValueConverter<EquineBeast, string>( v => v.ToString(), v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v)); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تبدیلگرهای مقدار توکار EF Core
برای بسیاری از اعمال متداول، در فضای نام Microsoft.EntityFrameworkCore.Storage.ValueConversion، تعدادی تبدیلگر مقدار تدارک دیده شدهاند که به این شرح میباشند:
BoolToZeroOneConverter: تبدیلگر bool به صفر و یک
BoolToStringConverter: تبدیلگر bool به Y و یا N
BoolToTwoValuesConverter: تبدیلگر bool به دو مقداری دلخواه
BytesToStringConverter: تبدیلگر آرایهای از بایتها به یک رشتهی Base64-encoded
CastingConverter: تبدیلگر یک نوع به نوعی دیگر
CharToStringConverter: تبدیلگر char به string
DateTimeOffsetToBinaryConverter: تبدیلگر DateTimeOffset به یک مقدار 64 بیتی باینری
DateTimeOffsetToBytesConverter: تبدیلگر DateTimeOffset به آرایهای از بایتها
DateTimeOffsetToStringConverter: تبدیلگر DateTimeOffset به رشته
DateTimeToBinaryConverter: تبدیلگر DateTime به یک مقدار 64 بیتی با درج DateTimeKind
DateTimeToStringConverter: تبدیلگر DateTime به یک رشته
DateTimeToTicksConverter: تبدیلگر DateTime به ticks آن
EnumToNumberConverter: تبدیلگر Enum به عدد متناظر با آن
EnumToStringConverter: تبدیلگر Enum به رشته
GuidToBytesConverter: تبدیلگر Guid به آرایهای از بایتها
GuidToStringConverter: تبدیلگر Guid به رشته
NumberToBytesConverter: تبدیلگر اعداد به آرایهای از بایتها
NumberToStringConverter: تبدیلگر اعداد به رشته
StringToBytesConverter: تبدیلگر رشته به آرایهای از بایتهای UTF8 معادل آن
TimeSpanToStringConverter: تبدیلگر TimeSpan به رشته
TimeSpanToTicksConverter: تبدیلگر TimeSpan به ticks آن
برای نمونه در این لیست، EnumToStringConverter نیز وجود دارد. بنابراین نیازی به تعریف دستی آن مانند مثال ابتدای بحث نیست و میتوان به صورت زیر از آن استفاده کرد:
var converter = new EnumToStringConverter<EquineBeast>(); modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion(converter);
تعیین نوع تبدیلگر مقدار، جهت ساده سازی تعاریف
برای حالاتی که تبدیلگر مقدار توکاری تعریف شدهاست، صرفا تعریف نوع تبدیل، کفایت میکند:
modelBuilder .Entity<Rider>() .Property(e => e.Mount) .HasConversion<string>();
public class Rider { public int Id { get; set; } [Column(TypeName = "nvarchar(24)")] public EquineBeast Mount { get; set; } }
نوشتن تبدیلگر خودکار مقادیر خواص، به نمونهای رمزنگاری شده
پس از آشنایی با مفهوم «تبدیلگرهای مقدار» در +EF Core 2.1، اکنون میتوانیم یک نمونهی سفارشی از آنرا نیز طراحی کنیم:
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { // … protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); // Custom application mappings builder.Entity<ConfigurationValue>(entity => { entity.Property(e => e.Value).IsRequired().HasConversion(encryptedConverter); }); } } }
میتوان قسمت HasConversion را به صورت زیر خودکار کرد:
ابتدا یک Attribute جدید را به نام Encrypted به برنامه اضافه میکنیم:
using System; namespace Test { [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] public sealed class EncryptedAttribute : Attribute { } }
namespace DbConfig.Web.DomainClasses { public class ConfigurationValue { public int Id { get; set; } public string Key { get; set; } [Encrypted] public string Value { get; set; } } }
namespace DbConfig.Web.DataLayer.Context { public class MyAppContext : DbContext { protected override void OnModelCreating(ModelBuilder builder) { var encryptedConverter = new ValueConverter<string, string>( convertToProviderExpression: v => new string(v.Reverse().ToArray()), // encrypt convertFromProviderExpression: v => new string(v.Reverse().ToArray()) // decrypt ); foreach (var entityType in builder.Model.GetEntityTypes()) { foreach (var property in entityType.GetProperties()) { var attributes = property.PropertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), false); if (attributes.Any()) { property.SetValueConverter(encryptedConverter); } } } }
از دیدگاه برنامه، ValueConverterهای تعریف شده، هیچگونه تاثیری را بر روی کوئری نوشتن و یا ثبت و ویرایش اطلاعات ندارند و عملکرد آنها کاملا از دیدگاه سایر قسمتهای برنامه مخفی است. برای مثال در برنامه، فرمان به روز رسانی خاصیت Value را با مقدار .A new value to test صادر کردهایم (مقدار دهی متداول)، اما همانطور که ملاحظه میکنید، نمونهی رمزنگاری شدهی آن به صورت خودکار در بانک اطلاعاتی درج شدهاست (پارامتر p0):
Executed DbCommand (22ms) [Parameters=[@p1='1', @p0='.tset ot eulav wen A' (Nullable = false) (Size = 4000)], CommandType='Text', CommandTimeout='180'] SET NOCOUNT ON; UPDATE [Configurations] SET [Value] = @p0 WHERE [Id] = @p1; SELECT @@ROWCOUNT;
و یا کوئری زیر
db.Set<ConfigurationValue>().Where(x => x.Value.EndsWith("world!"))
SELECT [x].[Id], [x].[Key], [x].[Value] FROM [Configurations] AS [x] WHERE RIGHT([x].[Value], LEN(N'world!')) = N'!dlrow'
اون افزونه رایگانی که مهندس نصیری معرفی کردن
از هر دو استفاده میکنه
یعنی هم متن درون view و Function , Procedure رو جستجو میکنه
که اگر رمز شده باشند دیگه نتیجه ای نداره
و هم عناوین ستونهای اون شئ رو جستجو میکنه
مطالب دورهها
آشنایی با نحوه ایجاد یک IoC Container
قبل از اینکه وارد بحث استفاده از کتابخانههای بسیار غنی IoC Container موجود شویم، بهتر است یک نمونه ساده آنها را طراحی کنیم تا بهتر بتوان با عملکرد و ساختار درونی آنها آشنا شد.
IoC Container چیست؟
IoC Container، فریم ورکی است برای انجام تزریق وابستگیها. در این فریم ورک امکان تنظیم اولیه وابستگیهای سیستم وجود دارد. برای مثال زمانیکه برنامه از یک IoC Container، نوع اینترفیس خاصی را درخواست میکند، این فریم ورک با توجه به تنظیمات اولیهاش، کلاسی مشخص را بازگشت خواهد داد.
IoC Containerهای قدیمیتر، برای انجام تنظیمات اولیه خود از فایلهای کانفیگ استفاده میکردند. نمونههای جدیدتر آنها از روشهای Fluent interfaces برای مشخص سازی تنظیمات خود بهره میبرند.
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
در این بین زنجیرهی وابستگیهای مورد نیاز نیز وهله سازی خواهند شد. برای مثال اگر وابستگی اول به وابستگی دوم برای وهله سازی نیاز دارد، کار وهله سازی وابستگیهای وابستگی دوم نیز به صورت خودکار انجام خواهند شد. (این موردی است که بسیاری از تازه واردان به این بحث تا یکبار آنرا امتحان نکنند باور نخواهند کرد!)
ج) سپس کد فراخوان وهله دریافتی را مورد پردازش قرار داده و سپس شروع به استفاده از متدها و خواص آن خواهد نمود.
در تصویر فوق محل قرارگیری یک IOC Container را مشاهده میکنید. یک IOC Container در مورد تمام وابستگیهای مورد نیاز، اطلاعات لازم را دارد. همچنین این فریم ورک در مورد کلاسی که قرار است از وابستگیهای سیستم استفاده نماید نیز مطلع است؛ به این ترتیب میتواند به صورت خودکار در زمان وهله سازی آن، نوعهای وابستگیهای مورد نیاز آنرا در اختیارش قرار دهد.
برای مثال در اینجا MyClass، وابستگی مشخص شده در سازنده خود را به نام IDependency از IOC Container درخواست میکند. سپس این IOC Container بر اساس تنظیمات اولیه خود، یکی از وابستگیهای A یا B را بازگشت خواهد داد.
آغاز به کار ساخت یک IOC Container نمونه
در ابتدا کدهای آغازین مثال بحث جاری را در نظر بگیرید:
در اینجا وابستگیهای کلاس خریدار از طریق سازنده آن که متداولترین روش تزریق وابستگیها است، در اختیار آن قرار خواهد گرفت. یک اینترفیس کردیت کارت تعریف شدهاست به همراه دو پیاده سازی نمونه آن مانند مسترکارت و ویزا کارت. سادهترین نوع فراخوانی آن نیز میتواند مانند کدهای ذیل باشد (تزریق وابستگیهای دستی):
در ادامه قصد داریم این فراخوانیها را اندکی هوشمندتر کنیم تا بتوان بر اساس تنظیمات برنامه، کار تزریق وابستگیها صورت گیرد و به سادگی بتوان اینترفیسهای متفاوتی را در اینجا درخواست و مورد استفاده قرار داد. اینجا است که به اولین IoC Container خود خواهیم رسید:
در اینجا کدهای کلاس Resolver یا همان IoC Container ابتدایی بحث را مشاهده میکنید. توضیحات قسمتهای مختلف آن به صورت کامنت ارائه شدهاند.
در ادامه نحوه استفاده از IoC Container ایجاد شده را مشاهده میکنید.
ابتدا کار تعاریف نگاشتهای اولیه انجام میشود. در این صورت زمانیکه متد Resolve فراخوانی میگردد، نوع درخواستی آن به همراه سازنده دارای آرگومانی از نوع ICreditCard وهله سازی شده و بازگشت داده خواهد شد. سپس با در دست داشتن یک وهله آماده، متد Charge آنرا فراخوانی خواهیم کرد.
بررسی نحوه استفاده از Microsoft Unity به عنوان یک IoC Container
Unity چیست؟
Unity یک فریم ورک IoC Container تهیه شده توسط مایکروسافت میباشد که آنرا به عنوان جزئی از Enterprise Library خود قرار داده است. بنابراین برای دریافت آن یا میتوان کل مجموعه Enterprise Library را دریافت کرد و یا به صورت مجزا به عنوان یک بسته نیوگت نیز قابل تهیه است.
برای این منظور در خط فرمان پاورشل نیوگت در VS.NET دستور ذیل را اجرا کنید:
پیاده سازی مثال خریدار توسط Unity
همان مثال قسمت قبل را درنظر بگیرید. قصد داریم اینبار بجای IoC Container دست سازی که تهیه شد، پیاده سازی آنرا به کمک MS Unity انجام دهیم.
همانطور که ملاحظه میکنید، API آن بسیار شبیه به کلاس دست سازی است که در قسمت قبل تهیه کردیم.
مطابق کدهای فوق، ابتدا تنظیمات IoC Container انجام شده است. به آن اعلام کردهایم که در صورت نیاز به ICreditCard، نوع MasterCard را یافته و وهله سازی کن. با این تفاوت که Unity هوشمندتر بوده و سطر مربوط به ثبت کلاس Shoper ایی را که در قسمت قبل انجام دادیم، در اینجا حذف شده است.
سپس به این IoC Container اعلام کردهایم که نیاز به یک وهله از کلاس خریدار داریم. در اینجا Unity کار وهله سازیهای خودکار وابستگیها و تزریق آنها را در سازنده کلاس خریدار انجام داده و نهایتا یک وهله قابل استفاده را در اختیار ادامه برنامه قرار خواهد داد.
یک نکته:
به صورت پیش فرض کار تزریق وابستگیها در سازنده کلاسها به صورت خودکار انجام میشود. اگر نیاز به Setter injection و مقدار دهی خواص کلاس وجود داشت میتوان به نحو ذیل عمل کرد:
نام خاصیت و مقدار مورد نظر به عنوان پارامتر متد RegisterType باید تعریف شوند.
مدیریت طول عمر اشیاء در Unity
توسط یک IoC Container میتوان یک وهله معمولی از شیءایی را درخواست کرد و یا حتی طول عمر این وهله را به صورت Singleton معرفی نمود (یک وهله در طول عمر کل برنامه). در Unity اگر تنظیم خاصی اعمال نشود، هربار که متد Resolve فراخوانی میگردد، یک وهله جدید را در اختیار ما قرار خواهد داد. اما اگر پارامتر متد RegisterType را با وهلهای از ContainerControlledLifetimeManager مقدار دهی کنیم:
از این پس با هربار فراخوانی متد Resolve، در صورت نیاز به وابستگی از نوع ICreditCard، تنها یک وهله مشترک از MasterCard ارائه خواهد شد.
حالت پیش فرض مورد استفاده، بدون ذکر پارامتر متد RegisterType، مقدار TransientLifetimeManager میباشد.
IoC Container چیست؟
IoC Container، فریم ورکی است برای انجام تزریق وابستگیها. در این فریم ورک امکان تنظیم اولیه وابستگیهای سیستم وجود دارد. برای مثال زمانیکه برنامه از یک IoC Container، نوع اینترفیس خاصی را درخواست میکند، این فریم ورک با توجه به تنظیمات اولیهاش، کلاسی مشخص را بازگشت خواهد داد.
IoC Containerهای قدیمیتر، برای انجام تنظیمات اولیه خود از فایلهای کانفیگ استفاده میکردند. نمونههای جدیدتر آنها از روشهای Fluent interfaces برای مشخص سازی تنظیمات خود بهره میبرند.
زمانیکه از یک IOC Container در کدهای خود استفاده میکنید، مراحلی چند رخ خواهند داد:
الف) کد فراخوان، از IOC Container، یک شیء مشخص را درخواست میکند. عموما اینکار با درخواست یک اینترفیس صورت میگیرد؛ هرچند محدودیتی نیز نداشته و امکان درخواست یک کلاس از نوعی مشخص نیز وجود دارد.
ب) در ادامه IOC Container به لیست اشیاء قابل ارائه توسط خود نگاه کرده و در صورت وجود، وهله سازی شیء درخواست شده را انجام و نهایتا شیء مطلوب را بازگشت خواهد داد.
در این بین زنجیرهی وابستگیهای مورد نیاز نیز وهله سازی خواهند شد. برای مثال اگر وابستگی اول به وابستگی دوم برای وهله سازی نیاز دارد، کار وهله سازی وابستگیهای وابستگی دوم نیز به صورت خودکار انجام خواهند شد. (این موردی است که بسیاری از تازه واردان به این بحث تا یکبار آنرا امتحان نکنند باور نخواهند کرد!)
ج) سپس کد فراخوان وهله دریافتی را مورد پردازش قرار داده و سپس شروع به استفاده از متدها و خواص آن خواهد نمود.
در تصویر فوق محل قرارگیری یک IOC Container را مشاهده میکنید. یک IOC Container در مورد تمام وابستگیهای مورد نیاز، اطلاعات لازم را دارد. همچنین این فریم ورک در مورد کلاسی که قرار است از وابستگیهای سیستم استفاده نماید نیز مطلع است؛ به این ترتیب میتواند به صورت خودکار در زمان وهله سازی آن، نوعهای وابستگیهای مورد نیاز آنرا در اختیارش قرار دهد.
برای مثال در اینجا MyClass، وابستگی مشخص شده در سازنده خود را به نام IDependency از IOC Container درخواست میکند. سپس این IOC Container بر اساس تنظیمات اولیه خود، یکی از وابستگیهای A یا B را بازگشت خواهد داد.
آغاز به کار ساخت یک IOC Container نمونه
در ابتدا کدهای آغازین مثال بحث جاری را در نظر بگیرید:
using System; namespace DI01 { public interface ICreditCard { string Charge(); } public class Visa : ICreditCard { public string Charge() { return "Charging with the Visa!"; } } public class MasterCard : ICreditCard { public string Charge() { return "Swiping the MasterCard!"; } } public class Shopper { private readonly ICreditCard creditCard; public Shopper(ICreditCard creditCard) { this.creditCard = creditCard; } public void Charge() { var chargeMessage = creditCard.Charge(); Console.WriteLine(chargeMessage); } } }
var shopper = new Shopper(new Visa()); shopper.Charge();
using System; using System.Collections.Generic; using System.Linq; namespace DI01 { public class Resolver { //کار ذخیره سازی و نگاشت از یک نوع به نوعی دیگر در اینجا توسط این دیکشنری انجام خواهد شد private Dictionary<Type, Type> dependencyMap = new Dictionary<Type, Type>(); /// <summary> /// یک نوع خاص از آن درخواست شده و سپس بر اساس تنظیمات برنامه، کار وهله سازی /// نمونه معادل آن صورت خواهد گرفت /// </summary> public T Resolve<T>() { return (T)Resolve(typeof(T)); } private object Resolve(Type typeToResolve) { Type resolvedType; // ابتدا بررسی میشود که آیا در تنظیمات برنامه نگاشت متناظری برای نوع درخواستی وجود دارد؟ if (!dependencyMap.TryGetValue(typeToResolve, out resolvedType)) { //اگر خیر، کار متوقف خواهد شد throw new Exception(string.Format("Could not resolve type {0}", typeToResolve.FullName)); } var firstConstructor = resolvedType.GetConstructors().First(); var constructorParameters = firstConstructor.GetParameters(); // در ادامه اگر این نوع، دارای سازندهی بدون پارامتری است // بلافاصله وهله سازی خواهد شد if (!constructorParameters.Any()) return Activator.CreateInstance(resolvedType); var parameters = new List<object>(); foreach (var parameterToResolve in constructorParameters) { // در اینجا یک فراخوانی بازگشتی صورت گرفته است برای وهله سازی // خودکار پارامترهای مختلف سازنده یک کلاس parameters.Add(Resolve(parameterToResolve.ParameterType)); } return firstConstructor.Invoke(parameters.ToArray()); } public void Register<TFrom, TTo>() { dependencyMap.Add(typeof(TFrom), typeof(TTo)); } } }
var resolver = new Resolver(); //تنظیمات اولیه resolver.Register<Shopper, Shopper>(); resolver.Register<ICreditCard, Visa>(); //تزریق وابستگیها و وهله سازی var shopper = resolver.Resolve<Shopper>(); shopper.Charge();
ابتدا کار تعاریف نگاشتهای اولیه انجام میشود. در این صورت زمانیکه متد Resolve فراخوانی میگردد، نوع درخواستی آن به همراه سازنده دارای آرگومانی از نوع ICreditCard وهله سازی شده و بازگشت داده خواهد شد. سپس با در دست داشتن یک وهله آماده، متد Charge آنرا فراخوانی خواهیم کرد.
بررسی نحوه استفاده از Microsoft Unity به عنوان یک IoC Container
Unity چیست؟
Unity یک فریم ورک IoC Container تهیه شده توسط مایکروسافت میباشد که آنرا به عنوان جزئی از Enterprise Library خود قرار داده است. بنابراین برای دریافت آن یا میتوان کل مجموعه Enterprise Library را دریافت کرد و یا به صورت مجزا به عنوان یک بسته نیوگت نیز قابل تهیه است.
برای این منظور در خط فرمان پاورشل نیوگت در VS.NET دستور ذیل را اجرا کنید:
PM> Install-Package Unity
پیاده سازی مثال خریدار توسط Unity
همان مثال قسمت قبل را درنظر بگیرید. قصد داریم اینبار بجای IoC Container دست سازی که تهیه شد، پیاده سازی آنرا به کمک MS Unity انجام دهیم.
using Microsoft.Practices.Unity; namespace DI02 { class Program { static void Main(string[] args) { var container = new UnityContainer(); container.RegisterType<ICreditCard, MasterCard>(); var shopper = container.Resolve<Shopper>(); shopper.Charge(); } } }
مطابق کدهای فوق، ابتدا تنظیمات IoC Container انجام شده است. به آن اعلام کردهایم که در صورت نیاز به ICreditCard، نوع MasterCard را یافته و وهله سازی کن. با این تفاوت که Unity هوشمندتر بوده و سطر مربوط به ثبت کلاس Shoper ایی را که در قسمت قبل انجام دادیم، در اینجا حذف شده است.
سپس به این IoC Container اعلام کردهایم که نیاز به یک وهله از کلاس خریدار داریم. در اینجا Unity کار وهله سازیهای خودکار وابستگیها و تزریق آنها را در سازنده کلاس خریدار انجام داده و نهایتا یک وهله قابل استفاده را در اختیار ادامه برنامه قرار خواهد داد.
یک نکته:
به صورت پیش فرض کار تزریق وابستگیها در سازنده کلاسها به صورت خودکار انجام میشود. اگر نیاز به Setter injection و مقدار دهی خواص کلاس وجود داشت میتوان به نحو ذیل عمل کرد:
container.RegisterType<ICreditCard, MasterCard>(new InjectionProperty("propertyName", 5));
مدیریت طول عمر اشیاء در Unity
توسط یک IoC Container میتوان یک وهله معمولی از شیءایی را درخواست کرد و یا حتی طول عمر این وهله را به صورت Singleton معرفی نمود (یک وهله در طول عمر کل برنامه). در Unity اگر تنظیم خاصی اعمال نشود، هربار که متد Resolve فراخوانی میگردد، یک وهله جدید را در اختیار ما قرار خواهد داد. اما اگر پارامتر متد RegisterType را با وهلهای از ContainerControlledLifetimeManager مقدار دهی کنیم:
container.RegisterType<ICreditCard, MasterCard>(new ContainerControlledLifetimeManager());
حالت پیش فرض مورد استفاده، بدون ذکر پارامتر متد RegisterType، مقدار TransientLifetimeManager میباشد.
در دنیای NET. همواره دو نوع LINQ وجود داشته داشته است: LINQ to Objects و ... مابقی. در حالت اول با <IEnumerable<Tها کار میکنیم که تمام عملیات در حافظه انجام میشود و در مابقی حالات یک <IQueryable<T وجود دارد که عبارت حاصل از آن جهت کاربردهای مختلفی به زبانهای متفاوتی مانند SQL ترجمه میشوند. در هر دو حالت کلی، Syntax نهایی یکی است و تنها اگر به منبع دادهی آنها دقت کنیم، میتوانیم نوع آنها را تشخیص دهیم. برای نمونه کوئری ذیل بر اساس منبع Blogs است که میتواند LINQ to Objects باشد و یا حالت <Queryable<Blog که قرار است به زبانی مشخص ترجمه شود:
اکنون فرض کنید که این عبارت قرار است به SQL ترجمه شده و سپس بر روی یک بانک اطلاعاتی اجرا شود. در این حالت مفسر LINQ باید بداند که متد Contains را چگونه به معادل SQL آن ترجمه کند و این ترجمه میتواند بر اساس بانکهای اطلاعاتی مختلف، متفاوت نیز باشد. اما در حالت LINQ to Objects یک چنین مشکلی وجود ندارد و این ترجمه مستقیما بر روی متد Contains کلاس string انجام میشود.
اما اکنون چطور؟
فرض کنید یک متد الحاقی را به نام ComputeHash به کلاس string اضافه کردهایم. یک چنین کوئری را اگر بر روی EF 6.x اجرا کنیم، برنامه با یک استثناء متوقف خواهد شد؛ چون امکان ترجمهی متد ComputeHash را به معادل SQL آن ندارد؛ اما EF Core برای انجام یک چنین کوئریهایی بهبود یافتهاست که به آن، محاسبات سمت کلاینت گفته میشود.
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
همچنین یک متد الحاقی را به نام ComputeHash به صورت ذیل تعریف کردهایم:
اکنون میخواهیم بلاگهایی را پیدا کنیم که Hash مربوط به Url آنها بیشتر از 10 است (صرفا جهت نمایش این قابلیت جدید):
اگر این کوئری را اجرا کنیم، یک چنین خروجی SQL ایی تولید خواهد شد و همچنین برنامه کرش هم نمیکند:
به این معنا که در ارزیابیهای سمت کلاینت:
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
این مثال بدون مشکل توسط EF Core و قابلیت جدید ارزیابی سمت کلاینت آن اجرا میشود، اما بهتر است از وقوع یک چنین رخدادهایی مطلع شویم:
برای این منظور تنها کافی است درخواست فعالسازی لاگ کردن QueryClientEvaluationWarning را در قسمت ConfigureWarnings آن ارائه دهیم. در این حالت اگر برنامه را مجددا اجرا کنیم، ابتدا یک چنین خروجی لاگ میشود:
عنوان کردهاست که قابلیت ترجمهی ComputeHash را به SQL نداشته و آنرا در نهایت به صورت محلی و در سمت کلاینت محاسبه میکند.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
اینبار به یک خروجی SQL قابل محاسبهی در سمت سرور، خواهیم رسید:
به همین جهت حداقل لاگ کردن ارزیابیهای سمت کلاینت را روشن کنید تا از وقوع یک چنین مسایلی مطلع گردید.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
var blogs = from blog in Blogs where blog.Name.Contains("Development") select blog;
اما اکنون چطور؟
var blogs = from blog in Blogs where blog.Name.ComputeHash() == 0 select blog;
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
public static class StringExtensions { public static int ComputeHash(this string str) { var hash = 0; foreach (var ch in str) { hash += (int)ch; } return hash; } }
using (var context = new BloggingContext()) { var blogs = context.Blogs .Where(blog => blog.Url.ComputeHash() >= 10) .ToList(); Console.WriteLine(blogs.First().Url); }
SELECT [blog].[BlogId], [blog].[Url] FROM [Blogs] AS [blog]
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs .Select(b => new { IdUrlString = string.Join(", ", b.BlogId, b.Url), }).ToList();
public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;"); optionsBuilder.ConfigureWarnings(warnings => { warnings.Log(CoreEventId.IncludeIgnoredWarning); warnings.Log(RelationalEventId.QueryClientEvaluationWarning); }); } } }
warn: Microsoft.EntityFrameworkCore.Query[200500] The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs .Select(b => new { IdUrlString = b.BlogId + "," + b.Url }).ToList();
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString] FROM [Blogs] AS [b]
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
اگر در کدهای خود قطعه کد ذیل را دارید:
استفادهی از using در اینجا، نهتنها غیرضروری و اشتباه است، بلکه سبب از کار افتادن زود هنگام برنامهی شما با صدور استثنای ذیل خواهد شد:
HttpClient خود را Dispose نکنید
کلاس HttpClient اینترفیس IDisposable را پیاده سازی میکند. بنابراین روش استفادهی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
اما در این حال فرض کنید به همین روش تعدادی درخواست را ارسال کردهاید:
مشکل این روش، در ایجاد سوکتهای متعددی است که حتی پس از بسته شدن برنامه نیز باز، باقی خواهند ماند:
این یک نمونهی خروجی برنامهی فوق، توسط دستور netstat «پس از بسته شدن کامل برنامه» است.
بنابراین اگر برنامهی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال میکند، سیستم عامل به حد اشباع ایجاد سوکتهای جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفادهی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکتهای ایجاد شدهاست. TIME_WAIT ایی را که در اینجا ملاحظه میکنید، به معنای بسته شدن اتصال از طرف برنامهی ما است؛ اما سیستم عامل هنوز منتظر نتیجهی نهایی، از طرف دیگر اتصال است که آیا قرار است بستهی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشتهاست. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم میشود:
بنابراین روش توصیه شدهی کار با HttpClient، داشتن یک وهلهی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شدهاست و دسترسی به یک شیء سراسری آن در برنامههای چند ریسمانی مشکلی را ایجاد نمیکند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.
تمام اجزای HttpClient به صورت Thread-safe طراحی نشدهاند
تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهلهی Singleton از آنرا در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکتهای سیستم عامل و ناپایدار کردن تمام برنامههایی که از آن سرویس میگیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفادهی از متدهای ذیل این کلاس thread-safe هستند:
اما تغییر این خواص در کلاس HttpClient به هیچ عنوان thread-safe نبوده و در برنامههای چند ریسمانی و چند کاربری، مشکل ساز میشوند:
بنابراین در طراحی کلاس مدیریت کنندهی HttpClient برنامهی خود نیاز است به ازای هر BaseAddress، یک HttpClient خاص آنرا ایجاد کرد و HttpClientهای سراسری نمیتوانند BaseAddressهای خود را نیز به اشتراک گذاشته و تغییری را در آن ایجاد کنند.
استفادهی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمیشود
با طراحی یک کلاس مدیریت کنندهی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفادهی مجدد میشوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجارهی اتصال آنرا دقیقا مشخص کنیم:
با اینکار هرچند هنوز هم از اتصالات استفادهی مجدد میشود، اما این استفادهی مجدد، نامحدود نبوده و مدت معینی را پیدا میکند.
طراحی یک کلاس، برای مدیریت سراسری وهلههای HttpClient
تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهلهی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان میبرد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شدهاست؛ اما تعدادی از خواص آن مانند BaseAddress اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمییابند)، نیاز است timeout آنرا تنظیم کرد.
بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
در اینجا به ازای هر baseAddress جدید، یک HttpClient خاص آن ایجاد میشود تا در کل برنامه مورد استفادهی مجدد قرار گیرد. برای مدیریت thread-safe ایجاد HttpClientها نیز از نکتهی مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary» استفاده شدهاست. همچنین نکات تنظیم ConnectionLeaseTimeout و سایر خواص غیر thread-safe کلاس HttpClient نیز در اینجا لحاظ شدهاند.
پس از تدارک این کلاس، نحوهی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده میکنید، آنرا به صورت ذیل ثبت کنید:
اکنون، یک نمونه، نحوهی استفادهی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل میباشد:
سرویس IHttpClientFactory یک HttpClient را به ازای host درخواستی ایجاد کرده و در طول عمر برنامه از آن استفادهی مجدد میکند. به همین جهت دیگر مشکل اشباع سوکتها در این سیستم رخ نخواهند داد.
برای مطالعهی بیشتر
You're using HttpClient wrong and it is destabilizing your software
Disposable, Finalizers, and HttpClient
Using HttpClient as it was intended (because you’re not)
Singleton HttpClient? Beware of this serious behaviour and how to fix it
Beware of the .NET HttpClient
Effectively Using HttpClient
using(var client = new HttpClient()) { // do something with http client }
Unable to connect to the remote server System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted.
HttpClient خود را Dispose نکنید
کلاس HttpClient اینترفیس IDisposable را پیاده سازی میکند. بنابراین روش استفادهی اصولی آن باید به صورت ذیل و با پیاده سازی خودکار رهاسازی منابع مرتبط با آن باشد:
using (var client = new HttpClient()) { var result = await client.GetAsync("http://example.com/"); }
for (int i = 0; i < 10; i++) { using (var client = new HttpClient()) { var result = await client.GetAsync("http://example.com/"); Console.WriteLine(result.StatusCode); } }
TCP 192.168.1.6:13996 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13997 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13998 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:13999 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14000 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14001 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14002 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14003 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14004 93.184.216.34:http TIME_WAIT TCP 192.168.1.6:14005 93.184.216.34:http TIME_WAIT
بنابراین اگر برنامهی شما تعداد زیادی کاربر دارد و یا تعداد زیادی درخواست را به روش فوق ارسال میکند، سیستم عامل به حد اشباع ایجاد سوکتهای جدید خواهد رسید.
این مشکل نیز ارتباطی به طراحی این کلاس و یا زبان #C و حتی استفادهی از using نیز ندارد. این رفتار، رفتار معمول سیستم عامل، با سوکتهای ایجاد شدهاست. TIME_WAIT ایی را که در اینجا ملاحظه میکنید، به معنای بسته شدن اتصال از طرف برنامهی ما است؛ اما سیستم عامل هنوز منتظر نتیجهی نهایی، از طرف دیگر اتصال است که آیا قرار است بستهی TCP ایی را دریافت کند یا خیر و یا شاید در بین راه تاخیری وجود داشتهاست. برای نمونه ویندوز به مدت 240 ثانیه یک اتصال را در این حالت حفظ خواهد کرد، که مقدار آن نیز در اینجا تنظیم میشود:
[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay]
بنابراین روش توصیه شدهی کار با HttpClient، داشتن یک وهلهی سراسری از آن در برنامه و عدم Dispose آن است. HttpClient نیز thread-safe طراحی شدهاست و دسترسی به یک شیء سراسری آن در برنامههای چند ریسمانی مشکلی را ایجاد نمیکند. همچنین Dispose آن نیز غیرضروری است و پس از پایان برنامه به صورت خودکار توسط سیستم عامل انجام خواهد شد.
تمام اجزای HttpClient به صورت Thread-safe طراحی نشدهاند
تا اینجا به این نتیجه رسیدیم که روش صحیح کار کردن با HttpClient، نیاز به داشتن یک وهلهی Singleton از آنرا در سراسر برنامه دارد و Dispose صریح آن، بجز اشباع سوکتهای سیستم عامل و ناپایدار کردن تمام برنامههایی که از آن سرویس میگیرند، حاصلی را به همراه نخواهد داشت. در این بین مطابق مستندات HttpClient، استفادهی از متدهای ذیل این کلاس thread-safe هستند:
CancelPendingRequests DeleteAsync GetAsync GetByteArrayAsync GetStreamAsync GetStringAsync PostAsync PutAsync SendAsync
BaseAddress DefaultRequestHeaders MaxResponseContentBufferSize Timeout
استفادهی سراسری و مجدد از HttpClient، تغییرات DNS را متوجه نمیشود
با طراحی یک کلاس مدیریت کنندهی سراسری HttpClient با طول عمر Singelton، به یک مشکل دیگر نیز برخواهیم خورد: چون در اینجا از اتصالات، استفادهی مجدد میشوند، دیگر تغییرات DNS را لحاظ نخواهند کرد.
برای حل این مشکل، در زمان ایجاد یک HttpClient سراسری، به ازای یک BaseAddress مشخص، باید از ServicePointManager کوئری گرفته و زمان اجارهی اتصال آنرا دقیقا مشخص کنیم:
var sp = ServicePointManager.FindServicePoint(new Uri("http://thisisasample.com")); sp.ConnectionLeaseTimeout = 60*1000; //In milliseconds
طراحی یک کلاس، برای مدیریت سراسری وهلههای HttpClient
تا اینجا به صورت خلاصه به نکات ذیل رسیدیم:
- HttpClient باید به صورت یک وهلهی سراسری Singleton مورد استفاده قرار گیرد. هر وهله سازی مجدد آن 35ms زمان میبرد.
- Dispose یک HttpClient غیرضروری است.
- HttpClient تقریبا thread safe طراحی شدهاست؛ اما تعدادی از خواص آن مانند BaseAddress اینگونه نیستند.
- برای رفع مشکل اتصالات چسبنده (اتصالاتی که هیچگاه پایان نمییابند)، نیاز است timeout آنرا تنظیم کرد.
بنابراین بهتر است این نکات را در یک کلاس به صورت ذیل کپسوله کنیم:
using System; using System.Collections.Generic; using System.Net.Http; namespace HttpClientTips { public interface IHttpClientFactory : IDisposable { HttpClient GetOrCreate( Uri baseAddress, IDictionary<string, string> defaultRequestHeaders = null, TimeSpan? timeout = null, long? maxResponseContentBufferSize = null, HttpMessageHandler handler = null); } }
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Threading; namespace HttpClientTips { /// <summary> /// Lifetime of this class should be set to `Singleton`. /// </summary> public class HttpClientFactory : IHttpClientFactory { // 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the HttpClient more than // once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple // threads but only one of the objects succeeds in creating the HttpClient. private readonly ConcurrentDictionary<Uri, Lazy<HttpClient>> _httpClients = new ConcurrentDictionary<Uri, Lazy<HttpClient>>(); private const int ConnectionLeaseTimeout = 60 * 1000; // 1 minute public HttpClientFactory() { // Default is 2 minutes: https://msdn.microsoft.com/en-us/library/system.net.servicepointmanager.dnsrefreshtimeout(v=vs.110).aspx ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds; // Increases the concurrent outbound connections ServicePointManager.DefaultConnectionLimit = 1024; } public HttpClient GetOrCreate( Uri baseAddress, IDictionary<string, string> defaultRequestHeaders = null, TimeSpan? timeout = null, long? maxResponseContentBufferSize = null, HttpMessageHandler handler = null) { return _httpClients.GetOrAdd(baseAddress, uri => new Lazy<HttpClient>(() => { // Reusing a single HttpClient instance across a multi-threaded application means // you can't change the values of the stateful properties (which are not thread safe), // like BaseAddress, DefaultRequestHeaders, MaxResponseContentBufferSize and Timeout. // So you can only use them if they are constant across your application and need their own instance if being varied. var client = handler == null ? new HttpClient { BaseAddress = baseAddress } : new HttpClient(handler, disposeHandler: false) { BaseAddress = baseAddress }; setRequestTimeout(timeout, client); setMaxResponseBufferSize(maxResponseContentBufferSize, client); setDefaultHeaders(defaultRequestHeaders, client); setConnectionLeaseTimeout(baseAddress, client); return client; }, LazyThreadSafetyMode.ExecutionAndPublication)).Value; } public void Dispose() { foreach (var httpClient in _httpClients.Values) { httpClient.Value.Dispose(); } } private static void setConnectionLeaseTimeout(Uri baseAddress, HttpClient client) { // This ensures connections are used efficiently but not indefinitely. client.DefaultRequestHeaders.ConnectionClose = false; // keeps the connection open -> more efficient use of the client ServicePointManager.FindServicePoint(baseAddress).ConnectionLeaseTimeout = ConnectionLeaseTimeout; // ensures connections are not used indefinitely. } private static void setDefaultHeaders(IDictionary<string, string> defaultRequestHeaders, HttpClient client) { if (defaultRequestHeaders == null) { return; } foreach (var item in defaultRequestHeaders) { client.DefaultRequestHeaders.Add(item.Key, item.Value); } } private static void setMaxResponseBufferSize(long? maxResponseContentBufferSize, HttpClient client) { if (maxResponseContentBufferSize.HasValue) { client.MaxResponseContentBufferSize = maxResponseContentBufferSize.Value; } } private static void setRequestTimeout(TimeSpan? timeout, HttpClient client) { if (timeout.HasValue) { client.Timeout = timeout.Value; } } } }
پس از تدارک این کلاس، نحوهی معرفی آن به سیستم باید به صورت Singleton باشد. برای مثال اگر از ASP.NET Core استفاده میکنید، آنرا به صورت ذیل ثبت کنید:
namespace HttpClientTips.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHttpClientFactory, HttpClientFactory>(); services.AddMvc(); }
اکنون، یک نمونه، نحوهی استفادهی از اینترفیس IHttpClientFactory تزریقی به صورت ذیل میباشد:
namespace HttpClientTips.Web.Controllers { public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; public HomeController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<IActionResult> Index() { var host = new Uri("http://localhost:5000"); var httpClient = _httpClientFactory.GetOrCreate(host); var responseMessage = await httpClient.GetAsync("home/about").ConfigureAwait(false); var responseContent = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false); return Content(responseContent); }
برای مطالعهی بیشتر
You're using HttpClient wrong and it is destabilizing your software
Disposable, Finalizers, and HttpClient
Using HttpClient as it was intended (because you’re not)
Singleton HttpClient? Beware of this serious behaviour and how to fix it
Beware of the .NET HttpClient
Effectively Using HttpClient
فعال سازی تنظیمات مسیریابی
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
که مطابق سورس ASP.NET Core، معادل است با فراخوانی متد app.UseMvc، با قالب پیش فرضی به صورت زیر:
قالب مشخص شدهی در اینجا به ASP.NET MVC میگوید که از کدام قسمتهای URL باید نام کلاس کنترلر (کلاس ویژهای که به کلمهی Controller ختم میشود) و نام اکشن متد متناظر با آنرا انتخاب کند (اکشن متد، متدی است عمومی در آن کلاس).
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
علت کار کردن مسیریابی آن نیز به ذکر متد app.UseMvcWithDefaultRoute در کلاس آغازین برنامه بر میگردد و همانطور که عنوان شد، این فراخوانی را میتوان با فراخوانی واضحتر ذیل جایگزین کرد:
پارامتر این متد که جایگزین متد ConfigureRoutes، در نگارشهای قبلی ASP.NET MVC شدهاست، از نوع IRouteBuilder میباشد.
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
این کلاس و کنترلر، به صورت پیش فرض نیاز به تعریف هیچ نوع مسیریابی جدیدی ندارد. همان مسیریابی پیش فرض ثبت شدهی در کلاس آغازین برنامه، تمام متدهای عمومی آن یا همان اکشن متدهای آنرا پوشش میدهد. برای مثال جهت رسیدن به اکشن متد SiteName آن، میتوان آدرس /About/SiteName/ را درخواست داد.
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
در اینجا با اولین Route تعریف شده، مشخص کردهایم که اگر قسمت اول URL درخواستی معادل about بود، پردازش برنامه باید به این کنترلر هدایت شود. بدیهی است الزامی به یکی بودن نام Route، با نام کنترلر، وجود ندارد. همچنین Route تعریف شدهی با رشتهی خالی، به معنای مسیریابی پیش فرض است. یعنی اگر آدرس /about/ درخواست داده شد، اکشن متد پیش فرض آن، متد Hello خواهد بود. در این حالت، ذکر Route بعدی برای اکشن متد SiteName الزامی است و اگر اینکار صورت نگیرد، به استثنای ذیل خواهیم رسید:
که عنوان کردهاست در این حالت مشخص نیست که اکشن متد پیش فرض، کدام است.
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
عموما مرسوم است که نام مسیریابی کنترلر همان نام کنترلر باشد و نام مسیریابی اکشن متد، همان نام اکشن متد مربوطه. به همین جهت میتوان از توکنهای ویژهی [controller] و [action] نیز در اینجا استفاده کرد که دقیقا به همان نام کنترلر و اکشن متد متناظر با آنها تفسیر خواهند شد. مزیت اینکار این است که در صورت تغییر نام متدها یا کنترلرها، دیگر نیازی نیست تا نامهای تعریف شدهی در ویژگیهای Route را نیز تغییر داد.
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
وجود api در اینجا به این معنا است که از این پس تنها آدرس /api/about/ پردازش خواهد شد و اگر صرفا آدرس /about/ درخواست شود، با خطای 404 و یا یافت نشد، کار خاتمه مییابد.
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
در اینجا تعریف مسیریابی آن با users/ و user معانی کاملا متفاوتی را دارند. اگر مسیریابی Users/{userid}/ را تعریف کنیم، یعنی مسیر ذیل از ریشهی سایت باید درخواست شود: http://localhost:7742/users/1
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
در اینجا اگر بخواهیم نوع پارامتر درخواستی را نیز دقیقا مشخص و مقید کنیم، میتوان به صورت ذیل عمل کرد:
اگر این کار را انجام ندهیم، با درخواست مسیر http://localhost:7742/dnt/about/users/test مقدار صفر به userId ارسال میشود (چون پارامتر test عددی نیست). اگر تنظیم فوق را انجام دهیم، کاربر خطای 404 را دریافت میکند.
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
مزیت آن این است که اکنون برای اشارهی به این مسیریابی خاص میتوان از این نام تعریف شده استفاده کرد:
پارامتر اول ذکر شده، نام مسیریابی و پارامتر دوم، پارامترهای مرتبط با این مسیریابی هستند.
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
در اینجا یک کلاس جدید را که اینترفیس IRouteConstraint را پیاده سازی میکند تعریف کردهایم:
سپس در متد match آن بررسی کردهایم که اگر userid=10 بود، خطای 404 صادر شود.
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
پس از آن، این نام جدید ثبت شدهی در اینجا، به نحو ذیل قابل استفاده است:
به این ترتیب userid باید از نوع int بوده و همچنین قید custom را نیز پوشش دهد (یعنی userid=10 نباشد).
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
در اینجا هر درخواستی که با مسیریابی default تطابق نداشت، توسط الگوی عمومی {url*} پردازش میشود و این پردازش در نهایت سبب راه اندازی برنامهی SPA میگردد. اما مشکل اینجا است که برای فایلهای استاتیک غیرموجود مانند تصاویر، فایلهای js و css نیز خروجی HTML ایی خواهیم داشت؛ بجای خروجی 404 و یافت نشد.
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
پس از بازیابی و نصب آن، اکنون catch all را حذف کرده و با یک سطر routes.MapSpaFallbackRoute ذیل جایگزین کنید:
و برای یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایلهای استاتیک» در AngularJS 2.0، علاوه بر عمومی کردن پوشهی wwwroot توسط UseFileServer نیاز است پوشهی node_modules را هم با تنظیمات ذیل عمومی کرد و در معرض دید عموم قرار داد (جایی که بستههای node.js نصب میشوند):
یکی دیگر از تغییرات عمدهی ASP.NET Core با نگارشهای قبلی آن، نحوهی مدیریت مسیریابیهای سیستم است. در نگارشهای قبلی مبتنی بر HTTP Moduleها، مسیریابیها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شدهاند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل میشود:
اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کردهاست:
میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام میشود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
app.UseMvcWithDefaultRoute();
public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app) { if (app == null) throw new ArgumentNullException("app"); return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"))); }
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
[Route("[controller]/[action]")]
مسیریابیهای قراردادی
در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers { public class HomeController { public string Index() { return "Running a POCO controller!"; } } }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); });
در این تعاریف، هر کدام از قسمتهای قرارگرفتهی داخل {}، مشخص کنندهی قسمتی از URL دریافتی بوده و نامهای controller و action در اینجا جزو نامهای از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار میگیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت میشود. همچنین قسمت آخر این پردازش به ?id ختم شدهاست. وجود ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شدهاند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش میشوند.
در اینجا به هر تعدادی که نیاز است میتوان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آنها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابیهای تعریف شده، در همان سطح سبب خاتمهی پردازش سایر مسیریابیها میشود.
استفاده از Attributes برای تعریف مسیریابیها
بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، میتوان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابیهای قراردادی، از نگارشهای آغازین ASP.NET MVC به همراه آن بودهاند، اما با زیاد شدن تعداد کنترلرها و مسیریابیهای سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمیتوان تشخیص داد که چه مسیریابیهای خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامههای بزرگ و ساده سازی تعاریف مسیریابیهای خاص آنها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شدهاست.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class AboutController : Controller { public ActionResult Hello() { return Content("Hello from DNT!"); } public ActionResult SiteName() { return Content("DNT"); } } }
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آنرا با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، میتوان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("About")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("SiteName")] public ActionResult SiteName() { return Content("DNT"); } } }
AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied: Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest) Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { [Route("[controller]")] public class AboutController : Controller { [Route("")] public ActionResult Hello() { return Content("Hello from DNT!"); } [Route("[action]")] public ActionResult SiteName() { return Content("DNT"); } } }
یک نکته: در حین تعریف مسیریابی یک کنترلر میتوان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
[Route("api/[controller]")]
تعریف قیود، برای مسیریابیهای تعریف شده
فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت میدهد، اضافه کردهایم:
//[Route("/Users/{userid}")] [Route("Users/{userid}")] public IActionResult GetUsers(int userId) { return Json(new { userId = userId }); }
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا میکند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشدهاست)؛ مانند:
[Route("/Users/{userid:int?}")]
[Route("Users/{userid:int}")]
قیودی را که در اینجا میتوان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودیهای اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشتهی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشتهی مجاز ورودی.
• range - ذکر بازهی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.
برای ترکیب چندین قید مختلف نیز میتوان از : استفاده کرد:
[Route("/Users/{userid:int:max(1000):min(10)}")]
ذکر نام Route برای ساده سازی تعریف آدرسی به آن
در حین تعریف یک Route میتوان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شدهی در کلاس آغازین برنامه):
[Route("/Users/{userid:int}", Name="GetUserById")]
string uri = Url.Link("GetUserById", new { userid = 1 });
مشخص سازی ترتیب پردازش مسیریابیها
ترتیب مسیریابیهای ثبت شدهی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آنها است.
در اینجا میتوان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش میشوند (مقدار پیش فرض آن نیز صفر است):
[Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]
امکان تعریف قیود سفارشی
اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، میتوان به صورت ذیل عمل کرد:
using System; using System.Globalization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Core1RtmEmptyTest { public class CustomRouteConstraint : IRouteConstraint { public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) { object value; if (!values.TryGetValue(routeKey, out value) || value == null) { return false; } long longValue; if (value is long) { longValue = (long)value; return longValue != 10; } var valueString = Convert.ToString(value, CultureInfo.InvariantCulture); if (long.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out longValue)) { return longValue != 10; } return false; } } }
public class CustomRouteConstraint : IRouteConstraint
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services) { services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
[Route("/Users/{userid:int:custom}")]
یک نکته: اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شدهاند.
معرفی بستهی نیوگت Microsoft.AspNetCore.SpaServices
مسیریابیهای پیش فرض ASP.NET Core با مسیریابیهای برنامههای SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواستهای رسیدهی به سرور، ابتدا به موتور پردازشی ASP.NET وارد میشوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمیشود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابیهای موجود اضافه کنند (به آن catch all هم میگویند)
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapRoute( name: "spa-fallback", template: "{*url}", defaults: new { controller = "Home", action = "Index" }); });
برای حل این مشکل مایکروسافت بستهای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینهی manage nuget packages را انتخاب کنید. سپس در برگهی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{ "dependencies": { //same as before "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007" },
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); });
// Serve wwwroot as root app.UseFileServer(); // Serve /node_modules as a separate root (for packages that use other npm modules client side) app.UseFileServer(new FileServerOptions { // Set root of file server FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")), // Only react to requests that match this path RequestPath = "/node_modules", // Don't expose file system EnableDirectoryBrowsing = false });
در SQL Server 2014، به Management studio آن ابزارهای جدیدی اضافه شدهاند تا کار تبدیل و مهاجرت جداول معمولی، به جداول بهینه سازی شدهی برای حافظه را سادهتر کنند. برای مثال امکان جدیدی به نام Transaction performance collector جهت بررسی کارآیی تراکنشهای جداول و یا رویههای ذخیره شده در محیط کاری جاری، طراحی شدهاست. پس از آن، این اطلاعات را آنالیز کرده و بر اساس میزان استفاده از آنها، توصیههایی را در مورد مهاجرت یا عدم نیاز به مهاجرت به سیستم جدید OLTP درون حافظهای ارائه میدهد. در ادامه این ابزارهای جدید را بررسی خواهیم کرد.
ابزار Memory Optimization Advisor
Memory Optimization Advisor یک Wizard مانند است که از آن برای گرفتن مشاوره در مورد تبدیل جداول موجود مبتنی بر دیسک سخت، به نمونههای بهینه سازی شده برای حافظه میتوان استفاده کرد. کار آن بررسی ساختار جداولی است که قصد مهاجرت آنها را دارید. برای مثال همانطور که پیشتر نیز عنوان شد، جداول بهینه سازی شده برای حافظه محدودیتهایی دارند؛ مثلا نباید کلید خارجی داشته باشند. این Wizard یک چنین مواردی را آنالیز کرده و گزارشی را ارائه میدهد. پس از اینکه مراحل آنرا به پایان رساندید و مشکلاتی را که گزارش میدهد، برطرف نمودید، کد تبدیل جدول را نیز به صورت خودکار تولید میکند.
برای دسترسی به آن، فقط کافی است بر روی نام جدول خود کلیک راست کرده و گزینهی memory optimization advisor را انتخاب کنید.
در دو قسمت اول این Wizard، کار بررسی ساختار جدول در حال مهاجرت صورت میگیرد. اگر نوع دادهای در آن پشتیبانی نشود یا قیود ویژهای در آن تعریف شده باشند، گزارشی را جهت رفع، دریافت خواهید کرد. پس از رفع آن، به صفحهی گزینههای مهاجرت میرسیم:
همانطور که ملاحظه میکنید، گروه فایل ایجاد شده در قسمت قبل، به صورت خودکار انتخاب شدهاست.
در ادامه میتوان نام دیگری را برای جدول مبتنی بر دیسک وارد کرد. در اینجا به صورت خودکار کلمهی old به آخر نام جدول اضافه شدهاست. در حین تولید جدول جدید بهینه سازی شدهی بر اساس ساختار جدول فعلی، این جدول قدیمی به صورت خودکار تغییر نام خواهد یافت و کلیه اطلاعات آن حفظ میشود.
همچنین تخمینی را نیز از مقدار حافظهی مورد نیاز برای نگهداری این جدول جدید درون حافظهای نیز ارائه میدهد. در این مثال چون رکوردی در جدول انتخابی وجود نداشتهاست، تخمین آن صفر است. عدد ارائه شده توسط آن بسیار مهم است و باید به همین میزان برای سیستم خود حافظه تهیه نمائید و یا از حافظهی موجود استفاده کنید.
در پایین صفحه میتوان انتخاب کرد که آیا دادههای جدول فعلی، به جدول درون حافظهای انتقال یابند یا خیر. به علاوه نوع ماندگاری اطلاعات آن نیز قابل تنظیم است. اگر گزینهی آخر را انتخاب کنید به معنای حالت SCHEMA_ONLY است. حالت پیش فرض آن SCHEMA_AND_DATA میباشد که در قسمتهای قبل بیشتر در مورد آن بحث شد.
در دو صفحهی بعد، کار انتخاب hash index و range index انجام میشود:
در اینجا hash index بر روی فیلد ID تولید شدهاست، به همراه تعیین bucket count آن و در صفحهی بعدی range index بر روی فیلد تاریخ تعریف گردیدهاست:
در آخر میتوان با کلیک بر روی دکمهی Script، صرفا دستورات T-SQL تغییر ساختار جدول را دریافت کرد و یا با کلیک بر روی دکمهی migrate به صورت خودکار کلیه موارد تنظیم شده را اجرا نمود.
خلاصهی این مراحل که توسط دکمهی Script آن تولید میشود، به صورت زیر است:
که در آن ابتدا کار تغییر نام جدول قبلی صورت میگیرد. سپس یک جدول جدید با ویژگی MEMORY_OPTIMIZED = ON را ایجاد میکند. در ساختار این جدول، hash index و range index تعریف شده، قابل مشاهده هستند. در آخر نیز کلیه اطلاعات جدول قدیمی را به جدول جدید منتقل میکند.
علاوه بر memory optimization advisor مخصوص جداول، ابزار دیگری نیز به نام Native compilation advisor برای آنالیز رویههای ذخیره شده تهیه شدهاست:
آیا سیستم فعلی ما واقعا نیازی به ارتقاء به جداول درون حافظهای دارد؟
تا اینجا در مورد نحوهی ایجاد جداول درون حافظهای و یا نحوهی تبدیل جداول موجود را به ساختار جدید بررسی کردیم. ولی آیا واقعا یک چنین تغییراتی برای ما سودمند هستند؟ برای پاسخ دادن به این سؤال ابزاری به نام AMR به management studio 2014 اضافه شدهاست (Analyze, Migrate, Report). کار آن تحت نظر قرار دادن جداول و رویههای ذخیره شدهی بانک اطلاعاتی است و سپس بر اساس بار سیستم، تعداد درخواستهای همزمان و میزان استفاده از جداول و تراکنشهای مرتبط با آنها، گزارشی را ارائه میدهد. بر این اساس بهتر میتوان تصمیم گرفت که کدام جداول بهتر است به جداول درون حافظهای تبدیل شوند.
برای تنظیم آن باید مراحل ذیل طی شوند:
در Management Studio، به برگهی Object Explorer آن مراجعه کنید. سپس پوشهی Management آنرا یافته و بر روی گزینهی Data Collection کلیک راست نمائید:
در اینجا گزینهی Configure Management Data Warehouse را انتخاب نمائید. در صفحهی باز شده، ابتدا بانک اطلاعاتی مدنظر را انتخاب نمائید. همچنین بهتر است بر روی دکمهی new کلیک کرده و یک بانک اطلاعاتی جدید را برای آن ایجاد نمائید، تا دچار تداخل اطلاعاتی و ساختاری نگردد:
در ادامه نام کاربری را که قرار است کار مدیریت ثبت و جمع آوری اطلاعات را انجام دهد، به همراه نقشهای آن انتخاب نمائید:
و در آخر در صفحهی بعدی بر روی دکمهی Finish کلیک کنید.
پس از ایجاد و انتخاب بانک اطلاعاتی Management Data Warehouse، نوبت به تنظیم گزینههای جمع آوری اطلاعات است:
در اینجا ابتدا سرور جاری را انتخاب کنید. پس از آن به صورت خودکار در لیست بانکهای اطلاعاتی قابل انتخاب، تنها همان بانک اطلاعاتی جدیدی را که برای مرحلهی قبل ایجاد کردیم، میتوان مشاهده کرد.
در صفحهی بعد، گزینهی «Transaction Performance Collection Sets» را انتخاب نمائید که دقیقا گزینهی مدنظر ما جهت یافتن آماری از وضعیت تراکنشهای سیستم است.
در ادامه بر روی گزینههای next و finish کلیک کنید تا کار تنظیمات به پایان برسد.
اکنون اگر به لیست وظایف تعریف شده در SQL Server agent مراجعه کنید، میتوانید، وظایف مرتبط با جمع آوری دادهها را نیز مشاهده نمائید:
وظایف Stored Procedure Usage Analysis هر نیم ساعت یکبار و وظایف Table Usage Analysis هر 15 دقیقه یکبار اجرا میشوند. البته امکان اجرای دستی این وظایف نیز مانند سایر وظایف SQL Server وجود دارند.
همچنین در پوشهی management، گزینهی Data collection نیز دو زیر شاخه اضافه شدهاند که نمایانگر آنالیز میزان مصرف جداول و رویههای ذخیره شده میباشند:
پس از این کارها باید مدتی صبر کنید (مثلا یک ساعت) تا سیستم به صورت معمول کارهای متداول خودش را انجام دهد. پس از آن میتوان به گزارشات AMR مراجعه کرد.
برای اینکار بر روی بانک اطلاعاتی Management Data Warehouse که در ابتدای عملیات ایجاد شد، کلیک راست نمائید و سپس مراحل ذیل را طی کنید:
Reports > Management Data Warehouse > Transaction Performance Analysis Overview
در گزارش ایجاد شده، ذیل گزینهی usage analysis لینکهایی وجود دارند که با مراجعه به آنها، چارتهایی از میزان مصرف بانکهای اطلاعاتی مختلف سیستم ارائه میشود. اگر پیام No data available را مشاهده کردید، یعنی هنوز باید مقداری صبر کنید تا کار جمع آوری اطلاعات به پایان برسد.
در این چارتها بانکهای اطلاعاتی که در سمت راست، بالای تصویر قرار میگیرند، انتخاب مناسبی برای تبدیل به بانکهای اطلاعاتی درون حافظهای هستند. محور افقی آن از چپ به راست بیانگر میزان کاهش سختی انتقال یک جدول به جدول درون حافظهای است (با درنظر گرفتن تمام مسایلی که باید تغییر کنند یا نوعهای دادهای که باید اصلاح شوند) و محور عمودی آن نمایانگر میزان بالا رفتن پاسخ دهی سیستم در جهت انجام کار بیشتر است.
هر زمان هم که کار تصمیمگیری شما به پایان رسید، میتوانید بر روی گزینهی Data collection کلیک راست کرده و آنرا غیرفعال نمائید.
برای مطالعه بیشتر
SQL Server 2014 Field Benchmarking In-Memory OLTP and Buffer Pool Extension Features
New AMR Tool: Simplifying the Migration to In-Memory OLTP
A Tour of the Hekaton AMR Tool
SQL Server 2014 Memory Optimization Advisor
Getting started with the AMR tool for migration to SQL Server In-memory OLTP Tables
How to Use Microsoft's AMR Tool
SQL Server 2014's Analysis, Migrate, and Report Tool
ابزار Memory Optimization Advisor
Memory Optimization Advisor یک Wizard مانند است که از آن برای گرفتن مشاوره در مورد تبدیل جداول موجود مبتنی بر دیسک سخت، به نمونههای بهینه سازی شده برای حافظه میتوان استفاده کرد. کار آن بررسی ساختار جداولی است که قصد مهاجرت آنها را دارید. برای مثال همانطور که پیشتر نیز عنوان شد، جداول بهینه سازی شده برای حافظه محدودیتهایی دارند؛ مثلا نباید کلید خارجی داشته باشند. این Wizard یک چنین مواردی را آنالیز کرده و گزارشی را ارائه میدهد. پس از اینکه مراحل آنرا به پایان رساندید و مشکلاتی را که گزارش میدهد، برطرف نمودید، کد تبدیل جدول را نیز به صورت خودکار تولید میکند.
برای دسترسی به آن، فقط کافی است بر روی نام جدول خود کلیک راست کرده و گزینهی memory optimization advisor را انتخاب کنید.
در دو قسمت اول این Wizard، کار بررسی ساختار جدول در حال مهاجرت صورت میگیرد. اگر نوع دادهای در آن پشتیبانی نشود یا قیود ویژهای در آن تعریف شده باشند، گزارشی را جهت رفع، دریافت خواهید کرد. پس از رفع آن، به صفحهی گزینههای مهاجرت میرسیم:
همانطور که ملاحظه میکنید، گروه فایل ایجاد شده در قسمت قبل، به صورت خودکار انتخاب شدهاست.
در ادامه میتوان نام دیگری را برای جدول مبتنی بر دیسک وارد کرد. در اینجا به صورت خودکار کلمهی old به آخر نام جدول اضافه شدهاست. در حین تولید جدول جدید بهینه سازی شدهی بر اساس ساختار جدول فعلی، این جدول قدیمی به صورت خودکار تغییر نام خواهد یافت و کلیه اطلاعات آن حفظ میشود.
همچنین تخمینی را نیز از مقدار حافظهی مورد نیاز برای نگهداری این جدول جدید درون حافظهای نیز ارائه میدهد. در این مثال چون رکوردی در جدول انتخابی وجود نداشتهاست، تخمین آن صفر است. عدد ارائه شده توسط آن بسیار مهم است و باید به همین میزان برای سیستم خود حافظه تهیه نمائید و یا از حافظهی موجود استفاده کنید.
در پایین صفحه میتوان انتخاب کرد که آیا دادههای جدول فعلی، به جدول درون حافظهای انتقال یابند یا خیر. به علاوه نوع ماندگاری اطلاعات آن نیز قابل تنظیم است. اگر گزینهی آخر را انتخاب کنید به معنای حالت SCHEMA_ONLY است. حالت پیش فرض آن SCHEMA_AND_DATA میباشد که در قسمتهای قبل بیشتر در مورد آن بحث شد.
در دو صفحهی بعد، کار انتخاب hash index و range index انجام میشود:
در اینجا hash index بر روی فیلد ID تولید شدهاست، به همراه تعیین bucket count آن و در صفحهی بعدی range index بر روی فیلد تاریخ تعریف گردیدهاست:
در آخر میتوان با کلیک بر روی دکمهی Script، صرفا دستورات T-SQL تغییر ساختار جدول را دریافت کرد و یا با کلیک بر روی دکمهی migrate به صورت خودکار کلیه موارد تنظیم شده را اجرا نمود.
خلاصهی این مراحل که توسط دکمهی Script آن تولید میشود، به صورت زیر است:
USE [testdb2] GO EXEC dbo.sp_rename @objname = N'[dbo].[tblNormal]', @newname = N'tblNormal_old', @objtype = N'OBJECT' GO USE [testdb2] GO SET ANSI_NULLS ON GO CREATE TABLE [dbo].[tblNormal] ( [CustomerID] [int] NOT NULL, [Name] [nvarchar](250) COLLATE Persian_100_CI_AI NOT NULL, [CustomerSince] [datetime] NOT NULL, INDEX [ICustomerSince] NONCLUSTERED ( [CustomerSince] ASC ), CONSTRAINT [tblNormal_primaryKey] PRIMARY KEY NONCLUSTERED HASH ( [CustomerID] )WITH ( BUCKET_COUNT = 131072) )WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_AND_DATA ) GO INSERT INTO [testdb2].[dbo].[tblNormal] ([CustomerID], [Name], [CustomerSince]) SELECT [CustomerID], [Name], [CustomerSince] FROM [testdb2].[dbo].[tblNormal_old] GO
علاوه بر memory optimization advisor مخصوص جداول، ابزار دیگری نیز به نام Native compilation advisor برای آنالیز رویههای ذخیره شده تهیه شدهاست:
آیا سیستم فعلی ما واقعا نیازی به ارتقاء به جداول درون حافظهای دارد؟
تا اینجا در مورد نحوهی ایجاد جداول درون حافظهای و یا نحوهی تبدیل جداول موجود را به ساختار جدید بررسی کردیم. ولی آیا واقعا یک چنین تغییراتی برای ما سودمند هستند؟ برای پاسخ دادن به این سؤال ابزاری به نام AMR به management studio 2014 اضافه شدهاست (Analyze, Migrate, Report). کار آن تحت نظر قرار دادن جداول و رویههای ذخیره شدهی بانک اطلاعاتی است و سپس بر اساس بار سیستم، تعداد درخواستهای همزمان و میزان استفاده از جداول و تراکنشهای مرتبط با آنها، گزارشی را ارائه میدهد. بر این اساس بهتر میتوان تصمیم گرفت که کدام جداول بهتر است به جداول درون حافظهای تبدیل شوند.
برای تنظیم آن باید مراحل ذیل طی شوند:
در Management Studio، به برگهی Object Explorer آن مراجعه کنید. سپس پوشهی Management آنرا یافته و بر روی گزینهی Data Collection کلیک راست نمائید:
در اینجا گزینهی Configure Management Data Warehouse را انتخاب نمائید. در صفحهی باز شده، ابتدا بانک اطلاعاتی مدنظر را انتخاب نمائید. همچنین بهتر است بر روی دکمهی new کلیک کرده و یک بانک اطلاعاتی جدید را برای آن ایجاد نمائید، تا دچار تداخل اطلاعاتی و ساختاری نگردد:
در ادامه نام کاربری را که قرار است کار مدیریت ثبت و جمع آوری اطلاعات را انجام دهد، به همراه نقشهای آن انتخاب نمائید:
و در آخر در صفحهی بعدی بر روی دکمهی Finish کلیک کنید.
پس از ایجاد و انتخاب بانک اطلاعاتی Management Data Warehouse، نوبت به تنظیم گزینههای جمع آوری اطلاعات است:
در اینجا ابتدا سرور جاری را انتخاب کنید. پس از آن به صورت خودکار در لیست بانکهای اطلاعاتی قابل انتخاب، تنها همان بانک اطلاعاتی جدیدی را که برای مرحلهی قبل ایجاد کردیم، میتوان مشاهده کرد.
در صفحهی بعد، گزینهی «Transaction Performance Collection Sets» را انتخاب نمائید که دقیقا گزینهی مدنظر ما جهت یافتن آماری از وضعیت تراکنشهای سیستم است.
در ادامه بر روی گزینههای next و finish کلیک کنید تا کار تنظیمات به پایان برسد.
اکنون اگر به لیست وظایف تعریف شده در SQL Server agent مراجعه کنید، میتوانید، وظایف مرتبط با جمع آوری دادهها را نیز مشاهده نمائید:
وظایف Stored Procedure Usage Analysis هر نیم ساعت یکبار و وظایف Table Usage Analysis هر 15 دقیقه یکبار اجرا میشوند. البته امکان اجرای دستی این وظایف نیز مانند سایر وظایف SQL Server وجود دارند.
همچنین در پوشهی management، گزینهی Data collection نیز دو زیر شاخه اضافه شدهاند که نمایانگر آنالیز میزان مصرف جداول و رویههای ذخیره شده میباشند:
پس از این کارها باید مدتی صبر کنید (مثلا یک ساعت) تا سیستم به صورت معمول کارهای متداول خودش را انجام دهد. پس از آن میتوان به گزارشات AMR مراجعه کرد.
برای اینکار بر روی بانک اطلاعاتی Management Data Warehouse که در ابتدای عملیات ایجاد شد، کلیک راست نمائید و سپس مراحل ذیل را طی کنید:
Reports > Management Data Warehouse > Transaction Performance Analysis Overview
در گزارش ایجاد شده، ذیل گزینهی usage analysis لینکهایی وجود دارند که با مراجعه به آنها، چارتهایی از میزان مصرف بانکهای اطلاعاتی مختلف سیستم ارائه میشود. اگر پیام No data available را مشاهده کردید، یعنی هنوز باید مقداری صبر کنید تا کار جمع آوری اطلاعات به پایان برسد.
در این چارتها بانکهای اطلاعاتی که در سمت راست، بالای تصویر قرار میگیرند، انتخاب مناسبی برای تبدیل به بانکهای اطلاعاتی درون حافظهای هستند. محور افقی آن از چپ به راست بیانگر میزان کاهش سختی انتقال یک جدول به جدول درون حافظهای است (با درنظر گرفتن تمام مسایلی که باید تغییر کنند یا نوعهای دادهای که باید اصلاح شوند) و محور عمودی آن نمایانگر میزان بالا رفتن پاسخ دهی سیستم در جهت انجام کار بیشتر است.
هر زمان هم که کار تصمیمگیری شما به پایان رسید، میتوانید بر روی گزینهی Data collection کلیک راست کرده و آنرا غیرفعال نمائید.
برای مطالعه بیشتر
SQL Server 2014 Field Benchmarking In-Memory OLTP and Buffer Pool Extension Features
New AMR Tool: Simplifying the Migration to In-Memory OLTP
A Tour of the Hekaton AMR Tool
SQL Server 2014 Memory Optimization Advisor
Getting started with the AMR tool for migration to SQL Server In-memory OLTP Tables
How to Use Microsoft's AMR Tool
SQL Server 2014's Analysis, Migrate, and Report Tool
دقیقا این کاری بود که من قبلا انجام داده بودم ولی مشکلی که پیش میآمد این بود که مثلا اگر در یک ارسال بایستی 200 پیامک ارسال گردد قبل از اینکه ارسال این 200 پیامک به اتمام برسد زمان اجرا به پایان رسیده و تابع execute مجددا فراخوانی میشود و از انجایی که هنوز وضعیت این رکورد در دیتابیس به ارسال شده تغییر پیدا نکرده مجددا این 200 پیامک ارسال میگردد. این مشکل حتی زمانی که از حلقه for هم استفاده نمیشود وجود دارد و در تعداد ارسال بالا به مشکل میخورد .
در زیر کدی که برای ارسال استفاده نموده ام را قرار دادم.
با تشکر از شما
در زیر کدی که برای ارسال استفاده نموده ام را قرار دادم.
با تشکر از شما
namespace SchedulerDemo.Jobs { using System; using System.Linq; using System.IO; using Quartz; using System.Collections.Generic; using System.Configuration; [PersistJobDataAfterExecution] [DisallowConcurrentExecution] public class SendJob : IJob { public void Execute(IJobExecutionContext context) { using (var db = new DALModel.DALEntities()) { byte status = (byte)AllEnums.Sms.Status.InProgress; var item = db.SentBoxes.Where(p => p.Status == status && p.IsDeleted==false && p.UserInfo.IsDeleted==false && p.HasTime == true && p.SendInTime == false && p.SendDateX <= DateTime.Now).OrderBy(p=>p.Id).FirstOrDefault(); Cls_SMS.ClsSend sms_Batch = new Cls_SMS.ClsSend(); if (item != null) { decimal smsCount = 0; if (item.UserInfo.CalculateType == Convert.ToByte(AllEnums.FinancialTransaction.CalculationUnit.Message)) { smsCount = Convert.ToDecimal(Function.GetSmsCount(item.Price, item.UserId)); } else { smsCount = Convert.ToDecimal(item.CorrectCount); } decimal adminCredit = Function.GetAdminCreditLink1000(); if (adminCredit != -1 && adminCredit >= smsCount) { if ((item.UserInfo.Credit - (item.UserInfo.LowCredit)) >= item.Price) { item.SendInTime = true; db.SaveChanges(); string numberList = item.NumberList; int position = item.NumberList.LastIndexOf(','); numberList = item.NumberList.Substring(0, position); List<string> receivers_List = new List<string>(); receivers_List = (numberList).Split(',').ToList(); string[] ret2 = new string[2]; string[] DestAdd = new string[receivers_List.Count]; DestAdd = receivers_List.ToArray(); ret2 = sms_Batch.SendSMS_Batch(item.Message, DestAdd, item.UserInfoSenderNumber.AllNumber.Number, ConfigurationManager.AppSettings["SmsUserNameLink1000"], ConfigurationManager.AppSettings["SmsPasswordLink1000"], ConfigurationManager.AppSettings["SmsIPAddressLink1000"], ConfigurationManager.AppSettings["SmsCompanyLink1000"], false, item.Id); var sentBoxUpdate = db.SentBoxes.FirstOrDefault(p => p.Id == item.Id); sentBoxUpdate.Status = Convert.ToByte(AllEnums.Sms.Status.Send); sentBoxUpdate.FinancialTransactionId = db.FinancialTransactions.Where(p => p.UserId == item.UserId).Max(p => p.Id); if (ret2 != null) { sentBoxUpdate.RetValue0 = ret2[0]; sentBoxUpdate.RetValue1 = ret2[1]; } db.SaveChanges(); } else { byte statusFailedForAccount = (byte)AllEnums.Sms.Status.FailedForAccount; item.SendInTime = true; item.Status = statusFailedForAccount; item.FailedCount = item.CorrectCount; item.FailedList = item.NumberList; db.SaveChanges(); } } else { byte statusFaildForError = (byte)AllEnums.Sms.Status.FaildForError; item.SendInTime = true; item.Status = statusFaildForError; item.FailedCount = item.CorrectCount; item.FailedList = item.NumberList; db.SaveChanges(); } } } } } } namespace SchedulerDemo.Interfaces { public interface IScheduleSend { void RunSendSms(); } } namespace SchedulerDemo.Jobs { using System; using Quartz; using Quartz.Impl; using SchedulerDemo.Interfaces; using SchedulerDemo.Jobs; public class SendSchedule : IScheduleSend { public void RunSendSms() { DateTimeOffset startTime = DateBuilder.FutureDate(2, IntervalUnit.Second); IJobDetail job = JobBuilder.Create<SendJob>() .WithIdentity("jobSendSmsInTime") .Build(); ITrigger trigger = TriggerBuilder.Create() .WithIdentity("triggerSendSmsInTime") .StartAt(startTime) .WithSimpleSchedule(x => x.WithIntervalInMinutes(5).RepeatForever()) .Build(); ISchedulerFactory sf = new StdSchedulerFactory(); IScheduler sc = sf.GetScheduler(); sc.ScheduleJob(job, trigger); sc.Start(); } } }
از زمان ASP.NET Core 2.1، قابلیت جدیدی به نام Generic Host، به آن اضافه شدهاست که از آن میتوان برای انجام کارهای متداول پس زمینه، مانند ارسال ایمیلهای خبرنامهی یک برنامه، تهیه فایلهای پشتیبان و غیره استفاده کرد.
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
بنابراین برای ایجاد یک HostedService، نیاز است سرویس کارهای پسزمینهی ما، اینترفیس IHostedService را پیاده سازی کند. متد StartAsync آن جائیاست که تنها یکبار پس از آغاز برنامه اجرا میشود و هدف آن اجرای کار پسزمینهی مدنظر است. متد StopAsync نیز دقیقا پیش از خاتمهی برنامه فراخوانی خواهد شد تا اگر نیاز به پاکسازی منابعی وجود داشته باشد، بتوان از این فرصت استفاده کرد. به این ترتیب اگر نیاز به اجرای متناوب کار پسزمینهای وجود دارد، پیاده سازی آن به خود ما واگذار شدهاست.
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
در ادامه برای معرفی این کار پسزمینه به سیستم به صورت یک سرویس با طول عمر Singleton خواهیم داشت:
روش دیگر انجام اینکار استفاده از متد الحاقی AddHostedService است:
مزیت اینکار این است که متد Configure واقع در کلاس Startup یک چنین امضایی را دارد:
و IHostingEnvironment هم در فضای نام Microsoft.AspNetCore.Hosting واقع شدهاست و هم در فضای نام Microsoft.Extensions.Hosting که IHostedService در آن قرار دارد. به همین جهت چون متد AddHostedService، تعریف IHostedService را مخفی میکند، خطای زمان کامپایلی را جهت مشخص سازی صریح فضای نام IHostingEnvironment دریافت نخواهید کرد:
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
پس از اجرای دستور dotnet run، سرویس پس زمینه شروع به کار کردهاست. پس از مدتی کلیدهای Ctrl+C را فشردهایم تا این حلقهی بینهایت و برنامه خاتمه یابد. اینجا است که مشاهده میکنید تازه قسمت هاست برنامهی وب ما شروع به کار کردهاست؛ یعنی دقیقا زمانیکه پروسهی برنامه در حال خاتمه یافتن است. چرا اینگونه رفتار کردهاست؟
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
برای استفادهی از آن تنها کافی است متد ExecuteAsync آنرا پیاده سازی کنیم. به این ترتیب اینبار پیاده سازی SampleHostedService به صورت زیر تغییر میکند:
اینبار اگر این کار پسزمینه را به سیستم معرفی:
و سپس برنامه را اجرا کنیم:
مشاهده میکنیم که ابتدا هاست وب برنامه شروع به کار کردهاست و سپس سرویس انجام کارهای پسزمینه در حال اجرا است و به این ترتیب اجرای این سرویس پسزمینه، تداخلی را در کار برنامهی وب ایجاد نکردهاست. بنابراین از این پس بجای استفادهی از IHostedService خام، از نمونهی بهبود یافتهی BackgroundService آن استفاده کنید.
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
از این پس برای تعریف کارهای پسزمینهای که نیاز به تزریق سرویسهایی با طول عمر Scoped یا Transient دارند، میتوان کلاس سرویس وظیفه را از ScopedBackgroundService مشتق کرد و سپس متد ExecuteInScope آنرا پیاده سازی نمود. serviceProvider ای که در اینجا در اختیار مصرف کننده قرار میگیرد، داخل Scope قرار دارد و توسط آن میتوان سرویسهای مدنظر را توسط متدهایی مانند serviceProvider.GetRequiredService، دریافت کرد.
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
عبارات Cron، روش بسیار متداولی برای تعریف و انجام کارهای زمانبندی شده در سیستمهای لینوکسی هستند. برای مثال عبارت * * * 0 1 سبب اجرای یک وظیفه، هر روز یک دقیقه پس از نیمهشب، میشود و فرمت کلی 5 قسمتی آن، به صورت زیر است:
و یا عبارت 6 قسمتی آن چنین مفهومی را دارد:
اگر ScopedBackgroundService فوق را با CrontabSchedule یاد شده ترکیب کنیم، میتوانیم به یک کلاس abstract دیگر برسیم که طراحی کلاس پایهی اجرای کارهای زمانبندی شده را ارائه میدهد:
این کلاس پایه، توسط متد CrontabSchedule.Parse، مقدار رشتهای Schedule را با فرمت Cron (فرمت 6 قسمتی که دارای ثانیه هم هست) دریافت و پردازش میکند. سپس متد GetNextOccurrence، زمان بعدی اجرای این وظیفه را مشخص میکند.
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
در اینجا ابتدا کار با پیاده سازی کلاس پایه ScheduledScopedBackgroundService شروع میشود. سپس باید مقدار Schedule را با فرمت 6 قسمتی مشخص کرد. برای مثال در سرویس فوق، این تنظیم سبب اجرای هر 10 ثانیه یکبار این وظیفه میگردد. در آخر، خود وظیفه داخل متد ScheduledExecuteInScope تعریف خواهد شد که serviceProvider دریافتی آن، داخل یک Scope قرار دارد.
روش معرفی آن به سیستم نیز مانند قبل است:
در این حالت اگر برنامه را اجرا کنید، یک چنین خروجی را که بیانگر اجرای هر 10 ثانیه یکبار وظیفهی تعریف شدهاست، مشاهده میکنید:
Generic Host چیست؟
Generic Host یکی از ویژگیهای جدید ASP.NET Core 2.1 است. هدف آن جداسازی HTTP pipeline برنامه، از Web Host API آن است. یکی از مزایای اینکار، امکان استفادهی از آن نه فقط در پروژههای وب، بلکه در پروژههای کنسول نیز میباشد. به این ترتیب میتوان کارهای غیر HTTP را از برنامهی وب مجزا کرد تا به کارآیی بیشتری رسید و برای این منظور اینترفیس IHostedService را که در فضای نام Microsoft.Extensions.Hosting قرار دارد، برای ثبت کارهای پسزمینهی خارج از اعمال web host جاری، ارائه دادهاند:
namespace Microsoft.Extensions.Hosting { public interface IHostedService { Task StartAsync(CancellationToken cancellationToken); Task StopAsync(CancellationToken cancellationToken); } }
یک مثال: معرفی کار پسزمینهای که هر دو ثانیه یکبار انجام میشود
در SampleHostedService زیر، عبارت Hosted service executing به همراه زمان جاری، هر دو ثانیه یکبار لاگ میشود و اگر برنامه را توسط دستور dotnet run اجرا کنید، میتوانید خروجی آنرا در کنسول، مشاهده کنید:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MvcTest { public class SampleHostedService : IHostedService { private readonly ILogger<SampleHostedService> _logger; public SampleHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { _logger.LogInformation("Starting Hosted service"); while (!cancellationToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); } } public Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("Stopping Hosted service"); return Task.CompletedTask; } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton<IHostedService, SampleHostedService>();
services.AddHostedService<SampleHostedService>();
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
Startup.cs(82,56): error CS0104: 'IHostingEnvironment' is an ambiguous reference between 'Microsoft.AspNetCore.Hosting.IHostingEnvironment' and 'Microsoft.Extensions.Hosting.IHostingEnvironment'
مشکلات پیاده سازی کار پسزمینهی SampleHostedService فوق
هر چند اگر مثال فوق را اجرا کنید، خروجی مناسبی را دریافت خواهید کرد، اما دارای این اشکال مهم نیز هست:
D:\MvcTest>dotnet run info: MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:10 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:12 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 14:45:14 Ctrl+C Application is shutting down... Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down.
از دیدگاه ASP.NET Core، یک کار پس زمینه زمانی خاتمه یافته محسوب میشود که متد StartAsync، مقدار Task.CompletedTask را بازگرداند؛ در غیراینصورت، در حال اجرا درنظر گرفته میشود و چون در پیاده سازی فوق این نکته رعایت نشدهاست، این Task همواره در حال اجرا و خاتمه نیافته محسوب میشود و نوبت به مابقی کارها نخواهد رسید. همچنین در قسمت StopAsync نیز بهتر است یک فیلد CancellationTokenSource تعریف شدهی در سطح کلاس را مورد استفاده قرار داد و متد Cancel آنرا فراخوانی کرد تا اطلاع رسانی صحیحی را به متد StartAsync در مورد خاتمهی برنامه، انجام دهد.
برای این منظور و جهت ساده سازی و پیاده سازی تمام این نکات، از اینترفیس خام IHostedService، یک کلاس abstract به نام BackgroundService نیز در فضای نام Microsoft.Extensions.Hosting پیش بینی شدهاست:
namespace Microsoft.Extensions.Hosting { public abstract class BackgroundService : IHostedService, IDisposable { protected BackgroundService(); public virtual void Dispose(); public virtual Task StartAsync(CancellationToken cancellationToken); public virtual Task StopAsync(CancellationToken cancellationToken); protected abstract Task ExecuteAsync(CancellationToken stoppingToken); } }
namespace MvcTest { public class PrinterHostedService : BackgroundService { private readonly ILogger<SampleHostedService> _logger; public PrinterHostedService(ILogger<SampleHostedService> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting Hosted service"); while (!stoppingToken.IsCancellationRequested) { _logger.LogInformation("Hosted service executing - {0}", DateTime.Now); await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } } } }
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<PrinterHostedService>();
D:\MvcTest>dotnet run Hosting environment: Development infoContent root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. : MvcTest.SampleHostedService[0] Starting Hosted service info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:23 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:25 info: MvcTest.SampleHostedService[0] Hosted service executing - 02/19/2019 15:00:27 Application is shutting down... ^C
یک نکته: تزریق وابستگی DbContext برنامه در یک سرویس کار پسزمینه
IHostedServiceها با طول عمر singleton به سیستم تزریق وابستگیها معرفی میشوند. در این حالت اگر سرویسهایی با طول عمر transient و یا scoped را به آنها تزریق کنید، دیگر طول عمر مدنظر شما را نداشته و آنها هم به صورت singleton عمل خواهند کرد. هر چند خود سیستم تزریق وابستگیهای NET Core. با صدور استثنائی، از این مساله جلوگیری میکند (در این مورد در مطالب «مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت چهارم - پرهیز از الگوی Service Locator در برنامههای وب» و همچنین «قسمت سوم - رهاسازی منابع سرویسهای IDisposable» بیشتر بحث شدهاست). یک چنین مواردی را به صورت زیر با تزریق IServiceScopeFactory و ساخت صریح یک Scope میتوان مدیریت کرد:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; public abstract class ScopedBackgroundService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; public ScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { using (var scope = _serviceScopeFactory.CreateScope()) { await ExecuteInScope(scope.ServiceProvider, stoppingToken); } } public abstract Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
طراحی سرویس کارهای پسزمینهی زمانبندی شده
ASP.NET Core، متد ExecuteAsync را یکبار بیشتر اجرا نمیکند. بنابراین پیاده سازی تایمری که بخواهد برای مثال ارسال ایمیلهای خبرنامهی سایت را هر روز ساعت 11 شب انجام دهد، به خود ما واگذار شدهاست. برای پیاده سازی بهتر این تایمر میتوان از کتابخانهی NCrontab که توسط نویسندهی کتابخانهی معروف ELMAH تهیه شدهاست، استفاده کرد که با برنامههای NET Core. نیز سازگاری دارد:
dotnet add package ncrontab
┌───────────── minute (0 - 59) │ ┌───────────── hour (0 - 23) │ │ ┌───────────── day of month (1 - 31) │ │ │ ┌───────────── month (1 - 12) │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; │ │ │ │ │ 7 is also Sunday on some systems) │ │ │ │ │ │ │ │ │ │ * * * * *
* * * * * * - - - - - - | | | | | | | | | | | +--- day of week (0 - 6) (Sunday=0) | | | | +----- month (1 - 12) | | | +------- day of month (1 - 31) | | +--------- hour (0 - 23) | +----------- min (0 - 59) +------------- sec (0 - 59)
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using NCrontab; using static NCrontab.CrontabSchedule; public abstract class ScheduledScopedBackgroundService : ScopedBackgroundService { private CrontabSchedule _schedule; private DateTime _nextRun; protected abstract string Schedule { get; } public ScheduledScopedBackgroundService(IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory) { _schedule = CrontabSchedule.Parse(Schedule, new ParseOptions { IncludingSeconds = true }); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } public override async Task ExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { do { var now = DateTime.Now; if (now > _nextRun) { await ScheduledExecuteInScope(serviceProvider, stoppingToken); _nextRun = _schedule.GetNextOccurrence(DateTime.Now); } await Task.Delay(1000, stoppingToken); //1 second delay } while (!stoppingToken.IsCancellationRequested); } public abstract Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken); }
روش استفادهی از آن برای تعریف یک وظیفهی جدید نیز به صورت زیر است:
using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; public class MyScheduledTask : ScheduledScopedBackgroundService { private readonly ILogger<MyScheduledTask> _logger; public MyScheduledTask( IServiceScopeFactory serviceScopeFactory, ILogger<MyScheduledTask> logger) : base(serviceScopeFactory) { _logger = logger; } protected override string Schedule => "*/10 * * * * *"; //Runs every 10 seconds public override Task ScheduledExecuteInScope(IServiceProvider serviceProvider, CancellationToken stoppingToken) { _logger.LogInformation("MyScheduledTask executing - {0}", DateTime.Now); return Task.CompletedTask; } }
روش معرفی آن به سیستم نیز مانند قبل است:
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddHostedService<MyScheduledTask>();
D:\MvcTest>dotnet run Hosting environment: Development Content root path: D:\MvcTest Now listening on: https://localhost:5001 Now listening on: http://localhost:5000 Application started. Press Ctrl+C to shut down. info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:18:50 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:00 info: MyScheduledTask[0] MyScheduledTask executing - 02/19/2019 19:19:10 Application is shutting down... ^C