ایجاد «خواص الحاقی»
احتمالا شاید عنوان کنید که با اشیاء dynamic میتوان چنین کاری را انجام داد. اما سؤال در مورد اشیاء غیر dynamic است.
یا نمونهی دیگر آن Attached Properties در برنامههای مبتنی بر Xaml هستند. میتوان به یک شیء از پیش موجود Xaml، خاصیتی را افزود که البته پیاده سازی آن منحصر است به همان نوع برنامهها.
راه حل پیشنهادی
یک Dictionary را ایجاد کنیم تا ارجاعی از اشیاء، به عنوان کلید، در آن ذخیره شده و سپس key/valueهایی به عنوان value هر شیء، در آن ذخیره شوند. این key/valueها همان خواص و مقادیر آنها خواهند بود. هر چند این راه حل به خوبی کار میکند اما ... مشکل نشتی حافظه دارد.
شیء Dictionary یک ارجاع قوی را از اشیاء، درون خودش نگه داری میکند و تا زمانیکه در حافظه باقی است، سیستم GC مجوز رهاسازی منابع آنها را نخواهد یافت؛ چون عموما این نوع Dictionaryها باید استاتیک تعریف شوند تا طول عمر آنها با طول عمر برنامه یکی گردد. بنابراین اساسا اشیایی که به این نحو قرار است پردازش شوند، هیچگاه dispose نخواهند شد. راه حلی برای این مساله در دات نت 4 به صورت توکار به دات نت فریم ورک اضافه شدهاست؛ به نام ساختار دادهای ConditionalWeakTable.
معرفی ConditionalWeakTable
ConditionalWeakTable جزو ساختارهای دادهای کمتر شناخته شدهی دات نت است. این ساختار داده، اشارهگرهایی را به ارجاعات اشیاء، درون خود ذخیره میکند. بنابراین چون ارجاعاتی قوی را به اشیاء ایجاد نمیکند، مانع عملکرد GC نیز نشده و برنامه در دراز مدت دچار مشکل نشتی حافظه نخواهد شد. هدف اصلی آن ایجاد ارتباطی بین CLR و DLR است. توسط آن میتوان به اشیاء دلخواه، خواصی را افزود. به علاوه طراحی آن به نحوی است که thread safe است و مباحث قفل گذاری بر روی اطلاعات، به صورت توکار در آن پیاده سازی شدهاست. کار DLR فراهم آوردن امکان پیاده سازی زبانهای پویایی مانند Ruby و Python برفراز CLR است. در این نوع زبانها میتوان به وهلههایی از اشیاء موجود، خاصیتهای جدیدی را متصل کرد.
به صورت خلاصه کار ConditionalWeakTable ایجاد نگاشتی است بین وهلههایی از اشیاء CLR (اشیایی غیرپویا) و خواصی که به آنها میتوان به صورت پویا انتساب داد. در کار GC اخلال ایجاد نمیکند و همچنین میتوان به صورت همزمان از طریق تردهای مختلف، بدون مشکل با آن کار کرد.
پیاده سازی خواص الحاقی به کمک ConditionalWeakTable
در اینجا نحوهی استفاده از ConditionalWeakTable را جهت اتصال خواصی جدید به وهلههای موجود اشیاء مشاهده میکنید:
using System.Collections.Generic; using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class AttachedProperties { public static ConditionalWeakTable<object, Dictionary<string, object>> ObjectCache = new ConditionalWeakTable<object, Dictionary<string, object>>(); public static void SetValue<T>(this T obj, string name, object value) where T : class { var properties = ObjectCache.GetOrCreateValue(obj); if (properties.ContainsKey(name)) properties[name] = value; else properties.Add(name, value); } public static T GetValue<T>(this object obj, string name) { Dictionary<string, object> properties; if (ObjectCache.TryGetValue(obj, out properties) && properties.ContainsKey(name)) return (T)properties[name]; return default(T); } public static object GetValue(this object obj, string name) { return obj.GetValue<object>(name); } } }
کاربرد اول
اگر با ASP.NET کار کرده باشید حتما با IPrincipal آشنایی دارید. خواصی مانند Identity یک کاربر در آن ذخیره میشوند.
سؤال: چگونه میتوان یک خاصیت جدید به نام مثلا Disclaimer را به وهلهای از این شیء افزود:
public static class ISecurityPrincipalExtension { public static bool Disclaimer(this IPrincipal principal) { return principal.GetValue<bool>("Disclaimer"); } public static void SetDisclaimer(this IPrincipal principal, bool value) { principal.SetValue("Disclaimer", value); } }
اگر صرفا قرار است یک خاصیت به شیءایی متصل شود، روش ذیل نیز قابل استفاده میباشد (بجای استفاده از دیکشنری از یک کلاس جهت تعریف خاصیت اضافی جدید استفاده شدهاست):
using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class PropertyExtensions { private class ExtraPropertyHolder { public bool IsDirty { get; set; } } private static readonly ConditionalWeakTable<object, ExtraPropertyHolder> _isDirtyTable = new ConditionalWeakTable<object, ExtraPropertyHolder>(); public static bool IsDirty(this object @this) { return _isDirtyTable.GetOrCreateValue(@this).IsDirty; } public static void SetIsDirty(this object @this, bool isDirty) { _isDirtyTable.GetOrCreateValue(@this).IsDirty = isDirty; } } }
کاربرد دوم
ایجاد Id منحصربفرد برای اشیاء برنامه.
فرض کنید در حال نوشتن یک Entity framework profiler هستید. طراحی فعلی سیستم Interception آن به نحو زیر است:
public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { }
راه حل: خوب ... یک خاصیت Id را به اشیاء موجود متصل کنید!
using System; using System.Runtime.CompilerServices; namespace ConditionalWeakTableSamples { public static class UniqueIdExtensions { static readonly ConditionalWeakTable<object, string> _idTable = new ConditionalWeakTable<object, string>(); public static string GetUniqueId(this object obj) { return _idTable.GetValue(obj, o => Guid.NewGuid().ToString()); } public static string GetUniqueId(this object obj, string key) { return _idTable.GetValue(obj, o => key); } } }
برای مطالعه بیشتر
The Conditional Weak Table: Enabling Dynamic Object Properties
How to create mixin using C# 4.0
Disclaimer Page using MVC
Extension Properties Revised
Easy Modeling
Providing unique ID on managed object using ConditionalWeakTable
برپایی پروژههای مورد نیاز
ابتدا یک پوشهی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا میکنیم:
> dotnet new react
سپس در این پوشه، پوشهی ClientApp پیشفرض آنرا حذف میکنیم؛ چون کمی قدیمی است. همچنین فایلهای کنترلر و سرویس آب و هوای پیشفرض آنرا به همراه پوشهی صفحات Razor آن، حذف میکنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
اکنون در ریشهی پروژهی ASP.NET Core ایجاد شده، دستور زیر را صادر میکنیم تا پروژهی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
> cd clientapp > npm install --save bootstrap axios
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود که برای افزودن فایل bootstrap.css آن به پروژهی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
- برای دریافت فایلها از سمت سرور، از کتابخانهی معروف axios استفاده خواهیم کرد.
کدهای سمت سرور دریافت فایلهای پویا
در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت میدهند. این محتوای باینری میتواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc; namespace DownloadFilesSample.Controllers { [Route("api/[controller]")] public class ReportsController : Controller { [HttpGet("[action]")] public IActionResult GetPdfReport() { return File(virtualPath: "~/app_data/sample.pdf", contentType: "application/pdf", fileDownloadName: "sample.pdf"); } } }
روش دریافت محتوای باینری در برنامههای React
برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایلهای PDF و اکسل و غیره، مهمترین نکته، تنظیم ویژگی responseType آن به blob است:
const getResults = async () => { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); }
ساخت URL برای دسترسی به اطلاعات باینری
تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی میکنند. این متد، شیء URL را از شیء window جاری دریافت میکند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید میکند. حاصل آن، یک URL ویژهاست مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شدهی از سرور را به این URL ویژه، مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport"; const [blobInfo, setBlobInfo] = useState({ blobUrl: "", fileName: "" }); useEffect(() => { getResults(); }, []); const getResults = async () => { try { const { headers, data } = await axios.get(apiUrl, { responseType: "blob" }); console.log("headers", headers); const pdfBlobUrl = window.URL.createObjectURL(data); console.log("pdfBlobUrl", pdfBlobUrl); const fileName = headers["content-disposition"] .split(";") .find(n => n.includes("filename=")) .replace("filename=", "") .trim(); console.log("filename", fileName); setBlobInfo({ blobUrl: pdfBlobUrl, fileName: fileName }); } catch (error) { console.log(error); } };
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شدهایم. به این معنا که متد getResults فراخوانی شدهی در آن، پس از رندر کامپوننت در DOM فراخوانی میشود و بهترین محلی است که از آن میتوان برای ارسال درخواستهای Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کردهایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آنرا تبدیل به یک blob URL کردهایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کردهایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر میشود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
- اکنون که نام فایل و URL دسترسی به دادهی فایل باینری دریافتی از سرور را استخراج و ایجاد کردهایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن میتوان برای مثال فایل PDF دریافتی را نمایش داد.
نمایش فایل PDF دریافتی از سرور، به همراه دکمههای دریافت، چاپ و بازکردن آن در برگهای جدید
در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده میکنید:
import axios from "axios"; import React, { useEffect, useState } from "react"; export default function DisplayPdf() { // ... const { blobUrl } = blobInfo; return ( <> <h1>Display PDF Files</h1> <button className="btn btn-info" onClick={handlePrintPdf}> Print PDF </button> <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}> Show PDF in a new tab </button> <button className="btn btn-success ml-2" onClick={handleDownloadPdf}> Download PDF </button> <section className="card mb-5 mt-3"> <div className="card-header"> <h4>using iframe</h4> </div> <div className="card-body"> <iframe title="PDF Report" width="100%" height="600" src={blobUrl} type="application/pdf" ></iframe> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using object</h4> </div> <div className="card-body"> <object data={blobUrl} aria-label="PDF Report" type="application/pdf" width="100%" height="100%" ></object> </div> </section> <section className="card mb-5"> <div className="card-header"> <h4>using embed</h4> </div> <div className="card-body"> <embed aria-label="PDF Report" src={blobUrl} type="application/pdf" width="100%" height="100%" ></embed> </div> </section> </> ); }
در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، میتوان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیتهای توکار مرورگر صورت میگیرد و نیاز به نصب افزونهی خاصی را ندارد.
در ادامه کدهای مرتبط با سه دکمهی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده میکنید.
مدیریت دکمهی چاپ PDF
پس از اینکه به blobUrl دسترسی یافتیم، اکنون میتوان یک iframe مخفی را ایجاد کرد، سپس src آنرا به این آدرس ویژه تنظیم نمود و در آخر متد print آنرا فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر میشود:
const handlePrintPdf = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const iframe = document.createElement("iframe"); iframe.style.display = "none"; iframe.src = blobUrl; document.body.appendChild(iframe); if (iframe.contentWindow) { iframe.contentWindow.print(); } };
مدیریت دکمهی نمایش فایل PDF در یک برگهی جدید
اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگهای جدید نمایش دهید، میتوان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => { const { blobUrl } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } window.open(blobUrl); };
مدیریت دکمهی دریافت فایل PDF
بجای نمایش فایل PDF میتوان دکمهای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
const handleDownloadPdf = () => { const { blobUrl, fileName } = blobInfo; if (!blobUrl) { throw new Error("pdfBlobUrl is null"); } const anchor = document.createElement("a"); anchor.style.display = "none"; anchor.href = blobUrl; anchor.download = fileName; document.body.appendChild(anchor); anchor.click(); };
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، میتوان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگیهای سمت کلاینت نیز میشود، ابتدا به پوشهی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار میکند. سپس به پوشهی اصلی برنامهی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائهی برنامهی React بر روی پورت 5001 میشود.
تولید SiteMap استاندارد و ایجاد یک ActionResult اختصاصی برای Return کردن SiteMap تولید شده
ایجاد خودکار آدرس sitemap.xml از روی آدرس کنترلر SiteMap با افزودن مسیریابی مرتبط با آن: (در حقیقت ایجاد آدرس، برای فایلی که وجود خارجی ندارد و نگاشت آن به یک کنترلر و اکشن متد خاصی در آن)
routes.MapRoute( "SiteMap_route", // Route name "sitemap.xml", // URL with parameters new { controller = "Sitemap", action = "index", name = UrlParameter.Optional, area = "" } // Parameter defaults );
مشکلی که در دراز مدت با SQLDom وجود خواهد داشت، مواردی مانند SelectStarExpression و CreateProcedureStatement و امثال آن هستند. اینها را از کجا باید تشخیص داد؟ همچنین مراحل بررسی این اجزاء، نسبتا طولانی هستند و نیاز به یک راه حل عمومیتر در این زمینه وجود دارد.
راه حلی برای این مشکل در مطلب «XML ‘Visualizer’ for the TransactSql.ScriptDom parse tree» ارائه شدهاست. در اینجا تمام اجزای TSqlFragment توسط Reflection مورد بررسی و استخراج قرار گرفته و نهایتا یک فایل XML از آن حاصل میشود.
اگر نکات ذکر شده در این مقاله را تبدیل به یک برنامه با استفاده مجدد کنیم، به چنین شکلی خواهیم رسید:
این برنامه را از اینجا میتوانید دریافت کنید:
DomToXml.zip
همانطور که در تصویر مشاهده میکنید، اینبار به سادگی، SelectStarExpression قابل تشخیص است و تنها کافی است در T-SQL پردازش شده، به دنبال SelectStarExpressionها بود. برای اینکار جهت ساده شدن آنالیز میتوان با ارث بری از کلاس پایه TSqlFragmentVisitor شروع کرد:
using System; using System.Linq; using Microsoft.SqlServer.TransactSql.ScriptDom; namespace DbCop { public class SelectStarExpressionVisitor : TSqlFragmentVisitor { public override void ExplicitVisit(SelectStarExpression node) { Console.WriteLine( "`Select *` detected @StartOffset:{0}, Line:{1}, T-SQL: {2}", node.StartOffset, node.StartLine, string.Join(string.Empty, node.ScriptTokenStream.Select(x => x.Text)).Trim()); base.ExplicitVisit(node); } } }
مرحلهی بعد، اجرای این کلاس Visitor است:
public static class GenericVisitor { public static void Start(string tSql, TSqlFragmentVisitor visitor) { IList<ParseError> errors; TSqlScript sqlFragment; using (var reader = new StringReader(tSql)) { var parser = new TSql120Parser(initialQuotedIdentifiers: true); sqlFragment = (TSqlScript)parser.Parse(reader, out errors); } if (errors != null && errors.Any()) { var sb = new StringBuilder(); foreach (var error in errors) sb.AppendLine(error.Message); throw new InvalidOperationException(sb.ToString()); } sqlFragment.Accept(visitor); } }
مثالی از نحوهی استفاده از کلاس GenericVisitor فوق را در اینجا ملاحظه میکنید:
var tsql = @"WITH ctex AS ( SELECT * FROM sys.objects ) SELECT * FROM ctex"; GenericVisitor.Start(tsql, new SelectStarExpressionVisitor());
کتابخانه MediatR
Simple mediator implementation in .NET. In-process messaging with no dependencies.
Supports request/response, commands, queries, notifications and events, synchronous and async with intelligent dispatching via C# generic variance.
You should install MediatR with NuGet:
Install-Package MediatR
Or via the .NET Core command line interface:
dotnet add package MediatR
AutoMapper به صورت داخلی و با استفاده از قراردادها نمیتونه xml رو به object تبدیل کنه ولی این کار به کمک LINQ to XML قابل انجامه.
مثالی که برای این پست انتخاب شده سوژهی داغ روزهای اخیره ؟!
مدل زیر رو در نظر داشته باشید
public class PreciousMetal { public string Name { get; set; } public float Price { get; set; } public DateTime UpdateTime { get; set; } }
قراره از یک وب سرویس اطلاعات مربوط به فلزات گرانبها رو دریافت و به مدل PreciousMetal نگاشت کنیم.ساختار اطلاعات دریافتی ما به شکل زیره
<pricelist currency="usd"> <price timestamp="1349347920" per="ozt" commodity="gold">1788.70</price> <price timestamp="1349347860" per="ozt" commodity="palladium">665.50</price> <price timestamp="1349347920" per="ozt" commodity="platinum">1701.25</price> <price timestamp="1349347920" per="ozt" commodity="silver">34.91</price> </pricelist>
برای نگاشتهای معمولی کار سختی نداریم و از MapFrom استفاده میکنیم مثلا برای قیمت
Mapper.CreateMap<XElement, PreciousMetal>().ForMember(des => des.Price, op => op.MapFrom(src => src.Value));
ولی برای زمان دریافت قیمت با توجه به متفاوت بودن زمان دریافتی مثلا در اینجا Unix time از Custom value resolvers استفاده میکنیم
public class UnixTimestampResolver : ValueResolver<XElement, DateTime> { protected override DateTime ResolveCore(XElement source) { var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0); return origin.AddSeconds(Convert.ToDouble(source.Attribute("timestamp").Value)); } }
همچنیا میخواهیم از معادل فارسی نام فلزات گرانبها استفاده کنیم
public class EnglishPMetalToFarsiResolver : ValueResolver<XElement, string> { readonly Dictionary<string, string> _pMetaldic = new Dictionary<string, string> { {"gold", "طلا"}, {"palladium", "پالادیوم"}, {"platinum", "پلاتین "}, {"silver", "نقره"} }; protected override string ResolveCore(XElement source) { string pMetalFarsi; return _pMetaldic.TryGetValue(source.Attribute("commodity").Value, out pMetalFarsi) ? pMetalFarsi : string.Empty; } }
نکته:از سری قبلی آشنایی با AutoMapper همیشه بین انتخاب Custom Value Formatters و Custom value resolvers مشکل داشتم مثلا همین قسمت بنظر خودم Custom Value Formatters مناسبتر میاد بعد کمی وقت گذاشتن مشخص شد گویا یه جورایی Custom Value Formatters اضافه س و اشتباه تو طراحی بوده.
و اما نحوه استفاده
static void Main(string[] args) { //تعریف نگاشتها Mapper.CreateMap<XElement, PreciousMetal>().ForMember(des => des.Name, op => op.ResolveUsing<EnglishPMetalToFarsiResolver>()) .ForMember(des => des.Price, op => op.MapFrom(src => src.Value)) .ForMember(des => des.UpdateTime, op => op.ResolveUsing<UnixTimestampResolver>()); Mapper.AssertConfigurationIsValid(); //دریافت قیمتها از منبع داده var doc = XDocument.Load("http://www.xmlcharts.com/cache/precious-metals.xml"); var priceData = doc.Descendants("pricelist").Take(1).Elements("price"); //فراخوانی نگاشت var preciousMetals = Mapper.Map<IEnumerable<XElement>, IList<PreciousMetal>>(priceData); foreach (var preciousMetal in preciousMetals) { Console.WriteLine(preciousMetal.Name + " " + preciousMetal.Price + " " + preciousMetal.UpdateTime.ToShortDateString()); } Console.ReadLine(); }
Want to learn about the latest and greatest in the 64-bit Visual Studio 2022? Join Scott Hanselman and Visual Studio product team as they take Visual Studio 2022 for a spin.
20:27 Profiling .NET apps in Visual Studio 2022
23:19 Cross platform apps with WSL and CMake in Visual Studio 2022
26:07 Testing your .NET app on Linux
28:00 Easily create CI/CD pipelines using GitHub actions with Visual Studio 2022
30:40 Balloon drop!