انتشار Windows 10 SDK
استفاده از Chart.js در Blazor
This is a Blazor library that wraps Chart.js. You can use it in both client- and server-side projects.
Asp.Net Core WebSockets Vs SignalR. Which should you use? (Full Course)
In this video we build 2 separate chat applications, one using Asp.Net Core WebSockets and the other using SignalR, allowing you to compare approaches and decide on which one works best for you. In both cases we build them with C#, .NET Core and JavaScript. You’ll also learn about:
- .NET Core Request Pipeline
- Request Delegates
- Asynchronous Programming in .NET (Async / Await)
- Introduction to Dependency Injection
امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستمهای اطلاعاتی میباشد؛ یکی از چالشهای اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا میباشد. از آنجاییکه بیشتر راهکارهای موجود از جمله 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، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.
تعیین اجباری بودن یا نبودن ستونها در EF Core
به صورت پیش فرض در EF Core، هر نوع CLR ایی که نال پذیر باشد، به صورت یک ستون اختیاری در نظر گرفته میشود؛ مانند:
string, int?, byte[]
int, decimal, bool, DateTime
برای لغو اختیاری بودن یک خاصیت نال پذیر میتوان از ویژگی Required استفاده کرد:
[Required] public string Url { get; set; }
و یا معادل Fluent API آن با استفاده از ذکر متد IsRequired است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Url) .IsRequired(); }
کار با رشتهها در EF Core
ذکر یک خاصیت رشتهای به این صورت:
public string FirstName { get; set; }
[StringLength(450)] public string FirstName { get; set; } [MaxLength(450)] public string LastName { get; set; } [MaxLength] public string Address { get; set; }
برای تعیین صریح یک فیلد رشتهای به حداکثر مقدار آن بهتر است ویژگی MaxLength را بدون ذکر پارامتری قید کرد. این مورد جهت سازگاری با بانکهای اطلاعاتی مختلف ضروری است.
معادل این تنظیمات با روش Fluent API به صورت زیر است:
برای تعیین صریح طول یک فیلد رشتهای:
modelBuilder.Entity<Person>() .Property(x => x.Address) .HasMaxLength(450);
modelBuilder.Entity<Person>() .Property(x => x.Address) .HasColumnType("nvarchar(max)");
[Column(TypeName = "varchar")] [MaxLength] public string Address { get; set; }
نکتهای در مورد تغییر نوع خواص: اگر از متد HasColumnType و یا ویژگی Column به نحو فوق استفاده کردید، نیاز است طول رشته را صریحا مشخص کنید. در غیر اینصورت در حین migration خطای ذیل را دریافت خواهید کرد:
Data type 'varchar' is not supported in this form. Either specify the length explicitly in the type name, for example as 'varchar(16)', or remove the data type and use APIs such as HasMaxLength to allow EF choose the data type.
[Column(TypeName = "varchar(max)")]
نکتهای در مورد ایندکسها: در قسمت قبل عنوان شد که میتوان بر روی خواص، ایندکس منحصربفرد اعمال کرد. در مورد رشتهها در SQL Server، اگر طول فیلد مدنظر حداکثر تا 900 بایت باشد، یک چنین کاری را میتوان انجام داد. البته این محدودیت 900 بایتی تا SQL Server 2014 وجود دارد. این سقف در SQL Server 2016 به 1700 بایت افزایش یافتهاست (900bytes for a clustered index. 1,700 for a nonclustered index). بنابراین چون نوع پیش فرض ستونهای رشتهای، یونیکد و nvarchar درنظر گرفته میشود، حداکثر طول امنی را که میتوان برای آن تعریف کرد، مساوی 450 است (نصف 900 بایت). به همین جهت ذکر ایندکس منحصربفرد بر روی یک ستون رشتهای، باید به همراه ذکر اجباری حداکثر طول مساوی 450 آن باشد.
کار با اعداد در EF Core
کلاس نمونهای را با ساختار ذیل درنظر بگیرید:
public class Person { public int Id { set; get; } public DateTime? DateAdded { set; get; } public DateTime? DateUpdated { set; get; } [StringLength(450)] public string FirstName { get; set; } [MaxLength(450)] public string LastName { get; set; } //[Column(TypeName = "varchar")] [MaxLength] public string Address { get; set; } //bit public bool IsActive { get; set; } //tiny Int public byte Age { get; set; } //small Int public short Short { get; set; } //int public int Int32 { get; set; } //Big int public long Long { get; set; } }
همانطور که ملاحظه میکنید، نوع bool دات نت به نوع bit در SQL Server، نوع long به bigint، نوع short به smallint، نوع int به int و نوع byte به tinyint نگاشت شدهاند.
نکتهای در مورد اعداد اعشاری: توصیه شدهاست در تعاریف موجودیتهای خود بهتر است از نوعهای float و یا double استفاده نکنید. برای کار با اعداد اعشاری از نوع decimal استفاده نمائید تا بتوانید از قابلیت مقایسهی دقیق آنها استفاده کنید. اطلاعات بیشتر: «روش صحیح مقایسه دو عدد اعشاری با هم»
کار با تاریخ در EF Core
اگر به تصویر فوق دقت کنید، نوع DateTime دات نت به datetime2 در سمت SQL Server ترجمه شدهاست:
CREATE TABLE [dbo].[Persons]( [DateAdded] [datetime2](7) NULL, [DateUpdated] [datetime2](7) NULL,
اطلاعات بیشتر: «کنترل نوعهای داده با استفاده از EF در SQL Server»
به علاوه در دات نت نوع DateTime از نوع value type است. بنابراین همانطور که در ابتدای بحث نیز عنوان شد، مقدار دهی آن اجباری است؛ مگر آنکه آنرا نال پذیر تعریف کنید.
کار با مباحث همزمانی در EF Core
EF Core به صورت پیش فرض، فرض میکند رکوردی را که با آن در حال کار هستید، توسط هیچ کاربر دیگری در شبکه تغییر نیافتهاست و تغییرات شما را در حین فراخوانی متد SaveChanges ذخیره میکند. اگر علاقمند هستید که EF Core در صورت تغییر مقدار خاصیت خاصی توسط سایر کاربران، این مساله را با صدور استثنایی به شما اطلاع رسانی کند، از ویژگی ConcurrencyCheck
[ConcurrencyCheck] public string Name { set; get; }
modelBuilder.Entity<Person>() .Property(p => p.Name) .IsConcurrencyToken();
اگر علاقمند هستید که تمام فیلدهای جدول تحت نظر قرارگیرند، میتوان از روش ویژهای به نام Timestamp/row version استفاده کرد:
[Timestamp] public byte[] Timestamp { get; set; }
modelBuilder.Entity<Blog>() .Property(p => p.Timestamp) .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken();
در این حالت در حین به روز رسانی یک چنین رکوردی، اگر از زمان کوئری آن (یافتن رکورد) و ذخیره سازی آن، شخص دیگری آنرا تغییر داده باشد، به علت عدم تطابق Timestamp ها، عملیات به روز رسانی باشکست روبرو شده و یک استثناء صادر میشود.
Today, we’re excited to introduce GitHub Package Registry, a package management service that makes it easy to publish public or private packages next to your source code.
GitHub Package Registry is fully integrated with GitHub, so you can use the same search, browsing, and management tools to find and publish packages as you do for your repositories. You can also use the same user and team permissions to manage code and packages together. GitHub Package Registry provides fast, reliable downloads backed by GitHub’s global CDN. And it supports familiar package management tools: JavaScript (npm), Java (Maven), Ruby (RubyGems), .NET (NuGet), and Docker images, with more to come.
سری آموزشی مقدماتی Web API
Our beginner's guide to building Web APIs with ASP.NET Core is designed to provide you with the foundation you need to start building Web APIs with .NET in a collection of short, pragmatic collection of videos. Web APIs have become a critical component in almost every type of software we use today. In this introductory video series, we will walk you through the fundamental concepts you need to know to get started with building Web APIs using ASP.NET Core. We will cover topics such as routing, validation, working with data, and much more.
همانطور که مطلع هستید سرویس پک سه SQL Server چند روزی است که منتشر شده. این به روز رسانی بر روی یک سرور بدون مشکل نصب شد؛ در سرور دیگر به علت داشتن یک سری برنامه امنیتی مزاحم (که مثلا دسترسی به رجیستری را مونیتور و سد میکنند) با شکست مواجه و در آخر پیغام Fail نمایش داده شد. مجددا آنرا اجرا کردم، سریع تمام مراحل را تمام کرد باز هم Fail را نمایش داد.
خوب؛ گفتم احتمالا مشکلی نیست. سعی کردم به سرور وصل شوم ... پیغام «این سرور دسترسی از راه دور را نمیپذیرد» و از این حرفهای متداول ظاهر شد. به لاگ موجود در Event log ویندوز که مراجعه کردم پیغام خطای زیر نمایان بود:
اوه! اوه! اوه! در این لحظهی عرفانی، دیتابیس master نابود شده! نمیشود وصل شد. سروری که داشت تا مدتی قبل بدون هیچ مشکلی کار میکرد، الان دیگر حتی نمیشود به آن وصل شد. به کنسول سرویسهای ویندوز مراجعه کردم (services.msc)، سعی کردم سرویس اس کیوال را که از کار افتاده دستی اجرا کنم، پیغام زیر مجددا در event log ظاهر شد:
قابلیت FILESTREAM را نمیتواند آغاز کند. پس از مدتی جستجو مشخص شد که این مورد را میشود در رجیستری ویندوز غیرفعال کرد؛ به صورت زیر:
2) Go To HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10.MSSQLServer\MSSQLSERVER\FileStream
3) Edit the value "EnableLevel" and set it to 0
4) Restart SQL Server.
پس از انجام اینکار، سرویس اس کیوال استارت شد (از طریق کنسول سرویسهای ویندوز). در ادامه، امکان اتصال به آن نبود (حتی با اکانت sa):
باز هم پس از مدتی جستجو معلوم گردید که «کمی باید صبر کرد». آن پیغام اول کار مبتنی بر تخریب دیتابیس master هم بیمورد است. پس از fail شدن نصب سرویس پک، هنوز برنامه نصاب آن در پشت صحنه مشغول به کار است. این مورد به وضوح در task manager ویندوز مشخص است. سرور به مدت 15 دقیقه به حال خود رها شد. پس از آن بدون مشکل اتصال برقرار گردید و همه چیز مجددا شروع به کار کرد.
بنابراین اگر در حین نصب سرویس پک SQL Server مشکلی پیش آمد، نگران نباشید. باید به نصاب آن زمان داد (برنامه mscorsw.exe در پشت صحنه مشغول به کار است). برنامه نصاب آن هم هیچ نوع خطای مفهومی را گزارش نمیدهد. تمام مراحل، بجای نمایش در برنامه تمام صفحه نصاب آن، در event log ویندوز ثبت میشود. این برنامه تمام صفحه فقط کارش نمایش یک progress bar است!
اگر ... هیچکدام از این موارد جواب نداد، امکان بازسازی دیتابیس master نیز وجود دارد: [^ , ^]
ولی دست نگه دارید و سریع اقدام نکنید. ابتدا به task manager مراجعه کنید. آیا برنامه mscorsw.exe در حال اجرا است؟ اگر بله، یعنی هنوز کار نصب تمام نشده. حداقل یک ربع باید صبر کنید.
The key highlights to cover this month include:
- Announcing SQL Server 2019 support
- New notebook features
- Announcing PowerShell notebooks
- Announcing collapsible code cells
- Performance improvements in notebooks
- Announcing Jupyter Books
- General availability of Schema Compare and SQL Server Dacpac extensions
- Announcing Visual Studio IntelliCode extension
- Bug fixes