امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستمهای اطلاعاتی میباشد؛ یکی از چالشهای اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا میباشد. از آنجاییکه بیشتر راهکارهای موجود از جمله ClosedXml یا Epplus کل ساختار را ابتدا تولید کرده و اصطلاحا خروجی مورد نظر را بافر میکنند، برای حجم بالای اطلاعات مناسب نخواهند بود. راهکار برای خروجی CSV به عنوان مثال خیلی سرراست میباشد و میتوان با چند خط کد، به نتیجه دلخواه از طریق مکانیزم Streaming رسید؛ ولی ساختار Excel به سادگی فرمت CSV نیست و برای مثال فرمت Excel Workbook با پسوند xlsx یک بسته Zip شدهای از فایلهای XML میباشد.
معرفی MiniExcel
MiniExcel یک کتابخانه سورس باز با هدف به حداقل رساندن مصرف حافظه در زمان پردازش فایلهای Excel در دات نت میباشد. در مقایسه با Aspose از منظر امکانات شاید حرفی برای گفتن نداشته باشد، ولی از جهت خواندن اطلاعات فایلهای Excel با قابلیت پشتیبانی از LINQ و Deferred Execution در کنار مصرف کم حافظه و جلوگیری از مشکل OOM خیلی خوب عمل میکند. در تصویر زیر مشخص است که برای عمده عملیات پیادهسازی شده، از استریمها بهره برده شده است.
همچنین در زیر مقایسهای روی خروجی ۱ میلیون رکورد با تعداد ۱۰ ستون در هر ردیف انجام شدهاست که قابل توجه میباشد:
Logic : create a total of 10,000,000 "HelloWorld" excel
LibraryMethodMax Memory UsageMean | |||
MiniExcel | 'MiniExcel Create Xlsx' | 15 MB | 11.53181 sec |
Epplus | 'Epplus Create Xlsx' | 1,204 MB | 22.50971 sec |
OpenXmlSdk | 'OpenXmlSdk Create Xlsx' | 2,621 MB | 42.47399 sec |
ClosedXml | 'ClosedXml Create Xlsx' | 7,141 MB | 140.93992 sec |
به شدت API خوش دستی برای استفاده دارد و شاید مطالعه سورس کد آن از جهت طراحی نیز درس آموزی داشته باشد. در ادامه چند مثال از مستندات آن را میتوانید ملاحظه کنید:
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.xlsx"); MiniExcel.SaveAs(path, new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} });
// DataReader export multiple sheets (recommand by Dapper ExecuteReader) using (var cnn = Connection) { cnn.Open(); var sheets = new Dictionary<string,object>(); sheets.Add("sheet1", cnn.ExecuteReader("select 1 id")); sheets.Add("sheet2", cnn.ExecuteReader("select 2 id")); MiniExcel.SaveAs("Demo.xlsx", sheets); }
طراحی یک ActionResult سفارشی برای استفاده از MiniExcel
برای این منظور نیاز است تا Stream مربوط به Response درخواست جاری را در اختیار این کتابخانه قرار دهیم و از سمت دیگر دیتای مورد نیاز را به نحوی که بافر نشود و از طریق مکانیزم Streaming در EF (استفاده از Deferred Execution و Enumerableها) مهیا کنیم. برای امکان تعویض پذیری (این سناریو در پروژه واقعی و باتوجه به جهت وابستگیها میتواند ضروری باشد) از دو واسط زیر استفاده خواهیم کرد:
public interface IExcelDocumentFactory { ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> headers, Stream stream); } public interface ILargeExcelDocument : IAsyncDisposable, IDisposable { Task Write<T>( PaginatedEnumerable<T> items, int count, int sizeLimit, CancellationToken cancellationToken = default) where T : notnull; }
متد CreateLargeDocument یک وهله از ILargeExcelDocument را در اختیار مصرف کننده قرار میدهد که قابلیت نوشتن روی آن از طریق متد Write را خواهد داشت. روش واکشی دیتا از طریق Delegate تعریف شده با نام PaginatedEnumerable به مصرف کننده محول شدهاست که در ادامه امضای آن را میتوانید مشاهده کنید:
public delegate IEnumerable<T> PaginatedEnumerable<out T>(int page, int pageSize);
در ادامه پیادهسازی واسط ILargeExcelDocument برای MiniExcel به شکل زیر خواهد بود:
internal sealed class MiniExcelDocument(Stream stream, IEnumerable<ExcelColumn> columns) : ILargeExcelDocument { private const int SheetLimit = 1_048_576; private bool _disposedValue; public async Task Write<T>( PaginatedEnumerable<T> items, int count, int sizeLimit, CancellationToken cancellationToken = default) where T : notnull { ThrowIfDisposed(); // TODO: apply sizeLimit var properties = FastReflection.Instance.GetProperties(typeof(T)) .ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase); var sheets = new Dictionary<string, object>(); var index = 1; while (count > 0) { cancellationToken.ThrowIfCancellationRequested(); IEnumerable<Dictionary<string, object>> reader = items(index, SheetLimit) .Select(item => { cancellationToken.ThrowIfCancellationRequested(); return columns.ToDictionary(h => h.Title, h => ValueOf(item, h.Name, properties)); }); sheets.Add($"sheet_{index}", reader); count -= SheetLimit; index++; } // This part is forward-only, and we are pretty sure that streaming will happen without buffering. await stream.SaveAsAsync(sheets, cancellationToken: cancellationToken); } private void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { // TODO: dispose managed state (managed objects) } // TODO: free unmanaged resources (unmanaged objects) and override finalizer // TODO: set large fields to null _disposedValue = true; } } ~MiniExcelDocument() { Dispose(disposing: false); } public void Dispose() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method Dispose(disposing: true); GC.SuppressFinalize(this); } public async ValueTask DisposeAsync() { Dispose(); await ValueTask.CompletedTask; } private void ThrowIfDisposed() { if (!_disposedValue) return; throw new ObjectDisposedException(nameof(MiniExcelDocument)); } private static object ValueOf<T>(T record, string prop, IDictionary<string, FastPropertyInfo> properties) where T : notnull { var property = properties[prop] ?? throw new InvalidOperationException($"There is no property with given name [{prop}]"); return NormalizeValue(property.GetValue?.Invoke(record)); } private static object NormalizeValue(object? value) { if (value == null) return null!; return value switch { DateTime dateTime => dateTime.ToShortPersianDateTimeString(), TimeSpan time => time.ToString(@"hh\:mm\:ss"), DateOnly dateTime => dateTime.ToShortPersianDateString(false), TimeOnly time => time.ToString(@"hh\:mm\:ss"), bool boolean => boolean ? "بلی" : "خیر", IEnumerable<object> values => string.Join(',', values.Select(NormalizeValue).ToList()), Enum enumField => enumField.GetEnumStringValue(), _ => value }; } }
در بدنه متد Write باتوجه به تعداد کل رکوردها، یک کوئری برای هر شیت از طریق فراخوانی متد منتسب به پارامتر items اجرا خواهد شد؛ توجه کنید که اجرای این کوئری مشخصا به تعویق افتاده و تا زمان اولین MoveNext، اجرایی صورت نخواهد گرفت (مفهوم Deferred Execution). به این ترتیب باقی کارها از جمله فرمت کردن مقادیر در سمت برنامه و از طریق Linq To Object انجام خواهد شد. همچنین پیادهسازی Factory مرتبط با آن به شکل زیر خواهد بود:
internal sealed class ExcelDocumentFactory : IExcelDocumentFactory { public ILargeExcelDocument CreateLargeDocument(IEnumerable<ExcelColumn> columns, Stream stream) { return new MiniExcelDocument(stream, columns); } }
در ادامه ActionResult سفارشی برای گرفتن خروجی اکسل را به شکل زیر می توان پیادهسازی کرد:
public class ExcelExportResult<T>(PaginatedEnumerable<T> items, int count, ExportMetadata metadata) : ActionResult where T : notnull { private const string ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; private const string Extension = ".xlsx"; private const int SizeLimit = int.MaxValue; private readonly IReadOnlyList<FastPropertyInfo> _properties = FastReflection.Instance.GetProperties(typeof(T)); public override async Task ExecuteResultAsync(ActionContext context) { var sp = context.HttpContext.RequestServices; var factory = sp.GetRequiredService<IExcelDocumentFactory>(); var disposition = new ContentDispositionHeaderValue(DispositionTypeNames.Attachment); disposition.SetHttpFileName(MakeFilename()); context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = disposition.ToString(); context.HttpContext.Response.Headers.Append(HeaderNames.ContentType, ContentType); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; //TODO: deal with exception, because our global exception handling cannot take into account while the response is started. await using var bodyStream = context.HttpContext.Response.BodyWriter.AsStream(); await context.HttpContext.Response.StartAsync(context.HttpContext.RequestAborted); await using (var document = factory.CreateLargeDocument(MakeColumns(), bodyStream)) { await document.Write(items, count, SizeLimit, context.HttpContext.RequestAborted); } await context.HttpContext.Response.CompleteAsync(); } private string MakeFilename() { return $"{metadata.Title} - {DateTime.UtcNow.ToEpochSeconds()}{Extension}"; } private IEnumerable<ExcelColumn> MakeColumns() { var types = _properties.ToDictionary(p => p.Name, p => p.PropertyType, StringComparer.OrdinalIgnoreCase); return metadata.Fields.Select(f => { var type = types[f.Name]; type = Nullable.GetUnderlyingType(type) ?? type; if (type.IsEnum || type == typeof(DateOnly) || type == typeof(TimeOnly) || type == typeof(bool) || type == typeof(TimeSpan) || type == typeof(DateTime)) { type = typeof(string); } return new ExcelColumn(f.Name, f.Title, type); }); } }
در اینجا از طریق ExportMetadata که از سمت کاربر تعیین میشود، مشخص خواهد شد که کدام فیلدها در فایل نهایی حضور داشته باشند. در بدنه متد ExecuteResultAsync یکسری هدر مرتبط با کار با فایلها تنظیم شدهاست و سپس از طریق BodyWriter و متد AsStream به استریم مورد نظر دست یافته و در اختیار متد Write مربوط به document ایجاد شده، قرار دادهایم. یک نمونه استفاده از آن برای موجودیت فرضی مشتری می تواند به شکل زیر باشد:
[ApiController, Route("api/customers")] public class CustomersController(IDbContext dbContext) : ControllerBase { [HttpGet("export")] public async Task<ActionResult> ExportCustomers([FromQuery] ExportMetadata metadata, CancellationToken cancellationToken) { var count = await dbContext.Set<Customer>().CountAsync(cancellationToken); return this.Export( (page, pageSize) => dbContext.Set<Customer>() .OrderBy(c => c.Id) .Skip((page - 1) * pageSize) .Take(pageSize) .AsNoTracking() .AsEnumerable(), // Enable streaming instead of buffering through deferred execution count, metadata); } }
در اینجا از طریق Extension Method مهیا شده روش کوئری کردن برای هر شیت را مشخص کردهایم؛ نکته مهم در ایجاد استفاده از متد AsEnumerable می باشد که در عمل یک Type Casting انجام می دهد که باقی متدهای استفاده شده روی خروجی، از طریق Linq To Object اعمال شود و همچنین نیاز به استفاده از ToList و یا موارد مشابه را نخواهیم داشت. نمونه درخواست GET برای این API می تواند به شکل زیر باشد:
http://localhost:5118/api/customers/export?Title=Test&Fields[0].Name=FirstName&Fields[0].Title=First name&Fields[1].Name=LastName&Fields[1].Title=Last name&Fields[2].Name=BirthDate&Fields[2].Title=BirthDate
سورس کد مثال قابل اجرا از طریق مخزن زیر قابل دسترس می باشد:
https://github.com/rabbal/large-excel-streaming
در این مثال در زمان آغاز برنامه، ۱۰ میلیون رکورد در جدول Customer ثبت خواهد شد که در ادامه می توان از آن خروجی Excel تهیه کرد.
نکته مهم: توجه داشته باشید که استفاده از این روش قابلیت از سرگیری مجدد برای دانلود را نخواهد داشت و شاید بهتر است این فرآیند را از طریق یک Job انجام داده و با استفاده از قابلیتهای Multipart Upload مربوط به یک BlobStroage مانند Minio، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.
مسیر راه Bootstrap
Starting today, Bootstrap 3 will move to end of life, and will no longer receive critical security updates.
Bootstrap 4 moved to Long Term Support release on February 14, 2019 and will no longer receive new features. It will continue to receive bug fixes, security updates, and documentation updates.
Bootstrap 5 is under active development. You can follow our progress on GitHub.
در چه برنامههایی استفاده از بانکهای اطلاعاتی NoSQL مناسبتر است؟
1) برنامههای مدیریت محتوا
2) کاتالوگهای محصولات (هر برنامهای با تعدادی شیء و خصوصا متادیتای متغیر)
3) شبکههای اجتماعی
4) Big Data
5) سایر
1) برنامههای مدیریت محتوا
بانکهای اطلاعاتی NoSQL سندگرا، جهت تهیه برنامههای مدیریت محتوا، بسیار مناسب هستند. در این نوع برنامهها، یک سری محتوا که دارای متادیتایی هستند، ذخیره خواهند شد. این متادیتاها مانند نوع، گروه و هر نوع خاصیت دیگری، میتواند باشند. برای ذخیره سازی این نوع اطلاعات، جفتهای key-value بسیار خوب عمل میکنند. همچنین در بانکهای اطلاعاتی سندگرای NoSQL، با استفاده از مفهوم برچسبها، امکان الصاق فایلهای متناظری به اسناد پیش بینی شدهاست. همانطور که در قسمت قبل نیز ذکر شد، در Document stores، نگارشهای قدیمی اسناد نیز حفظ میشوند. به این ترتیب، این خاصیت و توانمندی توکار، امکان دسترسی به نگارشهای مختلف یک محتوای خاص را به سادگی میسر میسازد. به علاوه اکثر Document stores امکان دسترسی به این مستندات را به کمک URLها و REST API، به صورت خودکار فراهم میسازند.
برای نمونه به CouchDB، عنوان Web database نیز داده شده است؛ از این جهت که یک برنامه وب را میتوان داخل بانک اطلاعاتی آن قرار داد. در اینجا منظور از برنامه وب، یک وب سایت قابل دسترسی از طریق URLها است و نه برنامههای سازمانی وب. برای نمونه ساختاری شبیه به برنامه معروف EverNote را میتوان داخل این نوع بانکهای اطلاعاتی به سادگی ایجاد کرد (خود بانک اطلاعاتی تشکیل شده است از یک وب سرور که REST API را پشتیبانی کرده و امکان دسترسی به اسناد را بدون نیاز به کدنویسی اضافهتری، از طریق URLها و HTTP Verbs استاندارد مهیا میکند).
2) کاتالوگهای محصولات
محصولات در یک کاتالوگ، ویژگیهای مشابه یکسان فراوانی دارند؛ اما تعدادی از این محصولات، دارای ویژگیهایی خاص و منحصربفردی نیز میباشند.
مثلا یک شیء محصول را درنظر بگیرید که دارای خواص مشترک و یکسان شماره، نام، توضیحات و قیمت است. اما بعضی از محصولات، بسته به ردهی خاصی که دارند، دارای ویژگیهای خاصی مانند قدرت تفکیک، رنگ، سرعت و غیره نیز هستند که از هر گروه، به گروه دیگری متغیر است.
برای مدیریت یک چنین نیازی، هر دو گروه key-value stores و wide column stores بانکهای اطلاعاتی NoSQL مناسب هستند؛ از این جهت که در یک key-value store نیازی به تعریف هیچ نوع ساختار خاصی، در ابتدای کار نیست و این ساختار میتواند از هر رکورد، به رکورد دیگری متفاوت باشد.
یا برای نمونه، یک برنامه فرم ساز را درنظر بگیرید که هر فرم آن، هر چند دارای یک سری خواص ثابت مانند نام، گروه و امثال آن است، اما هر کدام دارای فیلدهای تشکیل دهنده متفاوتی نیز میباشد. به این ترتیب با استفاده از key-value stores، دیگری نیازی به نگران بودن در مورد نحوه مدیریت اسکیمای متغیر مورد نیاز، نخواهد بود.
3) شبکههای اجتماعی
همانطور که در قسمت قبل نیز بحث شد، نوع خاص Graph databases برای کاربردهای برنامههای شبکههای اجتماعی و ردیابی تغییرات آنها بسیار مفید و کارآ هستند. برای مثال در یک شبکه افراد دارای تعدادی دنبال کننده هستند؛ عضو گروههای مختلف میباشند، در قسمتهای مختلفی نظر و مطلب ارسال میکنند. در اینجا، اشیاء نسبت به یکدیگر روابط مختلفی دارند. با استفاده از Graph databases، تشکیل روابط self-joins و تو در تو و بسیاری از روشهای خاص، مانند روابط many-to-many که در بانکهای اطلاعاتی رابطهای با تمهیدات ویژهای قابل تشکیل هستند، با سهولت بهتری مدیریت خواهند شد.
4) Big Data
الگوریتم MapReduce، برای کار با حجم دادههای عظیم، طراحی شده است و در این بین، بانکهای اطلاعاتی Wide column store (که در قسمت قبل بررسی شدند) و یا حتی Key-value store (مانند Amazon DynamoDB) بیشتر کاربرد دارند. در سناریوهای دادههای عظیم، واژههای Hadoop و Hbase دنیای NoSQL را زیاد خواهید شنید. Hadoop نسخه سورس باز MapReduce گوگل است و Hbase نیز نسخه سورس باز BigTable گوگل میباشد. مفاهیم پایهای Sharding و فایل سیستمهای Append-only (با سرعت بالای نوشتن) نیز به مدیریت BigData کمک میکنند.
در اینجا بحث مهم، خواندن اطلاعات و آنالیز آنها است و نه تهیه برنامههای معروف CRUD. بسیاری از اعمال آماری و ریاضی مورد نیاز بر روی دادههای عظیم، نیازی به اسکیمای از پیش مشخص شده بانکهای اطلاعاتی رابطهای را ندارند و یا در اینجا قابلیتهای نوشتن کوئریهای پیچیده نیز آنچنان مهم نیستند.
5) سایر کاربردها
- هر سیستمی که اطلاعات Log مانند را تولید میکند. منظور از Log، اطلاعاتی است که در حین رخداد خاصی تولید میشوند. عموما مرسوم است که این نوع اطلاعات را در فایلها، بجای بانک اطلاعاتی ذخیره کرد. بنابراین مدیریت این نوع فایلها توسط بانکهای اطلاعاتی NoSQL، قابلیت انجام امور آماری را بر روی آنها سادهتر خواهد ساخت.
- مدیریت اطلاعات برنامههایی مانند سیستمهای EMail.
و در چه برنامههایی استفاده از بانکهای اطلاعاتی رابطهای مناسبتر است؟
اگر تا اینجا به مزایای استفاده از بانکهای اطلاعاتی NoSQL اشاره شد، بدین معنا نیست که بانکهای اطلاعاتی رابطهای، منسوخ شدهاند یا دیگر قدر و قیمتی ندارند. واقعیت این است که هنوز بازه وسیعی از کاربردها را میتوان به کمک بانکهای اطلاعاتی رابطهای بهتر از بانکهای اطلاعاتی NoSQL مدیریت کرد. این کاربردها و مزیتها در 5 گروه عمده خلاصه میشوند:
1) نیاز به تراکنشها
2) اسکیمای پیش فرض
3) برنامههای LOB یا Line of business applications
4) زبانهای کوئری نویسی پیشرفته
5) نیاز به امکانات گزارشگیری پیشرفته
1) نیاز به تراکنشها
در سیستمهای تجاری عمومی، نیاز به پیاده سازی مفهوم ACID که در قسمتهای قبل به آن پرداخته شد، مانند Atomic transactions وجود دارد. Atomic transaction به زبان ساده به این معنا است که سیستم قادر است چندین دستور را در قالب یک گروه و در طی یک مرحله، به بانک اطلاعاتی اعمال کند و اگر یکی از این دستورات گروه در حال اعمال، با شکست مواجه شد، باید کل تراکنش برگشت خورده و امنیت کار تضمین گردد. در غیراینصورت با یک سیستم غیر هماهنگ مواجه خواهیم شد.
و همانطور که پیشتر نیز عنوان شد، سیستمهای NoSQL، مبنای کار را بر اساس «عاقبت یک دست شدن» اطلاعات قرار دادهاند؛ تا دسترسی پذیری به آنها افزایش یافته و سرعت عملیات به این نحو بهبود یابد. در این نوع سیستمها تضمینی در مورد ACID وجود ندارد.
2) اسکیمای پیش فرض
پروسههای متداول، دارای ساختاری مشخص و معمولی هستند. زیرا طراحی اولیه یک پروسه، بر مبنای مجموعهای از اطلاعات است که همیشه باید وجود داشته باشند و اگر همانند بحث کاتالوگهای محصولات، نیاز به متادیتای متغیر نباشد، ساختار و اسکیمای یک پروسه، از ابتدای طراحی آن مشخص میباشد.
و ... تمام اینها را به خوبی میتوان توسط بانکهای اطلاعاتی رابطهای، با تعریف یک اسکیمای مشخص، مدیریت کرد.
3) برنامههای LOB یا Line of business applications
در برنامههای تجاری متداول، طراحی طرحبندی فرمهای برنامه یا انقیاد دادهها، بر اساس یک اسکیما و ساختار مشخص صورت میگیرد. بدون داشتن یک اسکیمای مشخص، امکان تعاریف انقیاد دادهها به صورت strongly typed وجود نخواهد داشت. همچنین کل مفهوم Object relational mapping و ORMهای مختلف نیز بر اساس وجود یک اسکیمای مشخص و از پیش تعیین شده کار میکند. بنابراین بانکهای اطلاعاتی رابطهای، انتخاب بسیار مناسبی برای تهیه برنامههای تجاری روزمره هستند.
4) زبانهای کوئری نویسی پیشرفته
همانطور که عنوان شد برای تهیه کوئری بر روی اغلب بانکهای اطلاعاتی NoSQL، باید توسط یک برنامه ثانویه، کار پیاده سازی الگوریتم Map Reduce را انجام داد. هر چند تعدادی از این نوع بانکهای اطلاعاتی به صورت توکار دارای موتور MapReduce هستند، اما بسیاری از آنها خیر. به همین جهت برای تهیه کوئریهای متداول، کار پیاده سازی این برنامههای ثانویه مشکل خواهد بود. به این ترتیب نوشتن Ad Hoc queries و گزارشگیری بسیار مشکل میشوند.
علاوه بر امکانات خوب کوئری گرفتن در بانکهای اطلاعاتی رابطهای، این کوئریها در زمان اجرا نیز بر اساس اسکیمای موجود، بسیار بهینه و با سرعت بالا اجرا میشوند. قابلیتی که رسیدن به آن در بانکهای اطلاعاتی با اسکیمای متغیر، کار سادهای نیست و باید آنرا با کدنویسی شخصی بهینه کرد. البته اگر تعداد این نوع برنامههای ثانویه که به آنها imperative query در مقابل declarative query بانکهای رابطهای میگویند، کم باشد، شاید یکبار نوشتن و بارها استفاده کردن از آنها اهمیتی نداشته باشد؛ در غیراینصورت تبدیل به یک عذاب خواهد شد.
5) نیاز به امکانات گزارشگیری پیشرفته
گزارشگیرهای برنامههای تجاری نیز بر اساس یک ساختار و اسکیمای مشخص به کمک قابلیتهای پیشرفته کوئری نویسی بانکهای اطلاعاتی رابطهای به سادگی قابل تهیه هستند. برای تهیه گزارشاتی که قابلیت چاپ مناسبی را داشته باشند، محل قرارگیری فیلدهای مختلف در صفحه مهم هستند و با متغیر بودن آنها، قابلیت طراحی از پیش آنها را از دست خواهیم داد. در این حالت با اسکیمای متغیر، حداکثر بتوان یک dump از اطلاعات را به صورت ستونی نمایش داد.
بنابراین به صورت خلاصه، بانکهای اطلاعاتی رابطهای، جهت مدیریت کارهای روزمره تجاری اغلب شرکتها، بسیار ضروری و جزو مسایل پایهای بهشمار میروند و به این زودیها هم قرار نیست با نمونهی دیگری جایگزین شوند.
کتابخانه canvasjs
ویژگی های C++ 11/14/17 در VS 2015
مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت سوم - رهاسازی منابع سرویسهای IDisposable
رفتار IoC Container توکار ASP.NET Core با سرویسهای IDisposable
ASP.NET Core به همراه یک IoC Container توکار ارائه میشود و اگر سرویسی با طول عمرTransient و یا Scoped به آن معرفی شود و همچنین این سرویس اینترفیس IDisposable را نیز پیاده سازی کند، کار dispose خودکار آن در پایان درخواست جاری صورت میگیرد و نیازی به تنظیمات اضافهتری ندارد. در اینجا سرویسهایی با طول عمر Singleton نیز در پایان کار برنامه، زمانیکه خود ServiceProvider به پایان کارش میرسد، dispose خواهند شد.
البته این مورد یک شرط را نیز به همراه دارد: کار وهله سازی سرویسهای درخواستی باید توسط خود این IoC Container مدیریت شود تا در پایان کار بداند چگونه آنها را Dispose کند.
یک مثال: بررسی Dispose شدن خودکار یک سرویس IDisposable
namespace CoreIocServices { public interface IMyDisposableService { void Run(); } public class MyDisposableService : IMyDisposableService, IDisposable { private readonly ILogger<MyDisposableService> _logger; public MyDisposableService(ILogger<MyDisposableService> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger.LogInformation("+ {0} was created", this.GetType().Name); } public void Run() { _logger.LogInformation("Running MyDisposableService!"); } public void Dispose() { _logger.LogInformation("- {0} was disposed!", this.GetType().Name); } } }
اگر این سرویس را به یک برنامهی ASP.NET Core معرفی کنیم:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IMyDisposableService, MyDisposableService>();
namespace CoreIocSample02.Controllers { public class HomeController : Controller { private readonly IMyDisposableService _myDisposableService; public HomeController(IMyDisposableService myDisposableService) { _myDisposableService = myDisposableService; } public IActionResult Index() { _myDisposableService.Run(); return View(); }
در ادامه با اجرای برنامه، اگر به لاگهای آن دقت کنیم، این خروجی قابل مشاهده خواهد بود:
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] Route matched with {action = "Index", controller = "Home"}. Executing action CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02) info: CoreIocServices.MyDisposableService[0] + MyDisposableService was created . . . info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] Executed endpoint 'CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02)' info: CoreIocServices.MyDisposableService[0] - MyDisposableService was disposed! info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 1316.4719ms 200 text/html; charset=utf-8
بنابراین IoC Container، به صورت خودکار، کار Dispose این سرویس IDisposable را نیز انجام دادهاست.
Dispose خودکار وهلههایی که توسط IoC Container ایجاد نشدهاند
اگر ایجاد اشیاء از نوع IDisposable را خودتان و خارج از دید IoC Container توکار ASP.NET Core انجام میدهید، از مزیت پاکسازی خودکار منابع توسط آنها در پایان درخواست محروم خواهید شد، اما ... برای رفع این مشکل نیز متد context.Response.RegisterForDispose پیش بینی شدهاست. اگر شیءای از نوع IDisposable را توسط این متد به ASP.NET Core معرفی کنید، در پایان درخواست به صورت خودکار Dispose خواهد شد.
یک مثال: فرض کنید یک StreamWriter را داخل یک میانافزار ایجاد کردهاید، اما آنرا Dispose نکردهاید:
namespace CoreIocSample02 { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Use(async (context, next) => { var writer = File.CreateText(Path.GetTempFileName()); context.Response.RegisterForDispose(writer); context.Items["filewriter"] = writer; await writer.WriteLineAsync("some important information"); await writer.FlushAsync(); await next(); });
اکنون در ادامه، در اکشن متد WriteLog یک کنترلر دلخواه، کار ثبت وقایع با دریافت این writer از HttpContext.Items قابل انجام است؛ چون هنوز طول عمر درخواست جاری پایان نیافته و شیء writer به صورت خودکار Dispose نشدهاست:
namespace CoreIocSample02.Controllers { public class HomeController : Controller { public async Task<IActionResult> WriteLog() { var writer = HttpContext.Items["filewriter"] as StreamWriter; if (writer != null) { await writer.WriteLineAsync("more important information"); await writer.FlushAsync(); } return View(); }
زمانیکه به صورت متداولی از سیستم تزریق وابستگیهای ASP.NET Core استفاده میکنیم، به ازای هر درخواست HTTP رسیده، یک Scope از نوع IServiceScopeFactory ایجاد میشود و با پایان درخواست، این Scope نیز Dispose خواهد شد. به این ترتیب هر سرویس ایجاد شدهی درون این Scope نیز Dispose میشود؛ کاری شبیه به عملیات زیر:
using(var scope = serviceProvider.CreateScope()) { var provider = scope.ServiceProvider; var resolvedService = provider.GetRequiredService(someType); // Use resolvedService... }
مشکل! اگر از سرویس فرضی IOperationScoped با طول عمر Scoped در متدهای مختلف کلاس آغازین برنامه استفاده کنیم (مانند DbContext برنامه)، طول عمری را که دریافت خواهیم کرد singleton خواهد بود و نه Scoped؛ چون درون یک scopeFactory.CreateScope ایجاد شدهی به صورت خودکار توسط یک درخواست قرار نداریم. بنابراین هر درخواست وهلهای از سرویس IOperationScoped با طول عمر Scoped، تنها همان وهلهی ابتدایی آنرا باز میگرداند و singleton رفتار میکند؛ چون scope ایی ایجاد و تخریب نشدهاست.
در یک چنین مواردی، برای اطمینان حاصل کردن از dispose شدن سرویس در پایان کار، نیاز است مراحل ایجاد scope و dispose آنرا به صورت دستی به نحو ذیل مدیریت کنیم:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory) { using (var scope = scopeFactory.CreateScope()) { var initializer = scope.ServiceProvider.GetService<IOperationScoped>(); initializer.SeedAsync().Wait(); } }
Dispose کردن سرویسهای IDisposable در برنامههای Console
اگر همین سرویس IMyDisposableService را در مثال برنامهی کنسول قسمت اول استفاده کنیم:
var myDisposableService = serviceProvider.GetService<IMyDisposableService>(); myDisposableService.Run();
در برنامهی کنسول، کار ایجاد serviceProvider را خودمان انجام دادیم:
var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider();
namespace Microsoft.Extensions.DependencyInjection { public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback { public void Dispose(); public object GetService(Type serviceType); } }
namespace CoreIocSample01 { class Program { static void Main(string[] args) { var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); using (var serviceProvider = serviceCollection.BuildServiceProvider()) { var myDisposableService = serviceProvider.GetService<IMyDisposableService>(); myDisposableService.Run(); var testService = serviceProvider.GetService<ITestService>(); testService.Run(); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-02.zip
دوره 8 ساعته آموزش Blazor
⭐️ Course Contents ⭐️
⌨️ (0:00:00) Introduction
⌨️ (0:00:34) Blazor Architecture Overview
⌨️ (0:11:59) Blazor Architecture In Depth
⌨️ (0:24:00) Blazor Hosting Models
⌨️ (0:32:23) Project Structure
⌨️ (0:48:31) Data Binding
⌨️ (1:12:21) Components Basics
⌨️ (1:33:33) Communication Between Components Overview
⌨️ (1:37:59) Component Parameters
⌨️ (1:45:03) Route Parameters
⌨️ (1:57:05) Cascading Parameters
⌨️ (2:14:58) EventCallBack
⌨️ (2:27:16) Referencing Child Components
⌨️ (2:40:06) Templated Components - RenderFragment
⌨️ (2:51:31) Templated Components - Generic Typed Item List
⌨️ (3:07:01) Templated Components - Generic Typed RenderFragment
⌨️ (3:27:27) Templated Components - Generic Typed RenderFragment as a Form
⌨️ (3:37:30) When is Rendering Triggered
⌨️ (3:59:45) Lifecycle Events Sequence
⌨️ (4:11:39) Avoiding Data Initialization Pitfall
⌨️ (4:16:15) Forms and Validations
⌨️ (4:22:00) Dependency Injection
⌨️ (4:49:34) State Management with Flux in C#
⌨️ (5:34:37) Authentication
⌨️ (6:04:18) Creating a reusable DataGrid Component - Columns Configuration
⌨️ (6:36:27) Creating a reusable DataGrid Component - Paging
⌨️ (7:16:49) Creating a reusable DataGrid Component - Sorting
⌨️ (7:42:51) Thank you and My Contact Info
فریم ورک Identity در سال 2013 معرفی شد، که دنباله سیستم ASP.NET Membership بود. سیستم قبلی گرچه طی سالیان استفاده میشد اما مشکلات زیادی هم بهمراه داشت. بعلاوه با توسعه دنیای وب و نرم افزار، قابلیتهای مدرنی مورد نیاز بودند که باید پشتیبانی میشدند. فریم ورک Identity در ابتدا سیستم ساده و کارآمدی برای مدیریت کاربران بوجود آورد و مشکلات پیشین را تا حد زیادی برطرف نمود. بعنوان مثال فریم ورک جدید مبتنی بر EF Code-first است، که سفارشی کردن سیستم عضویت را بسیار آسان میکند و به شما کنترل کامل میدهد. یا مثلا احراز هویت مبتنی بر پروتوکل OAuth پشتیبانی میشود که به شما اجازه استفاده از فراهم کنندگان خارجی مانند گوگل، فیسبوک و غیره را میدهد.
نسخه جدید این فریم ورک ویژگیهای زیر را معرفی میکند (بعلاوه مواردی دیگر):
- مدل حسابهای کاربری توسعه داده شده. مثلا آدرس ایمیل و اطلاعات تماس را هم در بر میگیرد
- احراز هویت دو مرحله ای (Two-Factor Authentication) توسط اطلاع رسانی ایمیلی یا پیامکی. مشابه سیستمی که گوگل، مایکروسافت و دیگران استفاده میکنند
- تایید حسابهای کاربری توسط ایمیل (Account Confirmation)
- مدیریت کاربران و نقشها (Administration of Users & Roles)
- قفل کردن حسابهای کاربری در پاسخ به Invalid log-in attempts
- تامین کننده شناسه امنیتی (Security Token Provider) برای بازتولید شناسهها در پاسخ به تغییرات تنظیمات امنیتی (مثلا هنگام تغییر کلمه عبور)
- بهبود پشتیبانی از Social log-ins
- یکپارچه سازی ساده با Claims-based Authorization
Identity 2.0 تغییرات چشم گیری نسبت به نسخه قبلی بهوجود آورده است. به نسبت ویژگیهای جدید، پیچیدگیهایی نیز معرفی شدهاند. اگر به تازگی (مانند خودم) با نسخه 1 این فریم ورک آشنا شده و کار کرده اید، آماده شوید! گرچه لازم نیست از صفر شروع کنید، اما چیزهای بسیاری برای آموختن وجود دارد.
در این مقاله نگاهی اجمالی به نسخهی جدید این فریم ورک خواهیم داشت. کامپوننتهای جدید و اصلی را خواهیم شناخت و خواهیم دید هر کدام چگونه در این فریم ورک کار میکنند. بررسی عمیق و جزئی این فریم ورک از حوصله این مقاله خارج است، بنابراین به این مقاله تنها بعنوان یک نقطه شروع برای آشنایی با این فریم ورک نگاه کنید.
اگر به دنبال اطلاعات بیشتر و بررسیهای عمیقتر هستید، لینک هایی در انتهای این مقاله نگاشت شده اند. همچنین طی هفتههای آینده چند مقاله تخصصیتر خواهم نوشت تا از دید پیاده سازی بیشتر با این فریم ورک آشنا شوید.
در این مقاله با مقدار قابل توجهی کد مواجه خواهید شد. لازم نیست تمام جزئیات آنها را بررسی کنید، تنها با ساختار کلی این فریم ورک آشنا شوید. کامپوننتها را بشناسید و بدانید که هر کدام در کجا قرار گرفته اند، چطور کار میکنند و اجزای کلی سیستم چگونه پیکربندی میشوند. گرچه، اگر به برنامه نویسی دات نت (#ASP.NET, C) تسلط دارید و با نسخه قبلی Identity هم کار کرده اید، درک کدهای جدید کار ساده ای خواهد بود.
Identity 2.0 با نسخه قبلی سازگار نیست
اپلیکیشن هایی که با نسخه 1.0 این فریم ورک ساخته شده اند نمیتوانند بسادگی به نسخه جدید مهاجرت کنند. قابلیت هایی جدیدی که پیاده سازی شده اند تغییرات چشمگیری در معماری این فریم ورک بوجود آورده اند، همچنین API مورد استفاده در اپلیکیشنها نیز دستخوش تغییراتی شده است. مهاجرت از نسخه 1.0 به 2.0 نیاز به نوشتن کدهای جدید و اعمال تغییرات متعددی دارد که از حوصله این مقاله خارج است. فعلا همین قدر بدانید که این مهاجرت نمیتواند بسادگی در قالب Plug-in and play صورت پذیرد!
شروع به کار : پروژه مثالها را از NuGet دریافت کنید
در حال حاظر (هنگام نوشتن این مقاله) قالب پروژه استانداردی برای اپلیکیشنهای ASP.NET MVC که ا ز Identity 2.0 استفاده کنند وجود ندارد. برای اینکه بتوانید از نسخه جدید این فریم ورک استفاده کنید، باید پروژه مثال را توسط NuGet دریافت کنید. ابتدا پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب Empty را در دیالوگ تنظیمات انتخاب کنید.
کنسول Package Manager را باز کنید و با اجرای فرمان زیر پروژه مثالها را دانلود کنید.
PM> Install-Package Microsoft.AspNet.Identity.Samples -Pre
پیکربندی Identity : دیگر به سادگی نسخه قبلی نیست
به نظر من یکی از مهمترین نقاط قوت فریم ورک Identity یکی از مهمترین نقاط ضعفش نیز بود. سادگی نسخه 1.0 این فریم ورک کار کردن با آن را بسیار آسان میکرد و به سادگی میتوانستید ساختار کلی و روند کارکردن کامپوننتهای آن را درک کنید. اما همین سادگی به معنای محدود بودن امکانات آن نیز بود. بعنوان مثال میتوان به تایید حسابهای کاربری یا پشتیبانی از احراز هویتهای دو مرحله ای اشاره کرد.
برای شروع نگاهی اجمالی به پیکربندی این فریم ورک و اجرای اولیه اپلیکیشن خواهیم داشت. سپس تغییرات را با نسخه 1.0 مقایسه میکنیم.
در هر دو نسخه، فایلی بنام Startup.cs در مسیر ریشه پروژه خواهید یافت. در این فایل کلاس واحدی بنام Startup تعریف شده است که متد ()ConfigureAuth را فراخوانی میکند. چیزی که در این فایل مشاهده نمیکنیم، خود متد ConfigureAuth است. این بدین دلیل است که مابقی کد کلاس Startup در یک کلاس پاره ای (Partial) تعریف شده که در پوشه App_Start قرار دارد. نام فایل مورد نظر Startup.Auth.cs است که اگر آن را باز کنید تعاریف یک کلاس پاره ای بهمراه متد ()ConfigureAuth را خواهید یافت. در یک پروژه که از نسخه Identity 1.0 استفاده میکند، کد متد ()ConfigureAuth مطابق لیست زیر است.
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // Enable the application to use a cookie to // store information for the signed in user app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login") }); // Use a cookie to temporarily store information about a // user logging in with a third party login provider app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Uncomment the following lines to enable logging // in with third party login providers //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(); } }
public partial class Startup { public void ConfigureAuth(IAppBuilder app) { // Configure the db context, user manager and role // manager to use a single instance per request app.CreatePerOwinContext(ApplicationDbContext.Create); app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create); app.CreatePerOwinContext<ApplicationRoleManager>(ApplicationRoleManager.Create); // Enable the application to use a cookie to store information for the // signed in user and to use a cookie to temporarily store information // about a user logging in with a third party login provider // Configure the sign in cookie app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), Provider = new CookieAuthenticationProvider { // Enables the application to validate the security stamp when the user // logs in. This is a security feature which is used when you // change a password or add an external login to your account. OnValidateIdentity = SecurityStampValidator .OnValidateIdentity<ApplicationUserManager, ApplicationUser>( validateInterval: TimeSpan.FromMinutes(30), regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)) } }); app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); // Enables the application to temporarily store user information when // they are verifying the second factor in the two-factor authentication process. app.UseTwoFactorSignInCookie( DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5)); // Enables the application to remember the second login verification factor such // as phone or email. Once you check this option, your second step of // verification during the login process will be remembered on the device where // you logged in from. This is similar to the RememberMe option when you log in. app.UseTwoFactorRememberBrowserCookie( DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie); // Uncomment the following lines to enable logging in // with third party login providers //app.UseMicrosoftAccountAuthentication( // clientId: "", // clientSecret: ""); //app.UseTwitterAuthentication( // consumerKey: "", // consumerSecret: ""); //app.UseFacebookAuthentication( // appId: "", // appSecret: ""); //app.UseGoogleAuthentication(); } }
مورد بعدی ای که جلب توجه میکند فراخوانیهای دیگری برای پیکربندی احراز هویت دو مرحلهای است. همچنین پیکربندیهای جدیدی برای کوکیها تعریف شده است که در نسخه قبلی وجود نداشتند.
تا اینجا پیکربندیهای اساسی برای اپلیکیشن شما انجام شده است و میتوانید از اپلیکیشن خود استفاده کنید. بکارگیری فراهم کنندگان خارجی در حال حاضر غیرفعال است و بررسی آنها نیز از حوصله این مقاله خارج است. این کلاس پیکربندیهای اساسی Identity را انجام میدهد. کامپوننتهای پیکربندی و کدهای کمکی دیگری نیز وجود دارند که در کلاس IdentityConfig.cs تعریف شده اند.
پیش از آنکه فایل IdentityConfig.cs را بررسی کنیم، بهتر است نگاهی به کلاس ApplicationUser بیاندازیم که در پوشه Models قرار گرفته است.
کلاس جدید ApplicationUser در Identity 2.0
اگر با نسخه 1.0 این فریم ورک اپلیکیشنی ساخته باشید، ممکن است متوجه شده باشید که کلاس پایه IdentityUser محدود و شاید ناکافی باشد. در نسخه قبلی، این فریم ورک پیاده سازی IdentityUser را تا حد امکان ساده نگاه داشته بود تا اطلاعات پروفایل کاربران را معرفی کند.
public class IdentityUser : IUser { public IdentityUser(); public IdentityUser(string userName); public virtual string Id { get; set; } public virtual string UserName { get; set; } public virtual ICollection<IdentityUserRole> Roles { get; } public virtual ICollection<IdentityUserClaim> Claims { get; } public virtual ICollection<IdentityUserLogin> Logins { get; } public virtual string PasswordHash { get; set; } public virtual string SecurityStamp { get; set; } }
اگر از نسخه Identity 1.0 استفاده کرده باشید و مطالعاتی هم در این زمینه داشته باشید، میدانید که توسعه کلاس کاربران بسیار ساده است. مثلا برای افزودن فیلد آدرس ایمیل و اطلاعات دیگر کافی بود کلاس ApplicationUser را ویرایش کنیم و از آنجا که این فریم ورک مبتنی بر EF Code-first است بروز رسانی دیتابیس و مابقی اپلیکیشن کار چندان مشکلی نخواهد بود.
با ظهور نسخه Identity 2.0 نیاز به برخی از این سفارشی سازیها از بین رفته است. گرچه هنوز هم میتوانید بسادگی مانند گذشته کلاس ApplicationUser را توسعه و گسترش دهید، تیم ASP.NET تغییراتی بوجود آورده اند تا نیازهای رایج توسعه دهندگان را پاسخگو باشد.
اگر به کد کلاسهای مربوطه دقت کنید خواهید دید که کلاس ApplicationUser همچنان از کلاس پایه IdentityUser ارث بری میکند، اما این کلاس پایه پیچیدهتر شده است. کلاس ApplicationUser در پوشه Models و در فایلی بنام IdentityModels.cs تعریف شده است. همانطور که میبینید تعاریف خود این کلاس بسیار ساده است.
public class ApplicationUser : IdentityUser { public async Task<ClaimsIdentity> GenerateUserIdentityAsync( UserManager<ApplicationUser> manager) { // Note the authenticationType must match the one // defined in CookieAuthenticationOptions.AuthenticationType var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie); // Add custom user claims here return userIdentity; } }
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey> where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey> where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey> where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey> { public IdentityUser(); // Used to record failures for the purposes of lockout public virtual int AccessFailedCount { get; set; } // Navigation property for user claims public virtual ICollection<TClaim> Claims { get; } // Email public virtual string Email { get; set; } // True if the email is confirmed, default is false public virtual bool EmailConfirmed { get; set; } // User ID (Primary Key) public virtual TKey Id { get; set; } // Is lockout enabled for this user public virtual bool LockoutEnabled { get; set; } // DateTime in UTC when lockout ends, any // time in the past is considered not locked out. public virtual DateTime? LockoutEndDateUtc { get; set; } // Navigation property for user logins public virtual ICollection<TLogin> Logins { get; } // The salted/hashed form of the user password public virtual string PasswordHash { get; set; } // PhoneNumber for the user public virtual string PhoneNumber { get; set; } // True if the phone number is confirmed, default is false public virtual bool PhoneNumberConfirmed { get; set; } // Navigation property for user roles public virtual ICollection<TRole> Roles { get; } // A random value that should change whenever a users // credentials have changed (password changed, login removed) public virtual string SecurityStamp { get; set; } // Is two factor enabled for the user public virtual bool TwoFactorEnabled { get; set; } // User name public virtual string UserName { get; set; } }
اما از همه چیز مهمتر امضا (Signature)ی خود کلاس است. این آرگومانهای جنریک چه هستند؟ به امضای این کلاس دقت کنید.
public class IdentityUser<TKey, TLogin, TRole, TClaim> : IUser<TKey> where TLogin : Microsoft.AspNet.Identity.EntityFramework.IdentityUserLogin<TKey> where TRole : Microsoft.AspNet.Identity.EntityFramework.IdentityUserRole<TKey> where TClaim : Microsoft.AspNet.Identity.EntityFramework.IdentityUserClaim<TKey>
public virtual TKey Id { get; set; }
public virtual ICollection<TRole> Roles { get; }
در نسخه Identity 1.0 کلاس IdentityUserRole بصورت زیر تعریف شده بود.
public class IdentityUserRole { public IdentityUserRole(); public virtual IdentityRole Role { get; set; } public virtual string RoleId { get; set; } public virtual IdentityUser User { get; set; } public virtual string UserId { get; set; } }
public class IdentityUserRole<TKey> { public IdentityUserRole(); public virtual TKey RoleId { get; set; } public virtual TKey UserId { get; set; } }
بررسی پیاده سازی جدید IdentityUser از حوصله این مقاله خارج است. فعلا همین قدر بدانید که گرچه تعاریف پایه کلاس کاربران پیچیدهتر شده است، اما انعطاف پذیری بسیار خوبی بدست آمده که شایان اهمیت فراوانی است.
از آنجا که کلاس ApplicationUser از IdentityUser ارث بری میکند، تمام خواص و تعاریف این کلاس پایه در ApplicationUser قابل دسترسی هستند.
کامپوننتهای پیکربندی Identity 2.0 و کدهای کمکی
گرچه متد ()ConfigAuth در کلاس Startup، محلی است که پیکربندی Identity در زمان اجرا صورت میپذیرد، اما در واقع کامپوننتهای موجود در فایل IdentityConfig.cs هستند که اکثر قابلیتهای Identity 2.0 را پیکربندی کرده و نحوه رفتار آنها در اپلیکیشن ما را کنترل میکنند.
اگر محتوای فایل IdentityConfig.cs را بررسی کنید خواهید دید که کلاسهای متعددی در این فایل تعریف شده اند. میتوان تک تک این کلاسها را به فایلهای مجزایی منتقل کرد، اما برای مثال جاری کدها را بهمین صورت رها کرده و نگاهی اجمالی به آنها خواهیم داشت. بهرحال در حال حاظر تمام این کلاسها در فضای نام ApplicationName.Models قرار دارند.
Application User Manager و Application Role Manager
اولین چیزی که در این فایل به آنها بر میخوریم دو کلاس ApplicationUserManager و ApplicationRoleManager هستند. آماده باشید، مقدار زیادی کد با انواع داده جنریک در پیش روست!
public class ApplicationUserManager : UserManager<ApplicationUser> { public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store) { } public static ApplicationUserManager Create( IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context) { var manager = new ApplicationUserManager( new UserStore<ApplicationUser>( context.Get<ApplicationDbContext>())); // Configure validation logic for usernames manager.UserValidator = new UserValidator<ApplicationUser>(manager) { AllowOnlyAlphanumericUserNames = false, RequireUniqueEmail = true }; // Configure validation logic for passwords manager.PasswordValidator = new PasswordValidator { RequiredLength = 6, RequireNonLetterOrDigit = true, RequireDigit = true, RequireLowercase = true, RequireUppercase = true, }; // Configure user lockout defaults manager.UserLockoutEnabledByDefault = true; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5); manager.MaxFailedAccessAttemptsBeforeLockout = 5; // Register two factor authentication providers. This application uses // Phone and Emails as a step of receiving a code for verifying // the user You can write your own provider and plug in here. manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService(); var dataProtectionProvider = options.DataProtectionProvider; if (dataProtectionProvider != null) { manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>( dataProtectionProvider.Create("ASP.NET Identity")); } return manager; } public virtual async Task<IdentityResult> AddUserToRolesAsync( string userId, IList<string> roles) { var userRoleStore = (IUserRoleStore<ApplicationUser, string>)Store; var user = await FindByIdAsync(userId).ConfigureAwait(false); if (user == null) { throw new InvalidOperationException("Invalid user Id"); } var userRoles = await userRoleStore .GetRolesAsync(user) .ConfigureAwait(false); // Add user to each role using UserRoleStore foreach (var role in roles.Where(role => !userRoles.Contains(role))) { await userRoleStore.AddToRoleAsync(user, role).ConfigureAwait(false); } // Call update once when all roles are added return await UpdateAsync(user).ConfigureAwait(false); } public virtual async Task<IdentityResult> RemoveUserFromRolesAsync( string userId, IList<string> roles) { var userRoleStore = (IUserRoleStore<ApplicationUser, string>) Store; var user = await FindByIdAsync(userId).ConfigureAwait(false); if (user == null) { throw new InvalidOperationException("Invalid user Id"); } var userRoles = await userRoleStore .GetRolesAsync(user) .ConfigureAwait(false); // Remove user to each role using UserRoleStore foreach (var role in roles.Where(userRoles.Contains)) { await userRoleStore .RemoveFromRoleAsync(user, role) .ConfigureAwait(false); } // Call update once when all roles are removed return await UpdateAsync(user).ConfigureAwait(false); } }
مورد حائز اهمیت بعدی در متد ()Create فراخوانی ()<context.Get<ApplicationDBContext است. بیاد بیاورید که پیشتر نگاهی به متد ()ConfigAuth داشتیم که چند فراخوانی CreatePerOwinContext داشت که توسط آنها Callback هایی را رجیستر میکردیم. فراخوانی متد ()<context.Get<ApplicationDBContext این Callbackها را صدا میزند، که در اینجا فراخوانی متد استاتیک ()ApplicationDbContext.Create خواهد بود. در ادامه بیشتر درباره این قسمت خواهید خواهند.
اگر دقت کنید میبینید که احراز هویت، تعیین سطوح دسترسی و تنظیمات مدیریتی و مقادیر پیش فرض آنها در متد ()Create انجام میشوند و سپس وهله ای از نوع خود کلاس ApplicationUserManager بازگشت داده میشود. همچنین سرویسهای احراز هویت دو مرحله ای نیز در همین مرحله پیکربندی میشوند. اکثر پیکربندیها و تنظیمات نیازی به توضیح ندارند و قابل درک هستند. اما احراز هویت دو مرحله ای نیاز به بررسی عمیقتری دارد. در ادامه به این قسمت خواهیم پرداخت. اما پیش از آن نگاهی به کلاس ApplicationRoleManager بیاندازیم.
public class ApplicationRoleManager : RoleManager<IdentityRole> { public ApplicationRoleManager(IRoleStore<IdentityRole,string> roleStore) : base(roleStore) { } public static ApplicationRoleManager Create( IdentityFactoryOptions<ApplicationRoleManager> options, IOwinContext context) { var manager = new ApplicationRoleManager( new RoleStore<IdentityRole>( context.Get<ApplicationDbContext>())); return manager; } }
سرویسهای ایمیل و پیامک برای احراز هویت دو مرحله ای و تایید حسابهای کاربری
دو کلاس دیگری که در فایل IdentityConfig.cs وجود دارند کلاسهای EmailService و SmsService هستند. بصورت پیش فرض این کلاسها تنها یک wrapper هستند که میتوانید با توسعه آنها سرویسهای مورد نیاز برای احراز هویت دو مرحله ای و تایید حسابهای کاربری را بسازید.
public class EmailService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your email service here to send an email. return Task.FromResult(0); } }
public class SmsService : IIdentityMessageService { public Task SendAsync(IdentityMessage message) { // Plug in your sms service here to send a text message. return Task.FromResult(0); } }
// Register two factor authentication providers. This application uses // Phone and Emails as a step of receiving a code for verifying // the user You can write your own provider and plug in here. manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider<ApplicationUser> { MessageFormat = "Your security code is: {0}" }); manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider<ApplicationUser> { Subject = "SecurityCode", BodyFormat = "Your security code is {0}" }); manager.EmailService = new EmailService(); manager.SmsService = new SmsService();
کلاس کمکی SignIn
هنگام توسعه پروژه مثال Identity، تیم توسعه دهندگان کلاسی کمکی برای ما ساختهاند که فرامین عمومی احراز هویت کاربران و ورود آنها به اپلیکیشن را توسط یک API ساده فراهم میسازد. برای آشنایی با نحوه استفاده از این متدها میتوانیم به کنترلر AccountController در پوشه Controllers مراجعه کنیم. اما پیش از آن بگذارید نگاهی به خود کلاس SignInHelper داشته باشیم.
public class SignInHelper { public SignInHelper( ApplicationUserManager userManager, IAuthenticationManager authManager) { UserManager = userManager; AuthenticationManager = authManager; } public ApplicationUserManager UserManager { get; private set; } public IAuthenticationManager AuthenticationManager { get; private set; } public async Task SignInAsync( ApplicationUser user, bool isPersistent, bool rememberBrowser) { // Clear any partial cookies from external or two factor partial sign ins AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie, DefaultAuthenticationTypes.TwoFactorCookie); var userIdentity = await user.GenerateUserIdentityAsync(UserManager); if (rememberBrowser) { var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(user.Id); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity, rememberBrowserIdentity); } else { AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, userIdentity); } } public async Task<bool> SendTwoFactorCode(string provider) { var userId = await GetVerifiedUserIdAsync(); if (userId == null) { return false; } var token = await UserManager.GenerateTwoFactorTokenAsync(userId, provider); // See IdentityConfig.cs to plug in Email/SMS services to actually send the code await UserManager.NotifyTwoFactorTokenAsync(userId, provider, token); return true; } public async Task<string> GetVerifiedUserIdAsync() { var result = await AuthenticationManager.AuthenticateAsync( DefaultAuthenticationTypes.TwoFactorCookie); if (result != null && result.Identity != null && !String.IsNullOrEmpty(result.Identity.GetUserId())) { return result.Identity.GetUserId(); } return null; } public async Task<bool> HasBeenVerified() { return await GetVerifiedUserIdAsync() != null; } public async Task<SignInStatus> TwoFactorSignIn( string provider, string code, bool isPersistent, bool rememberBrowser) { var userId = await GetVerifiedUserIdAsync(); if (userId == null) { return SignInStatus.Failure; } var user = await UserManager.FindByIdAsync(userId); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, provider, code)) { // When token is verified correctly, clear the access failed // count used for lockout await UserManager.ResetAccessFailedCountAsync(user.Id); await SignInAsync(user, isPersistent, rememberBrowser); return SignInStatus.Success; } // If the token is incorrect, record the failure which // also may cause the user to be locked out await UserManager.AccessFailedAsync(user.Id); return SignInStatus.Failure; } public async Task<SignInStatus> ExternalSignIn( ExternalLoginInfo loginInfo, bool isPersistent) { var user = await UserManager.FindAsync(loginInfo.Login); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } return await SignInOrTwoFactor(user, isPersistent); } private async Task<SignInStatus> SignInOrTwoFactor( ApplicationUser user, bool isPersistent) { if (await UserManager.GetTwoFactorEnabledAsync(user.Id) && !await AuthenticationManager.TwoFactorBrowserRememberedAsync(user.Id)) { var identity = new ClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie); identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id)); AuthenticationManager.SignIn(identity); return SignInStatus.RequiresTwoFactorAuthentication; } await SignInAsync(user, isPersistent, false); return SignInStatus.Success; } public async Task<SignInStatus> PasswordSignIn( string userName, string password, bool isPersistent, bool shouldLockout) { var user = await UserManager.FindByNameAsync(userName); if (user == null) { return SignInStatus.Failure; } if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } if (await UserManager.CheckPasswordAsync(user, password)) { return await SignInOrTwoFactor(user, isPersistent); } if (shouldLockout) { // If lockout is requested, increment access failed // count which might lock out the user await UserManager.AccessFailedAsync(user.Id); if (await UserManager.IsLockedOutAsync(user.Id)) { return SignInStatus.LockedOut; } } return SignInStatus.Failure; } }
این متدها ویژگیهای جدیدی که در Identity 2.0 عرضه شده اند را در بر میگیرند. متد آشنایی بنام ()SignInAsync را میبینیم، و متدهای دیگری که مربوط به احراز هویت دو مرحله ای و external log-ins میشوند. اگر به متدها دقت کنید خواهید دید که برای ورود کاربران به اپلیکیشن کارهای بیشتری نسبت به نسخه پیشین انجام میشود.
بعنوان مثال متد Login در کنترلر AccountController را باز کنید تا نحوه مدیریت احراز هویت در Identity 2.0 را ببینید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (!ModelState.IsValid) { return View(model); } // This doen't count login failures towards lockout only two factor authentication // To enable password failures to trigger lockout, change to shouldLockout: true var result = await SignInHelper.PasswordSignIn( model.Email, model.Password, model.RememberMe, shouldLockout: false); switch (result) { case SignInStatus.Success: return RedirectToLocal(returnUrl); case SignInStatus.LockedOut: return View("Lockout"); case SignInStatus.RequiresTwoFactorAuthentication: return RedirectToAction("SendCode", new { ReturnUrl = returnUrl }); case SignInStatus.Failure: default: ModelState.AddModelError("", "Invalid login attempt."); return View(model); } }
مقایسه Sign-in با نسخه Identity 1.0
در نسخه 1.0 این فریم ورک، ورود کاربران به اپلیکیشن مانند لیست زیر انجام میشد. اگر متد Login در کنترلر AccountController را باز کنید چنین قطعه کدی را میبینید.
[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) { if (ModelState.IsValid) { var user = await UserManager.FindAsync(model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); } else { ModelState.AddModelError("", "Invalid username or password."); } } // If we got this far, something failed, redisplay form return View(model); }
private async Task SignInAsync(ApplicationUser user, bool isPersistent) { AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync( user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties() { IsPersistent = isPersistent }, identity); }
ApplicationDbContext
اگر از نسخه پیشین Identity در اپلیکیشنهای ASP.NET MVC استفاده کرده باشید با کلاس ApplicationDbContext آشنا هستید. این کلاس پیاده سازی پیش فرض EF فریم ورک است، که اپلیکیشن شما توسط آن دادههای مربوط به Identity را ذخیره و بازیابی میکند.
در پروژه مثال ها، تیم Identity این کلاس را بطور متفاوتی نسبت به نسخه 1.0 پیکربندی کرده اند. اگر فایل IdentityModels.cs را باز کنید تعاریف کلاس ApplicationDbContext را مانند لیست زیر خواهید یافت.
public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } static ApplicationDbContext() { // Set the database intializer which is run once during application start // This seeds the database with admin user credentials and admin role Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer()); } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } }
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> { protected override void Seed(ApplicationDbContext context) { InitializeIdentityForEF(context); base.Seed(context); } public static void InitializeIdentityForEF(ApplicationDbContext db) { var userManager = HttpContext .Current.GetOwinContext() .GetUserManager<ApplicationUserManager>(); var roleManager = HttpContext.Current .GetOwinContext() .Get<ApplicationRoleManager>(); const string name = "admin@admin.com"; const string password = "Admin@123456"; const string roleName = "Admin"; //Create Role Admin if it does not exist var role = roleManager.FindByName(roleName); if (role == null) { role = new IdentityRole(roleName); var roleresult = roleManager.Create(role); } var user = userManager.FindByName(name); if (user == null) { user = new ApplicationUser { UserName = name, Email = name }; var result = userManager.Create(user, password); result = userManager.SetLockoutEnabled(user.Id, false); } // Add user admin to Role Admin if not already added var rolesForUser = userManager.GetRoles(user.Id); if (!rolesForUser.Contains(role.Name)) { var result = userManager.AddToRole(user.Id, role.Name); } } }
نکته حائز اهمیت دیگر متد ()InitializeIdentityForEF است. این متد کاری مشابه متد ()Seed انجام میدهد که هنگام استفاده از مهاجرتها (Migrations) از آن استفاده میکنیم. در این متد میتوانید رکوردهای اولیه ای را در دیتابیس ثبت کنید. همانطور که مشاهده میکنید در قطعه کد بالا نقشی مدیریتی بنام Admin ایجاد شده و کاربر جدیدی با اطلاعاتی پیش فرض ساخته میشود که در آخر به این نقش منتسب میگردد. با انجام این مراحل، پس از اجرای اولیه اپلیکیشن کاربری با سطح دسترسی مدیر در اختیار خواهیم داشت که برای تست اپلیکیشن بسیار مفید خواهد بود.
در این مقاله نگاهی اجمالی به Identity 2.0 در پروژههای ASP.NET MVC داشتیم. کامپوننتهای مختلف فریم ورک و نحوه پیکربندی آنها را بررسی کردیم و با تغییرات و قابلیتهای جدید به اختصار آشنا شدیم. در مقالات بعدی بررسی هایی عمیقتر خواهیم داشت و با نحوه استفاده و پیاده سازی قسمتهای مختلف این فریم ورک آشنا خواهیم شد.
مطالعه بیشتر