«... nist.gov برای شدت آسیبپذیری ShellShock شدت ۱۰ از ۱۰ در نظر گرفته است. این آسیبپذیری برای بهرهبرداری نیاز به دسترسی اولیه ندارد و در شرایطی از راه دور با ارسال یک درخواست میتوان دستوراتی را سیستم قربانی اجرا کرد. در آسیبپذیری HeartBleed نیز بدون نیاز به دسترسی اولیه امکان بهرهبرداری وجود داشت ولی امکان اجرای دستور بروی سیستم قربانی به صورت مستقیم وجود نداشت ...»
مثال - نمایش درصد پیشرفت عملیات توسط SignalR
نکتهای در مورد نگارشهای مختلف SignalR
اگر برنامه شما قرار است دات نت 4 را پشتیبانی کند، آخرین نگارش SignalR که با آن سازگار است، نگارش 1.1.3 میباشد. بنابراین اگر دستور ذیل را اجرا کنید:
PM> Install-Package Microsoft.AspNet.SignalR
اگر دستور ذیل را اجرا کنید، SiganlR 1.x را نصب میکند که با دات نت 4 به بعد سازگار است:
PM> Install-Package Microsoft.AspNet.SignalR -Version 1.1.3
با اینکار Microsoft.AspNet.SignalR.JS نیز به صورت خودکار نصب میگردد و به این ترتیب کلاینت جاوا اسکریپتی SiganlR نیز در برنامه قابل استفاده خواهد بود.
تنظیمات فایل Global.asax.cs
سطر فراخوانی متد RouteTable.Routes.MapHubs باید در ابتدای متد Application_Start فایل Global.asax.cs قرار گیرد (پیش از هر تنظیم دیگری). تفاوتی هم نمیکند که برنامه وب فرم است یا MVC. به این ترتیب مسیریابیهای SignalR تنظیم شده و مسیر http://localhost/signalr/hubs قابل استفاده خواهد بود.
تنظیمات اسکریپتهای سمت کلاینت مورد نیاز
پس از نصب بسته SignalR، سه اسکریپت ذیل باید به ابتدای صفحه وب اضافه شوند تا کلاینتهای جاوا اسکریپتی SignalR بتوانند با سرور ارتباط برقرار کنند:
<script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script> <script src="Scripts/jquery.signalR-1.1.3.min.js" type="text/javascript"></script> <script src="signalr/hubs" type="text/javascript"></script>
تعریف کلاس Hub برنامه
using Microsoft.AspNet.SignalR; namespace WebFormsSample03.Common { public class ProgressHub : Hub { /// <summary> /// این متد استاتیک تعریف شده تا در برنامه به صورت مستقیم قابل استفاده باشد /// یا میشد اصلا این متد تعریف نشود و از همان دریافت زمینه هاب در کنترلر استفاده گردد /// </summary> public static void UpdateProgressBar(int value, string connectionId) { var ctx = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); ctx.Clients.Client(connectionId).updateProgressBar(value); //فراخوانی یک متد در سمت کلاینت } } }
البته تعریف این متد در اینجا ضروری نبود. حتی میشد بدنه کلاس هاب را خالی تعریف کرد و متد GetHubContext را مستقیما داخل یک کنترلر فراخوانی نمود.
متد UpdateProgressBar، مقدار value را به تنها یک کلاینت که Id آن مساوی connectionId دریافتی است، ارسال میکند. این کلاینت باید یک callback جاوا اسکریپتی را جهت تامین متد پویای updateProgressBar تدارک ببیند.
کلاس Web API کنترلر دریافت فایلها
فرقی نمیکند که برنامه شما از نوع وب فرم است یا MVC. امکانات Web API در هر دو نوع پروژه، قابل دسترسی است (همان ایده یک ASP.NET واحد).
بنابراین نیاز است یک کنترلر وب API جدید را به پروژه اضافه کرده و محتوای آن را به شکل ذیل تغییر دهیم:
using System.Threading; using System.Web.Http; using WebFormsSample03.Common; namespace WebFormsSample03 { public class DownloadRequest { public string Url { set; get; } public string ConnectionId { set; get; } } public class DownloaderController : ApiController { public void Post([FromBody]DownloadRequest data) { //todo: start downloading the data.Url .... ProgressHub.UpdateProgressBar(10, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(40, data.ConnectionId); Thread.Sleep(3000); ProgressHub.UpdateProgressBar(64, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(77, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(92, data.ConnectionId); Thread.Sleep(3000); ProgressHub.UpdateProgressBar(99, data.ConnectionId); Thread.Sleep(2000); ProgressHub.UpdateProgressBar(100, data.ConnectionId); } } }
using System; using System.Web.Http; using System.Web.Routing; namespace WebFormsSample03 { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { // Register the default hubs route: ~/signalr RouteTable.Routes.MapHubs(); RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
همچنین در اینجا با توجه به مسیریابی تعریف شده، باید اطلاعات را به آدرس api/Downloader از نوع Post ارسال کرد.
تعریف کلاینت متصل به Hub
در سمت سرور، متد پویای updateProgressBar فراخوانی شده است. اکنون باید این متد را در سمت کلاینت پیاده سازی کنیم:
<form id="form1" runat="server"> <div> <input id="txtUrl" value="http://www.site.com/file.rar" type="text" /> <input id="send" type="button" value="start download ..." /> <br /> <div id="bar" style="border: #000 1px solid; width:300px;"></div> </div> </form> <script type="text/javascript"> $(function () { $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ میکند var progressHub = $.connection.progressHub; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است progressHub.client.updateProgressBar = function (value) { //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است //به این ترتیب سرور میتواند کلاینت را فراخوانی کند $("#bar").html(GaugeBar.generate(value)); }; $.connection.hub.start() // فاز اولیه ارتباط را آغاز میکند .done(function () { $("#send").click(function () { $("#send").attr('disabled', 'disabled'); var myClientId = $.connection.hub.id; // اکنون اتصال برقرار است به سرور $.ajax({ type: "POST", contentType: "application/json", url: "/api/Downloader", data: JSON.stringify({ Url: $("#txtUrl").val(), ConnectionId: myClientId }) }).success(function () { $("#send").removeAttr('disabled'); }).fail(function () { // }); }); }); }); </script>
در ابتدای کار صفحه، اتصال به progressHub برقرار میشود. اگر دقت کنید، نام این هاب با حروف کوچک در اینجا (در سمت کلاینت) آغاز میگردد.
سپس با تعریف یک callback به نام progressHub.client.updateProgressBar، پیامهای دریافتی از طرف سرور را به یک افزونه progress bar جیکوئری، برای نمایش ارسال میکند.
کار اتصال به رویداد کلیک دکمهی آغاز دریافت فایل، در متد done باید انجام شود. این callback زمانی فراخوانی میگردد که کار اتصال به سرور با موفقیت صورت گرفته باشد.
سپس در ادامه توسط jQuery Ajax، اطلاعات Url و همچنین Id کلاینت را به مسیر api/Downloader یا همان web api controller ارسال میکنیم.
کدهای کامل این مثال را از اینجا نیز میتوانید دریافت نمائید:
WebFormsSample03.zip
Delegate در سی شارپ
using System; using System.Windows.Forms; namespace CSharpDelegates { public delegate void Display(string sMsg); public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { Display del = new Display(ShowMessage); del("This is an example for delegate"); } private void ShowMessage(string strMessage) { MessageBox.Show(strMessage); } } }
using System.Collections.Generic; namespace CSharpDelegates { public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } public int Experience { get; set; } public double Salary { get; set; } public void IncreaseSalary(List<Employee> Employees) { foreach (Employee emp in Employees) { if (emp.Salary < 10000) { emp.Salary = emp.Salary + emp.Salary * 0.3; } } } } }
using System.Collections.Generic; namespace CSharpDelegates { public delegate bool SalaryIncreaseEligibility(Employee emp); public class Employee { public int EmployeeId { get; set; } public string Name { get; set; } public int Experience { get; set; } public double Salary { get; set; } public string IncreaseSalary(List<Employee> Employees, SalaryIncreaseEligibility del) { string sSalIncreasdEmployees = "Salary increased for "; foreach (Employee emp in Employees) { if (del(emp)) { emp.Salary = emp.Salary + emp.Salary * 0.3; sSalIncreasdEmployees = sSalIncreasdEmployees + emp.Name + " ,"; } } return sSalIncreasdEmployees; } } }
using System; using System.Collections.Generic; using System.Windows.Forms; namespace CSharpDelegates { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { List<Employee> empList = new List<Employee>(); empList.Add(new Employee() { EmployeeId = 100, Name = "Mark", Salary = 2000, Experience = 3 }); empList.Add(new Employee() { EmployeeId = 101, Name = "John", Salary = 15000, Experience = 8 }); empList.Add(new Employee() { EmployeeId = 102, Name = "David", Salary = 4000, Experience = 4 }); empList.Add(new Employee() { EmployeeId = 103, Name = "Bob", Salary = 50000, Experience = 14 }); empList.Add(new Employee() { EmployeeId = 104, Name = "Alex", Salary = 9000, Experience = 6 }); SalaryIncreaseEligibility del = new SalaryIncreaseEligibility(SalaryEligibility); Employee objEmp = new Employee(); string sMsg = objEmp.IncreaseSalary(empList, del); MessageBox.Show(sMsg); } private bool SalaryEligibility(Employee emp) { if (emp.Salary > 10000) { return true; } else { return false; } } } }
Install-Package iTextSharp
شروع کار با iTextSharp
معمولا برای کار با iText یک سری روال تکراری از قبیل انتخاب نام فایل نهایی، تعریف فونت، سایز کاغذ، حاشیه بندی و ... را باید طی کنید که کدهای آن را در ذیل مشاهده میکنید:
var fileStream = new FileStream("card.pdf", FileMode.Create, FileAccess.Write, FileShare.None); var docFont = GetFont(); var pageSize = PageSize.A6.Rotate(); // سایز کارت را اینجا باید مشخص کرد var doc = new Document(pageSize); doc.SetMargins(18f, 18f, 15f, 2f); var pdfWriter = PdfWriter.GetInstance(doc, fileStream); doc.Open();
برای درج عکس به صورت شفاف در پس زمینه کارت باید از کد زیر استفاده کرد:
// درج لوگوی مسابقات به صورت شفاف در پس زمینه var canvas = pdfWriter.DirectContentUnder; var logoImg = Image.GetInstance(competitionImagePath); logoImg.SetAbsolutePosition(0, 0); logoImg.ScaleAbsolute(pageSize); var graphicsState = new PdfGState { FillOpacity = 0.2F }; canvas.SetGState(graphicsState); canvas.AddImage(logoImg);
چیدمان و طرح بندی بندی عناصر در iTextSharp
برای طراحی کارت یا کلا کار طراحی، باید با نحوهی قرار دادن و طرح بندی عناصر مثل تصاویر و نوشتهها و ابزارهای مورد نیاز برای این کار، آشنا شوید. خوشبختانه در iText برای این کار ابزارهای خوبی وجود دارد.
حتما با تگ Table در HTML آشنایی دارید. در سالهای دور، حتی کل صفحهی وب را به وسیلهی Table ساختار دهی میکردند. در iTextSharp نیز کلاسی به نام PdfPTable در دسترس است که میتوان از آن به عنوان قالبی برای قرار دادن عناصر، در صفحه استفاده کرد. این Table همانند هر جدولی دارای یک سری سطر و ستون است که میتوانیم عناصر مورد نظرمان مثل تصویر و نوشته و... را در آن قرار دهیم.
// جدولی که برای چیدمان عناصر ارم دانشگاه و عنوان و عکس شخص استفاده میشود var topTable = new PdfPTable(3) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, }; var universityLogoImage = Image.GetInstance(universityLogoPath); universityLogoImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(universityLogoImage) { HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); topTable.AddCell(new PdfPCell(new Phrase("کارت مسابقات دانشگاه آزاد اسلامی", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_CENTER, Border = 0, }); var userImage = Image.GetInstance(userModel.ImagePath); userImage.Border = Rectangle.TOP_BORDER | Rectangle.RIGHT_BORDER | Rectangle.BOTTOM_BORDER | Rectangle.LEFT_BORDER; userImage.BorderWidth = 1f; userImage.BorderColor = new BaseColor(204, 204, 204); // gray color userImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(userImage) { HorizontalAlignment = 2, Border = 0 }); int[] topTableColumnsWidth = { 10, 25, 10 }; topTable.SetWidths(topTableColumnsWidth); doc.Add(topTable);
public class UserModel { public string FirstName { get; set; } public string LastName { get; set; } public string StudentNumber { get; set; } public string NationalCode { get; set; } public string UniversityName { get; set; } public string ImagePath { get; set; } }
using System; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; using Font = iTextSharp.text.Font; using Image = iTextSharp.text.Image; using Rectangle = iTextSharp.text.Rectangle; namespace ITextSharpCardSample { public class CardReport { public static void Generate(UserModel userModel, string competitionImagePath, string universityLogoPath) { var fileStream = new FileStream("card.pdf", FileMode.Create, FileAccess.Write, FileShare.None); var docFont = GetFont(); var pageSize = PageSize.A6.Rotate(); // سایز کارت را اینجا باید مشخص کرد var doc = new Document(pageSize); doc.SetMargins(18f, 18f, 15f, 2f); var pdfWriter = PdfWriter.GetInstance(doc, fileStream); doc.Open(); // درج لوگوی مسابقات به صورت شفاف در پس زمینه var canvas = pdfWriter.DirectContentUnder; var logoImg = Image.GetInstance(competitionImagePath); logoImg.SetAbsolutePosition(0, 0); logoImg.ScaleAbsolute(pageSize); var graphicsState = new PdfGState { FillOpacity = 0.2F }; canvas.SetGState(graphicsState); canvas.AddImage(logoImg); // جدولی که برای چیدمان عناصر ارم دانشگاه و عنوان و عکس شخص استفاده میشود var topTable = new PdfPTable(3) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, }; var universityLogoImage = Image.GetInstance(universityLogoPath); universityLogoImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(universityLogoImage) { HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); topTable.AddCell(new PdfPCell(new Phrase("کارت مسابقات دانشگاه آزاد اسلامی", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_CENTER, Border = 0, }); var userImage = Image.GetInstance(userModel.ImagePath); userImage.Border = Rectangle.TOP_BORDER | Rectangle.RIGHT_BORDER | Rectangle.BOTTOM_BORDER | Rectangle.LEFT_BORDER; userImage.BorderWidth = 1f; userImage.BorderColor = new BaseColor(204, 204, 204); // gray color userImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(userImage) { HorizontalAlignment = 2, Border = 0 }); int[] topTableColumnsWidth = { 10, 25, 10 }; topTable.SetWidths(topTableColumnsWidth); doc.Add(topTable); // جدول مشخصات شرکت کننده مثل نام و نام خانوادگی var infoTable = new PdfPTable(4) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, SpacingBefore = 15, }; infoTable.AddCell(new PdfPCell(new Phrase("نام:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0, PaddingBottom = 15 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.FirstName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("نام خانوادگی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.LastName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("شماره\nدانشجویی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0, PaddingBottom = 15 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.StudentNumber, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("کد ملی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.NationalCode, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("واحد دانشگاهی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.UniversityName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); // دو سلول بعدی صرفا جهت تکمیل شدن یک ردیف است تا عملکرد صحیح خود را داشته باشد infoTable.AddCell(new PdfPCell(new Phrase("", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); int[] infoTableColumnsWidth = { 20, 15, 20, 15 }; infoTable.SetWidths(infoTableColumnsWidth); doc.Add(infoTable); doc.Close(); } private static Font GetFont() { const string fontName = "Iranian Sans"; if (FontFactory.IsRegistered(fontName)) return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); var fontPath = "Fonts/irsans.ttf"; // مسیر فونت FontFactory.Register(fontPath); return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } } }
class Program { static void Main(string[] args) { var userModel = new UserModel { FirstName = "علی", LastName = "احمدی", NationalCode = "1234567890", StudentNumber = "23242342", UniversityName = "آزاد", ImagePath = "Images/avatar.jpg" }; CardReport.Generate(userModel, "Images/competition_logo.jpg", "Images/university_logo.png"); System.Diagnostics.Process.Start("card.pdf"); } }
امکان خروجی اکسل از گزارشات سیستم، یکی از بایدهای بیشتر سیستمهای اطلاعاتی میباشد؛ یکی از چالشهای اصلی در تولید این نوع خروجی، افزایش مصرف حافظه متناسب با افزایش حجم دیتا میباشد. از آنجاییکه بیشتر راهکارهای موجود از جمله 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، خروجی مورد نظر از قبل ذخیره کرده و لینک دانلودی را در اختیار کاربر قرار دهید.
بازسازی کد (Refactoring)
مدیریت سراسری خطاها در یک برنامهی Angular
طریقه بررسی صحت کدملی به کمک متدهای الحاقی
[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]