مطالب
تهیه XML امضاء شده جهت تولید مجوز استفاده از برنامه
اگر به فایل مجوز استفاده از برنامه‌‌ای مانند EF Profiler دقت کنید، یک فایل XML به ظاهر ساده بیشتر نیست:
<?xml version="1.0" encoding="utf-8"?>
<license id="17d46246-a6cb-4196-98a0-ff6fc08cb67f" expiration="2012-06-12T00:00:00.0000000" type="Trial" prof="EFProf">
  <name>MyName</name>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>b8N0bDE4gTakfdGKtzDflmmyyXI=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>IPELgc9BbkD8smXSe0sGqp5vS57CtZo9ME2ZfXSq/thVu...=</SignatureValue>
  </Signature>
</license>
در این فایل ساده متنی، نوع مجوز استفاده از برنامه، Trial ذکر شده است. شاید عنوان کنید که خوب ... نوع مجوز را به Standard تغییر می‌دهیم و فایل را ذخیره کرده و نهایتا استفاده می‌کنیم. اما ... نتیجه حاصل کار نخواهد کرد! حتی اگر یک نقطه به اطلاعات این فایل اضافه شود، دیگر قابل استفاده نخواهد بود. علت آن هم به قسمت Signature فایل XML فوق بر می‌گردد.
در ادامه به نحوه تولید و استفاده از یک چنین مجوزهای امضاء شده‌ای در برنامه‌های دات نتی خواهیم پرداخت.


تولید کلیدهای RSA

برای تهیه امضای دیجیتال یک فایل XML نیاز است از الگوریتم RSA استفاده شود.
برای تولید فایل XML امضاء شده، از کلید خصوصی استفاده خواهد شد. برای خواندن اطلاعات مجوز (فایل XML امضاء شده)، از کلیدهای عمومی که در برنامه قرار می‌گیرند کمک خواهیم گرفت (برای نمونه برنامه EF Prof این کلیدها را در قسمت Resourceهای خود قرار داده است).
استفاده کننده تنها زمانی می‌تواند مجوز معتبری را تولید کند که دسترسی به کلیدهای خصوصی تولید شده را داشته باشد.
        public static string CreateRSAKeyPair(int dwKeySize = 1024)
        {
            using (var provider = new RSACryptoServiceProvider(dwKeySize))
            {
                return provider.ToXmlString(includePrivateParameters: true);
            }
        }
امکان تولید کلیدهای اتفاقی مورد استفاده در الگوریتم RSA، در دات نت پیش بینی شده است. خروجی متد فوق یک فایل XML است که به همین نحو در صورت نیاز توسط متد provider.FromXmlString مورد استفاده قرار خواهد گرفت.


تهیه ساختار مجوز

در ادامه یک enum که بیانگر انواع مجوزهای برنامه ما است را مشاهده می‌کنید:
namespace SignedXmlSample
{
    public enum LicenseType
    {
        None,
        Trial,
        Standard,
        Personal
    }
}
به همراه کلاسی که اطلاعات مجوز تولیدی را دربر خواهد گرفت:
using System;
using System.Xml.Serialization;

namespace SignedXmlSample
{
    public class License
    {
        [XmlAttribute]
        public Guid Id { set; get; }
        
        public string Domain { set; get; }

        [XmlAttribute]
        public string IssuedTo { set; get; }

        [XmlAttribute]
        public DateTime Expiration { set; get; }

        [XmlAttribute]
        public LicenseType Type { set; get; }
    }
}
خواص این کلاس یا عناصر enum یاد شده کاملا دلخواه هستند و نقشی را در ادامه بحث نخواهند داشت؛ از این جهت که از مباحث XmlSerializer برای تبدیل وهله‌ای از شیء مجوز به معادل XML آن استفاده می‌شود. بنابراین المان‌های آن‌را مطابق نیاز خود می‌توانید تغییر دهید. همچنین ذکر ویژگی XmlAttribute نیز اختیاری است. در اینجا صرفا جهت شبیه سازی معادل مثالی که در ابتدای بحث مطرح شد، از آن استفاده شده است. این ویژگی راهنمایی است برای کلاس XmlSerializer تا خواص مزین شده با آن‌را به شکل یک Attribute در فایل نهایی ثبت کند.


تولید و خواندن مجوز دارای امضای دیجیتال

کدهای کامل کلاس تولید و خواندن یک مجوز دارای امضای دیجیتال را در اینجا مشاهده می‌کنید:
using System;
using System.IO;
using System.Security.Cryptography; // needs a ref. to `System.Security.dll` asm.
using System.Security.Cryptography.Xml;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace SignedXmlSample
{
    public static class LicenseGenerator
    {
        public static string CreateLicense(string licensePrivateKey, License licenseData)
        {
            using (var provider = new RSACryptoServiceProvider())
            {
                provider.FromXmlString(licensePrivateKey);
                var xmlDocument = createXmlDocument(licenseData);
                var xmlDigitalSignature = getXmlDigitalSignature(xmlDocument, provider);
                appendDigitalSignature(xmlDocument, xmlDigitalSignature);
                return xmlDocumentToString(xmlDocument);
            }
        }

        public static string CreateRSAKeyPair(int dwKeySize = 1024)
        {
            using (var provider = new RSACryptoServiceProvider(dwKeySize))
            {
                return provider.ToXmlString(includePrivateParameters: true);
            }
        }

        public static License ReadLicense(string licensePublicKey, string xmlFileContent)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xmlFileContent);

            using (var provider = new RSACryptoServiceProvider())
            {
                provider.FromXmlString(licensePublicKey);

                var nsmgr = new XmlNamespaceManager(doc.NameTable);
                nsmgr.AddNamespace("sig", "http://www.w3.org/2000/09/xmldsig#");

                var xml = new SignedXml(doc);
                var signatureNode = (XmlElement)doc.SelectSingleNode("//sig:Signature", nsmgr);
                if (signatureNode == null)
                    throw new InvalidOperationException("This license file is not signed.");

                xml.LoadXml(signatureNode);
                if (!xml.CheckSignature(provider))
                    throw new InvalidOperationException("This license file is not valid.");

                var ourXml = xml.GetXml();
                if (ourXml.OwnerDocument == null || ourXml.OwnerDocument.DocumentElement == null)
                    throw new InvalidOperationException("This license file is coruppted.");

                using (var reader = new XmlNodeReader(ourXml.OwnerDocument.DocumentElement))
                {
                    var xmlSerializer = new XmlSerializer(typeof(License));
                    return (License)xmlSerializer.Deserialize(reader);
                }
            }
        }

        private static void appendDigitalSignature(XmlDocument xmlDocument, XmlNode xmlDigitalSignature)
        {
            xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(xmlDigitalSignature, true));
        }

        private static XmlDocument createXmlDocument(License licenseData)
        {
            var serializer = new XmlSerializer(licenseData.GetType());
            var sb = new StringBuilder();
            using (var writer = new StringWriter(sb))
            {
                var ns = new XmlSerializerNamespaces(); ns.Add("", "");
                serializer.Serialize(writer, licenseData, ns);
                var doc = new XmlDocument();
                doc.LoadXml(sb.ToString());
                return doc;
            }
        }

        private static XmlElement getXmlDigitalSignature(XmlDocument xmlDocument, AsymmetricAlgorithm key)
        {
            var xml = new SignedXml(xmlDocument) { SigningKey = key };
            var reference = new Reference { Uri = "" };
            reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
            xml.AddReference(reference);
            xml.ComputeSignature();
            return xml.GetXml();
        }

        private static string xmlDocumentToString(XmlDocument xmlDocument)
        {
            using (var ms = new MemoryStream())
            {
                var settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 };
                var xmlWriter = XmlWriter.Create(ms, settings);
                xmlDocument.Save(xmlWriter);
                ms.Position = 0;
                return new StreamReader(ms).ReadToEnd();
            }
        }
    }
}
توضیحات:
در حین کار با متد CreateLicense، پارامتر licensePrivateKey همان اطلاعاتی است که به کمک متد CreateRSAKeyPair قابل تولید است. توسط پارامتر licenseData، اطلاعات مجوز در حال تولید اخذ می‌شود. در این متد به کمک provider.FromXmlString، اطلاعات کلیدهای RSA دریافت خواهند شد. سپس توسط متد createXmlDocument، محتوای licenseData دریافتی به یک فایل XML نگاشت می‌گردد (بنابراین اهمیتی ندارد که حتما از ساختار کلاس مجوز یاد شده استفاده کنید). در ادامه متد getXmlDigitalSignature با در اختیار داشتن معادل XML شیء مجوز و کلیدهای لازم، امضای دیجیتال متناظری را تولید می‌کند. با استفاده از متد appendDigitalSignature، این امضاء را به فایل XML اولیه اضافه می‌کنیم. از این امضاء جهت بررسی اعتبار مجوز برنامه در متد ReadLicense استفاده خواهد شد.
برای خواندن یک فایل مجوز امضاء شده در برنامه خود می‌توان از متد ReadLicense استفاده کرد. توسط آرگومان licensePublicKey، اطلاعات کلید عمومی دریافت می‌شود. این کلید دربرنامه، ذخیره و توزیع می‌گردد. پارامتر xmlFileContent معادل محتوای فایل XML مجوزی است که قرار است مورد ارزیابی قرار گیرد.


مثالی در مورد نحوه استفاده از کلاس تولید مجوز

در ادامه نحوه استفاده از متدهای CreateLicense و  ReadLicense را ملاحظه می‌کنید؛ به همراه آشنایی با نمونه کلیدهایی که باید به همراه برنامه منتشر شوند:
using System;
using System.IO;

namespace SignedXmlSample
{
    class Program
    {
        static void Main(string[] args)
        {
            //Console.WriteLine(LicenseGenerator.CreateRSAKeyPair());
            writeLicense();            
            readLicense();

            Console.WriteLine("Press a key...");
            Console.ReadKey();
        }

        private static void readLicense()
        {
            var xml = File.ReadAllText("License.xml");
            const string publicKey = @"<RSAKeyValue>
                                    <Modulus>
                                        mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc=
                                    </Modulus>
                                    <Exponent>
                                        AQAB
                                    </Exponent>
                                 </RSAKeyValue>";

            var result = LicenseGenerator.ReadLicense(publicKey, xml);

            Console.WriteLine(result.Domain);
            Console.WriteLine(result.IssuedTo);
        }

        private static void writeLicense()
        {
            const string rsaData = @"<RSAKeyValue>
                    <Modulus>
                        mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc=
                    </Modulus>
                    <Exponent>
                        AQAB
                    </Exponent>
                    <P>
                        xwPKN77EcolMTD2O2Csv6k9Y4aen8UBVYjeQ4PtrNGz0Zx6I1MxLEFzRpiKC/Ney3xKg0Icwj0ebAQ04d5+HAQ==
                    </P>
                    <Q>
                        w568t0Xe6OBUfCyAuo7tTv4eLgczHntVLpjjcxdUksdVw7NJtlnOLApJVJ+U6/85Z7Ji+eVhuN91yn04pQkAtw==
                    </Q>
                    <DP>
                        svkEjRdA4WP5uoKNiHdmMshCvUQh8wKRBq/D2aAgq9fj/yxlj0FdrAxc+ZQFyk5MbPH6ry00jVWu3sY95s4PAQ==
                    </DP>
                    <DQ>
                        WcRsIUYk5oSbAGiDohiYeZlPTBvtr101V669IUFhhAGJL8cEWnOXksodoIGimzGBrD5GARrr3yRcL1GLPuCEvQ==
                    </DQ>
                    <InverseQ>
                        wIbuKBZSCioG6MHdT1jxlv6U1+Y3TX9sHED9PqGzWWpVGA+xFJmQUxoFf/SvHzwbBlXnG0DLqUvxEv+BkEid2w==
                    </InverseQ>
                    <D>                        Yk21yWdT1BfXqlw30NyN7qNWNuM/Uvh2eaRkCrhvFTckSucxs7st6qig2/RPIwwfr6yIc/bE/TRO3huQicTpC2W3aXsBI9822OOX4BdWCec2txXpSkbZW24moXu+OSHfAdYoOEN6ocR7tAGykIqENshRO7HvONJsOE5+1kF2GAE=
                    </D>
                  </RSAKeyValue>";

            string data = LicenseGenerator.CreateLicense(
                                                rsaData,
                                                new License
                                                {
                                                     Id = Guid.NewGuid(),
                                                     Domain = "dotnettips.info",
                                                     Expiration = DateTime.Now.AddYears(2),
                                                     IssuedTo = "VahidN",
                                                     Type = LicenseType.Standard
                                                });
            File.WriteAllText("License.xml", data);
        }
    }
}
ابتدا توسط متد CreateRSAKeyPair کلیدهای لازم را تهیه و ذخیره کنید. این کار یکبار باید صورت گیرد.
همانطور که مشاهده می‌کنید، اطلاعات کامل یک نمونه از آن، در متد writeLicense مورد نیاز است. اما در متد readLicense تنها به قسمت عمومی آن یعنی Modulus و Exponent نیاز خواهد بود (موارد قابل انتشار به همراه برنامه).

سؤال: امنیت این روش تا چه اندازه است؟
پاسخ: تا زمانیکه کاربر نهایی به کلیدهای خصوصی شما دسترسی پیدا نکند، امکان تولید معادل آن‌ها تقریبا در حد صفر است و به طول عمر او قد نخواهد داد!
اما ... مهاجم می‌تواند کلیدهای عمومی و خصوصی خودش را تولید کند. مجوز دلخواهی را بر این اساس تهیه کرده و سپس کلید عمومی جدید را در برنامه، بجای کلیدهای عمومی شما درج (patch) کند! بنابراین روش بررسی اینکه آیا برنامه جاری patch شده است یا خیر را فراموش نکنید. یا عموما مطابق معمول قسمتی از برنامه که در آن مقایسه‌ای بین اطلاعات دریافتی و اطلاعات مورد انتظار وجود دارد، وصله می‌شوند؛ این مورد عمومی است و منحصر به روش جاری نمی‌باشد.

دریافت نسخه جنریک این مثال:
SignedXmlSample.zip
نظرات مطالب
ASP.NET MVC #21
با سلام.
علت اینکه پارامتر ids مربوط به اکشن delete همواره null  میگیرد چیست؟
@{
    var postUrl = Url.Action(actionName: "Delete", controllerName: "Student");
}
<div class="deleteDialog">
    <div>
        آیتم‌های انتخاب شده حذف خواهند شد. آیا تأیید می‌کنید؟
    </div>
    <p>
        <input type="submit" id="btn_SubmitDelete" value="حذف" />
        <input type="submit" id="btn_CancelDelete" value="انصراف" />
    </p>
</div>
<script type="text/javascript">
    $(function () {
        $("#btn_SubmitDelete").click(function (e) {
            var button = $(this);
            e.preventDefault();
            var data = "1,3,8,9";
            $.ajax({
                type: "POST",
                url: "@postUrl",
                data:  JSON.stringify({ ids: data}),
                contentType: "application/json; charset=utf-8",
                dataType: 'json',
                cache: false,
                beforeSend: function () { },
                success: function (html) {
                    alert(html);
                    $(".deleteDialog").parent("div").css("display", "none");
                },
                complete: function () {
                    button.removeAttr('disabled');
                    button.val("حذف");
                }
            });
        });
        $("#btn_CancelDelete").click(function (e) {
            e.preventDefault();
            var button = $(this);
            $(".deleteDialog").parent("div").css("display", "none");
        });
    });
</script>
[HttpGet]
 public ActionResult Delete()
 {
       return PartialView("Pv_Delete");
 }
 [HttpPost]
 [AjaxOnly]
  public ActionResult Delete(string ids)
  {
            var allIds = ids.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList();
            Thread.Sleep(2000);
           if (true)
           {
                return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet);
           }
            return Json(new { result = "error" });
 }

مطالب دوره‌ها
تبدیل روش‌های قدیمی کدنویسی غیرهمزمان به async سی شارپ 5
در قسمت اول این سری، با مدل برنامه نویسی Event based asynchronous pattern ارائه شده از دات نت 2 و همچنین APM یا Asynchronous programming model موجود از نگارش یک دات نت، آشنا شدیم (به آن الگوی IAsyncResult هم گفته می‌شود). نکته‌ی مهم این الگوها، استفاده‌ی گسترده از آن‌ها در کدهای کلاس‌های مختلف دات نت فریم ورک است و برای بسیاری از آن‌ها هنوز async API سازگار با نگارش مبتنی بر Taskهای سی‌شارپ 5 ارائه نشده‌است. هرچند دات نت 4.5 سعی کرده‌است این خلاء را پوشش دهد، برای مثال متد الحاقی DownloadStringTaskAsync را به کلاس WebClient اضافه کرده‌است و امثال آن، اما هنوز بسیاری از کلاس‌های دیگر دات نتی هستند که معادل Task based API ایی برای آن‌ها طراحی نشده‌است. در ادامه قصد داریم بررسی کنیم چگونه می‌توان این الگوهای مختلف قدیمی برنامه نویسی غیرهمزمان را با استفاده از روش‌های جدیدتر ارائه شده بکار برد.



نگاشت APM به یک Task

در قسمت اول، نمونه مثالی را از APM، که در آن کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجه‌ی عملیات به دست می‌آید، مشاهده کردید. در ادامه می‌خواهیم یک محصور کننده‌ی جدید را برای این نوع API قدیمی تهیه کنیم، تا آن‌را به صورت یک Task ارائه دهد.
    public static class ApmWrapper
    {
        public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count)
        {
            return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null);
        }
    }
همانطور که در این مثال مشاهده می‌کنید، یک چنین سناریوهایی در TPL یا کتابخانه‌ی Task parallel library پیش بینی شده‌اند. در اینجا یک محصور کننده برای متدهای BeginRead و EndRead کلاس Stream دات نت ارائه شده‌است. به عمد نیز به صورت یک متد الحاقی تهیه شده‌است تا در حین استفاده از آن اینطور به نظر برسد که واقعا کلاس Stream دارای یک چنین متد Async ایی است. مابقی کار توسط متد Task.Factory.FromAsync انجام می‌شود. متد FromAsync دارای امضاهای متعددی است تا اکثر حالات APM را پوشش دهد.
در مثال فوق BeginRead و EndRead استفاده شده از نوع delegate هستند. چون خروجی EndRead از نوع int است، خروجی متد نیز از نوع Task of int تعیین شده‌است. همچنین سه پارامتر ابتدایی BeginRead ، دقیقا data، offset و count هستند. دو پارامتر آخر آن callback و state نام دارند. پارامتر callback توسط متد FromAsync فراهم می‌شود و state نیز در اینجا null درنظر گرفته شده‌است.
یک مثال استفاده از آن‌را در ادامه مشاهده می‌کنید:
using System;
using System.IO;
using System.Threading.Tasks;

namespace Async06
{
    public static class ApmWrapper
    {
        public static Task<int> ReadAsync(this Stream stream, byte[] data, int offset, int count)
        {
            return Task<int>.Factory.FromAsync(stream.BeginRead, stream.EndRead, data, offset, count, null);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            using (var stream = File.OpenRead(@"..\..\program.cs"))
            {
                var data = new byte[10000];
                var task = stream.ReadAsync(data, 0, data.Length);
                Console.WriteLine("Read bytes: {0}", task.Result);
            }
        }
    }
}
File.OpenRead، خروجی از نوع استریم دارد. سپس متد الحاقی ReadAsync بر روی آن فراخوانی شده‌است و نهایتا تعداد بایت خوانده شده نمایش داده می‌شود.
البته همانطور که پیشتر نیز عنوان شد، استفاده از خاصیت Result، اجرای کد را بجای غیرهمزمان بودن، به حالت همزمان تبدیل می‌کند.
در اینجا چون خروجی متد ReadAsync یک Task است، می‌توان از متد ContinueWith نیز بر روی آن جهت دریافت نتیجه استفاده کرد:
using (var stream = File.OpenRead(@"..\..\program.cs"))
{
    var data = new byte[10000];
    var task = stream.ReadAsync(data, 0, data.Length);
    task.ContinueWith(t => Console.WriteLine("Read bytes: {0}", t.Result)).Wait();
}


یک نکته
پروژه‌ی سورس بازی به نام Async Generator در GitHub، سعی کرده‌است برای ساده سازی نوشتن محصور کننده‌های مبتنی بر Task روش APM، یک Code generator تولید کند. فایل‌های آن‌را از آدرس ذیل می‌توانید دریافت کنید:

نگاشت EAP به یک Task

نمونه‌ای از Event based asynchronous pattern یا EAP را در قسمت اول، زمانیکه روال رخدادگردان webClient.DownloadStringCompleted را بررسی کردیم، مشاهده نمودید. کار کردن با آن نسبت به APM بسیار ساده‌تر است و نتیجه‌ی نهایی عملیات غیرهمزمان را در یک روال رخدادگران، در اختیار استفاده کننده قرار می‌دهد. همچنین در روش EAP، اطلاعات در همان Synchronization Context ایی که عملیات شروع شده‌است، بازگشت داده می‌شود. به این ترتیب اگر آغاز کار در ترد UI باشد، نتیجه نیز در همان ترد دریافت خواهد شد. به این ترتیب دیگر نگران دسترسی به مقدار آن در کارهای UI نخواهیم بود؛ اما در APM چنین ضمانتی وجود ندارد.
متاسفانه TPL همانند روش FromAsync معرفی شده در ابتدای بحث، راه حل توکاری را برای محصور سازی متدهای روش EAP ارائه نداده‌است. اما با استفاده از امکانات TaskCompletionSource آن می‌توان چنین کاری را انجام داد. در ادامه سعی خواهیم کرد همان متد الحاقی توکار DownloadStringTaskAsync ارائه شده در دات نت 4.5 را از صفر بازنویسی کنیم.
    public static class WebClientExtensions
    {
        public static Task<string> DownloadTextTaskAsync(this WebClient web, string url)
        {
            var tcs = new TaskCompletionSource<string>();

            DownloadStringCompletedEventHandler handler = null;
            handler = (sender, args) =>
            {
                web.DownloadStringCompleted -= handler;

                if (args.Cancelled)
                {
                    tcs.SetCanceled();
                }
                else if(args.Error!=null)
                {
                    tcs.SetException(args.Error);
                }
                else
                {
                    tcs.SetResult(args.Result);
                }
            };

            web.DownloadStringCompleted += handler;
            web.DownloadStringAsync(new Uri(url));

            return tcs.Task;
        }
    }
روش انجام کار را در اینجا ملاحظه می‌کنید. ابتدا باید تعاریف delaget مرتبط با رخدادگردان Completed اضافه شوند. یکبار += را ملاحظه می‌کنید و بار دوم -= را. مورد دوم جهت آزاد سازی منابع و جلوگیری از نشتی حافظه‌ی ‌روال رخدادگردان هنوز متصل، ضروری است.
سپس از TaskCompletionSource برای تبدیل این عملیات به یک Task کمک می‌گیریم. اگر args.Cancelled مساوی true باشد، یعنی عملیات دریافت فایل لغو شده‌است. بنابراین متد SetCanceled منبع Task ایجاد شده را فراخوانی خواهیم کرد. این مورد استثنایی را در کدهای فراخوان سبب می‌شود. به همین دلیل بررسی خطا با یک if else پس از آن انجام شده‌است. برای بازگشت خطای دریافت شده از متد SetException و برای بازگشت نتیجه‌ی واقعی دریافتی، از متد SetResult می‌توان استفاده کرد.
به این ترتیب متد الحاقی غیرهمزمان جدیدی را به نام DownloadTextTaskAsync برای محصور سازی متد EAP ایی به نام DownloadStringAsync و همچنین رخدادگران آن تهیه کردیم.
مطالب
ایجاد رشته Alphanumeric تصادفی در سی شارپ
برای ایجاد یک رشته تصادفی Alphanumeric (شامل حرف و عدد) روشهای زیادی وجود دارد ولی در اینجا به تشریح 2 روش آن اکتفا می‌کنیم.

روش کلی: ابتدا بازه رشته تصادفی مورد نظر را تعیین می‌کنیم. سپس به اندازه طول رشته، اندیس تصادفی ایجاد می‌کنیم و بوسیله آنها کاراکتر تصادفی را از بازه بدست می‌آورم و در انتها کاراکترهای تصادفی را با هم ادغام کرده تا رشته نهایی حاصل شود.
  • روش اول:

ابتدا بازه (char) رشته را مشخص می‌کنیم.

var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
سپس بوسیله LINQ آن را به اندازه طول رشته دلخواه (در این مثال 8کاراکتر) تکرار می‌کنیم و برای انتخاب تصادفی یک کاراکتر در هر بازه (char) تکرار شده از کلاس Random  جهت بدست آوردن اندیس تصادفی بازه استفاده می‌کنیم.
var random = new Random();
var result = new string(
Enumerable.Repeat(chars, 8)
          .Select(s => s[random.Next(s.Length)])
          .ToArray());
توجه: از این روش برای هیچ کدام از موارد مهم و کلیدی مانند ساخت کلمه عبور و توکن استفاده نکنید.
  • روش دوم:

همانند روش اول ابتدا بازه رشته را تعیین می‌کنیم.

char[] chars = new char[62];
chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();
بعد از تعریف بازه، یک سری اعداد تصادفی غیر صفر را بوسیله کلاس RNGCryptoServiceProvider و متد GetNonZeroBytes آن، در متغیری که قرار است بعدا در ایجاد رشته‌ی تصادفی نیاز است پر می‌کنیم.
byte[] data = new byte[maxSize];
RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();
crypto.GetNonZeroBytes(data);
در این مرحله به تعداد طول رشته تصادفی مورد نظر عدد تصادفی بین 0 تا 255 ذخیره شده در متغیر data داریم، برای ایجاد اندیس تصادفی از باقیمانده عدد تصادفی ایجاد شده در مرحله قبل  (byte) به طول بازه (chars.Length) استفاده می‌کنیم سپس کاراکترهای تصادفی را کنار یکدیگر قرار می‌دهیم.
StringBuilder result = new StringBuilder(maxSize);
foreach (byte b in data)
{
  result.Append(chars[b % (chars.Length)]);
}
و در نهایت متد ما جهت ایجاد رشته Alphanumeric در روش دوم به شکل زیر خواهد بود:
public static string GetRandomAlphaNumeric (int maxSize)
{
      char[] chars = new char[62];
      chars ="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".ToCharArray();            
      RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider();            
      byte[] data = new byte[maxSize];
      crypto.GetNonZeroBytes(data);
      StringBuilder result = new StringBuilder(maxSize);
      foreach (byte b in data)
      {
           result.Append(chars[b % (chars.Length)]);
      }
      return result.ToString();
}
لازم به یادآوری است که رشته ایجاد شده در 2روش بیان شده منحصر بفرد نیست بلکه تصادفی است، درواقع تصادفی بودن با منحصر بودن متفاوت است، برای ایجاد رشته‌های منحصر بفرد روشهایی (البته این روشها 100 درصد نیستند ولی از قابلیت اطمینان بالایی برخوردار هستند) وجود دارد که پست‌های بعدی به آنها اشاره خواهم کرد.
مطالب
حذف همزمان چندین رکورد GridView با استفاده از CheckBox در ASP.NET
همانطور که می‌دانید GridView جزء جداناپذیر از اکثر پروژه‌های برنامه نویسان ASP.NET Web forms می‌باشد. اکثرا روشی که در میان برنامه نویسان بیشتر استفاده می‌شود، قرار دادن یک دکمه/لینک در هر ردیف از GridView برای حذف رکورد مورد نظر می‌باشد. در این مقاله  قصد دارم روشی را ارائه کنم تا کاربر قادر باشد هر تعداد رکورد را که مدنظر دارد، انتخاب کرده و با فشردن دکمه "حذف" رکوردهای انتخاب شده را حذف کند. 
برای درک بهتر، ابتدا جدولی به اسم "Emploee" را در SQL Server با مشخصات زیر ساخته :
CREATE TABLE [dbo].[Employee] (  
    [EmpId]     INT          NOT NULL,  
    [FirstName] VARCHAR (20) NOT NULL,  
    [LastName]  VARCHAR (20) NOT NULL,  
    [City]      VARCHAR (20) NOT NULL,  
    PRIMARY KEY CLUSTERED ([EmpId] ASC)  
);
1- یک GridView به صفحه افزوده و خاصیت AutoGenerateColumns  آن را برابر False قرار دهید .  
2- فیلدهایی را که قصد نمایش آنها در GridView را دارید به صورت زیر به GridView بیفزایید :
<asp:BoundField DataField="FirstName" HeaderText="First Name" />
3- برای قرار دادن کنترل‌های Asp.net  که در اینجا منظور CheckBox می‌باشد می‌بایست از TemplateField و قرار دادن تگ ItemTemplate درون آن، به صورت زیر استفاده نمایید :
<asp:TemplateField>  
                    <ItemTemplate>  
                        <asp:CheckBox ID="chkDel" runat="server" />  
                    </ItemTemplate>  
                </asp:TemplateField>
و بعد از تگ GridView دکمه‌ای را برای حذف موارد انتخابی در فرم قرار دهید :
<asp:Button ID="btnDeleteRecord" runat="server" OnClick="btnDeleteRecord_Click" Text="Delete"  />
برای نمایش یک پیغام به کاربر  به منظور Confirm کردن دستور حذف در سمت کلاینت، قطعه کد Javascript زیر را قرار می‌دهیم:
function DeleteConfirm() 
        {  
            var Ans = confirm("Do you want to Delete Selected Employee Record?");  
            if (Ans)
            {  
                return true;  
            }  
            else 
            {  
                return false;  
            }  
        }
و در رویداد Page_Load کدهای زیر را جهت نمایش مقادیر در GridView و افزودن تابع فوق به دکمه، قبل از حذف رکوردها می‌افزاییم :
protected void Page_Load(object sender, EventArgs e)  
{  
    if(!IsPostBack)  
    {  
        //Displaying the Data  
        showData();  
        //Adding an Attribute to Server Control(i.e. btnDeleteRecord)  
        btnDeleteRecord.Attributes.Add("onclick", "javascript:return DeleteConfirm()");  
    }  
}
//Method for Displaying Data  
protected void showData()  
{  
    DataTable dt = new DataTable();  
    SqlConnection con = new SqlConnection(cs);  
    SqlDataAdapter adapt = new SqlDataAdapter("select * from Employee",con);  
    con.Open();  
    adapt.Fill(dt);  
    con.Close();  
    GridView1.DataSource = dt;  
    GridView1.DataBind();  
}
ابتدا تابع  DeleteRecode را به صورت زیر پیاده سازی میکنیم :
که یک پارمتر را از ورودی دریافت میکند که ID رکورد انتخاب شده می‌باشد و با استفاده از ID، رکورد مورد نظر را حذف میکنیم :
protected void DeleteRecord(int empid)  
{  
    SqlConnection con = new SqlConnection(cs);  
    SqlCommand com = new SqlCommand("delete from Employee where EmpId=@ID",con);  
    com.Parameters.AddWithValue("@ID",empid);  
    con.Open();  
    com.ExecuteNonQuery();  
    con.Close();  
}
و اما بخش مهم مربوط به رویداد دکمه می‌باشد. در هنگام کلیک بر روی دکمه باید تمامی رکوردهای GridView را چک و تمامی رکوردهایی را که CheckBox آنها تیک خورده است گرفته و ID رکورد مورد نظر را به تابع DeleteRecode فرستاد و در پایان برای اعمال تغییرات، متد ShowDate را فراخوانی و GridView را مجددا Bind می‌کنیم.
protected void btnDeleteRecord_Click(object sender, EventArgs e)  
{  
    foreach (GridViewRow grow in GridView1.Rows)  
    {  
        //Searching CheckBox("chkDel") in an individual row of Grid  
        CheckBox chkdel = (CheckBox)grow.FindControl("chkDel");  
        //If CheckBox is checked than delete the record with particular empid  
        if(chkdel.Checked)  
        {  
            int empid = Convert.ToInt32(grow.Cells[1].Text);  
            DeleteRecord(empid);  
        }  
    }  
    //Displaying the Data in GridView  
    showData();  
}

مطالب
بررسی بهبودهای ProblemDetails در ASP.NET Core 7x
در زمان ارائه‌ی ASP.NET Core 2.1، ویژگی جدیدی به نام [ApiController] ارائه شد که با استفاده از آن، یکسری اعمال توکار جهت سهولت کار با Web API توسط خود فریم‌ورک انجام می‌شوند؛ برای مثال عدم نیاز به بررسی وضعیت ModelState و بررسی خودکار آن با علامتگذاری یک کنترلر به صورت ApiController. یکی دیگر از این ویژگی‌های توکار، تبدیل خروجی تمام status codeهای بزرگتر و یا مساوی 400 یا همان Bad Request، به شیء جدید و استاندارد ProblemDetails است:
{
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "status": 403,
}
 بازگشت یک چنین خروجی یک‌دست و استانداردی، استفاده‌ی از آن‌را توسط کلاینت‌ها، ساده و قابل پیش‌بینی می‌کند. البته باید درنظر داشت که اگر در این‌حالت، برنامه یک استثنای معمولی را سبب شود، ProblemDetails ای بازگشت داده نمی‌شود. اگر برنامه در حالت توسعه اجرا شود، با استفاده از میان‌افزار app.UseDeveloperExceptionPage، یک صفحه‌ی نمایش جزئیات خطا ظاهر می‌شود و اگر برنامه در حالت تولید و ارائه‌ی نهایی اجرا شود، یک صفحه‌ی خالی (بدون داشتن response body) با status code مساوی 500 بازگشت داده می‌شود. این کمبود ویژه و امکانات سفارشی سازی بیشتر آن، به صورت توکار به ASP.NET Core 7x اضافه شده‌اند و دیگر نیازی به استفاده از کتابخانه‌های ثالث دیگری برای انجام آن نیست.


ProblemDetails بر اساس RFC7807 طراحی شده‌است

RFC7807، قالب استانداردی را برای ارائه‌ی خطاهای HTTP APIها تعریف می‌کند تا نیازی به وجود تعاریف متعددی در این زمینه نباشد و خروجی آن قابل پیش‌بینی و قابل بررسی توسط تمام کلاینت‌های یک API باشد. کلاس ProblemDetails در ASP.NET Core نیز بر همین اساس طراحی شده‌است.
این RFC دو فرمت خروجی را بر اساس مقدار مشخص شده‌ی در هدر Content-Type بازگشت داده شده، مجاز می‌داند:
  • JSON: “application/problem+json” media type
  • XML: “application/problem+xml” media type

که با توجه به این هدر ارسالی، اگر از یک کلاینت از نوع HttpClient استفاده کنیم، می‌توان بر اساس مقدار ویژه‌ی «application/problem+json» تشخیص داد که خروجی API دریافتی، به همراه خطا است و نحوه‌ی پردازش آن به صورت زیر خواهد بود:
var mediaType = response.Content.Headers.ContentType?.MediaType;
if (mediaType != null && mediaType.Equals("application/problem+json", StringComparison.InvariantCultureIgnoreCase))
{
   var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(null, ct) ?? new ProblemDetails();
   // ...
}
در اینجا بدنه‌ی اصلی شیء ProblemDetails بازگشت داده شده، می‌تواند به همراه اعضای زیر باشد:
- type: یک رشته‌است که به آدرس مستندات HTML ای مرتبط با خطای بازگشت داده شده، اشاره می‌کند.
- title: رشته‌ای است که خلاصه‌ی خطای رخ‌داده را بیان می‌کند.
- detail: رشته‌ای است که توضیحات بیشتری را در مورد خطای رخ‌داده، بیان می‌کند.
- instance: رشته‌ای است که به آدرس محل بروز خطا اشاره می‌کند.
- status: عددی است که بیانگر HTTP status code بازگشتی از سمت سرور است.


البته اگر ویژگی ApiController بر روی کنترلرهای خود استفاده نمی‌کنید، می‌توانید این خروجی را به صورت زیر هم با استفاده از return Problem، تولید کنید:
[HttpPost("/sales/products/{sku}/availableForSale")]
public async Task<IActionResult> AvailableForSale([FromRoute] string sku)
{
   return Problem(
            "Product is already Available For Sale.",
            "/sales/products/1/availableForSale",
            400,
            "Cannot set product as available.",
            "http://example.com/problems/already-available");
}


امکان افزودن اعضای سفارشی به شیء ProblemDetails

امکان بسط این خروجی، با افزودن اعضای سفارشی نیز پیش‌بینی شده‌است. یک نمونه‌ی متداول و پرکاربرد آن، بازگشت خطاهای مرتبط با اعتبارسنجی اطلاعات رسیده‌است:
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
Content-Language: en
{
    "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
    "title": "One or more validation errors occurred.",
    "status": 400,
    "errors": {
        "User": [
            "The user name is not verified."
        ]
    }
}
در اینجا عضو جدید errors را بنابر نیاز این مساله‌ی خاص، مشاهده می‌کنید که در صورت استفاده از ویژگی ApiController بر روی کنترلرهای Web API، به صورت خودکار توسط ASP.NET Core تولید می‌شود و نیازی به تنظیم خاصی و یا کدنویسی اضافه‌تری ندارد. کلاس مخصوص آن نیز ValidationProblemDetails‌ است.


جهت افزودن اعضای سفارشی دیگری به شیء ProblemDetails می‌توان به صورت زیر عمل کرد:
namespace WebApplication.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class DemoController : ControllerBase
    {
        [HttpPost]
        public ActionResult Post()
        {
            var problemDetails = new ProblemDetails
            {
                Detail = "The request parameters failed to validate.",
                Instance = null,
                Status = 400,
                Title = "Validation Error",
                Type = "https://example.net/validation-error",
            };

            problemDetails.Extensions.Add("invalidParams", new List<ValidationProblemDetailsParam>()
            {
                new("name", "Cannot be blank."),
                new("age", "Must be great or equals to 18.")
            });

            return new ObjectResult(problemDetails)
            {
                StatusCode = 400
            };
        }
    }

    public class ValidationProblemDetailsParam
    {
        public ValidationProblemDetailsParam(string name, string reason)
        {
            Name = name;
            Reason = reason;
        }

        public string Name { get; set; }
        public string Reason { get; set; }
    }
}
شیء ProblemDetails، به همراه خاصیت Extensions است که می‌توان به آن یک <Dictionary<string, object را انتساب داد و نمونه‌ای از آن‌را در مثال فوق مشاهده می‌کنید. این مثال سبب می‌شود تا عضو جدیدی با کلید دلخواه invalidParams، به همراه لیستی از name و reasonها به خروجی نهایی اضافه شود. مقدار این کلید، از نوع object است؛ یعنی هر شیء دلخواهی را در اینجا می‌توان تعریف و استفاده کرد.


معرفی سرویس جدید ProblemDetails در دات نت 7

در دات نت 7 می‌توان سرویس‌های جدید ProblemDetails را به نحو زیر به برنامه اضافه کرد:
services.AddProblemDetails();
پس از آن به 3 روش مختلف می‌توان از امکانات این سرویس‌ها استفاده کرد:
الف) با اضافه کردن میان‌افزار مدیریت خطاها
app.UseExceptionHandler();
پس از آن، هر استثنای مدیریت نشده‌ای نیز به صورت یک ProblemDetails ظاهر می‌شود و دیگر همانند قبل، سبب نمایش یک صفحه‌ی خالی نخواهد شد.

ب) با افزودن میان‌افزار StatusCodePages
app.UseStatusCodePages();
در این حالت مواردی که استثناء شمرده نمی‌شوند مانند 404، در صورت بروز رسیدن به یک مسیریابی یافت نشده و یا 405، در صورت درخواست یک HTTP method غیرمعتبر نیز توسط یک ProblemDetails استاندارد مدیریت می‌شوند.

ج) با افزودن میان‌افزار صفحه‌ی استثناءهای توسعه دهنده‌ها
app.UseDeveloperExceptionPage();
به این ترتیب در خروجی ProblemDetails، اطلاعات بیشتری از استثناء رخ‌داده، مانند استک‌تریس آن ظاهر خواهد شد.


امکان بازگشت ساده‌تر یک ProblemDetails سفارشی در دات نت 7

برای سفارشی سازی خروجی ProblemDetails، علاوه بر راه‌حلی که پیشتر در این مطلب مطرح شد، می‌توان در دات نت 7 از روش تکمیلی ذیل نیز استفاده کرد:
builder.Services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
            ctx.ProblemDetails.Extensions.Add("MachineName", Environment.MachineName));
به این ترتیب در صورت لزوم می‌توان یک عضو سفارشی سراسری را به تمام اشیاء ProblemDetails برنامه به صورت خودکار اضافه کرد و یا اگر می‌خواهیم این مورد را کمی اختصاصی‌تر کنیم، می‌توان به صورت زیر عمل کرد:

الف) تعریف یک ErrorFeature سفارشی
public class MyErrorFeature
{
    public ErrorType Error  { get; set; }
}
​
public enum ErrorType
{
    ArgumentException
}
در ASP.NET Core می‌توان به شیء HttpContext.Features قابل تنظیم در هر اکشن متدی، اشیاء دلخواهی را مانند شیء سفارشی فوق، اضافه کرد و سپس در قسمت options.CustomizeProblemDetails تنظیماتی که ذکر شد، به دریافت و تنظیم آن، واکنش نشان داد.

ب) تنظیم مقدار ErrorFeature سفارشی در اکشن متدها
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            var errorType = new MyErrorFeature
            {
                Error = ErrorType.ArgumentException
            };
            HttpContext.Features.Set(errorType);
            return BadRequest();
        }
​
        return Ok(value);
    }
پس از تعریف شیءایی که قرار است به HttpContext.Features اضافه شود، اکنون روش تنظیم و مقدار دهی آن‌را در یک اکشن متد، در مثال فوق مشاهده می‌کنید.

ج) واکنش نشان دادن به دریافت ErrorFeature سفارشی
services.AddProblemDetails(options =>
    options.CustomizeProblemDetails = ctx =>
    {
        var MyErrorFeature = ctx.HttpContext.Features.Get<MyErrorFeature>();
​
        if (MyErrorFeature is not null)
        {
            (string Title, string Detail, string Type) details = MyErrorFeature.Error switch
            {
                ErrorType.ArgumentException =>
                (
                    nameof(ArgumentException),
                    "This is an argument-exception.",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1"
                ),
                _ =>
                (
                    nameof(Exception),
                    "default-exception",
                    "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1"
                )
            };
​
            ctx.ProblemDetails.Title = details.Title;
            ctx.ProblemDetails.Detail = details.Detail;
            ctx.ProblemDetails.Type = details.Type;
        }
    }
);
پس از تنظیم HttpContext.Features در اکشن متدی، می‌توان در options.CustomizeProblemDetails فوق، توسط متد ctx.HttpContext.Features.Get به آن شیء خاص تنظیم شده، در صورت وجود دسترسی یافت و سپس جزئیات بیشتری را از آن استخراج و مقادیر ctx.ProblemDetails جاری را که قرار است به کاربر بازگشت داده شوند، بازنویسی کرد و یا تغییر داد.
 

امکان تبدیل ساده‌تر اطلاعات استثناءهای سفارشی به یک ProblemDetails سفارشی در دات نت 7

بجای استفاده از تنظیمات services.AddProblemDetails جهت بازنویسی مقدار شیء ProblemDetails بازگشتی، می‌توان جزئیات میان‌افزار app.UseExceptionHandler را نیز سفارشی سازی کرد و به بروز استثناءهای خاصی واکنش نشان داد. برای مثال فرض کنید یک استثنای سفارشی را به صورت زیر طراحی کرده‌اید:
public class MyCustomException : Exception
{
    public MyCustomException(
        string message,
        HttpStatusCode statusCode = HttpStatusCode.BadRequest
    ) : base(message)
    {
        StatusCode = statusCode;
    }
​
    public HttpStatusCode StatusCode { get; }
}
و سپس در اکشن متدی، سبب بروز آن شده‌اید:
    [HttpGet("{value}")]
    public IActionResult MyErrorTest(int value)
    {
        if (value <= 0)
        {
            throw new MyCustomException("The value should be positive!");
        }
​
        return Ok(value);
    }
اکنون می‌توان در میان‌افزار مدیریت استثناءهای برنامه، نسبت به مدیریت این استثناء خاص، واکشن نشان داد و ProblemDetails متناظری را تولید و بازگشت داد:
app.UseExceptionHandler(exceptionHandlerApp =>
{
    exceptionHandlerApp.Run(async context =>
    {
        context.Response.ContentType = "application/problem+json";
​
        if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
        {
            var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = exceptionHandlerFeature?.Error;
​
            if (exceptionType is not null)
            {
                (string Title, string Detail, string Type, int StatusCode) details = exceptionType switch
                {
                    MyCustomException MyCustomException =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
                        context.Response.StatusCode = (int)MyCustomException.StatusCode
                    ),
                    _ =>
                    (
                        exceptionType.GetType().Name,
                        exceptionType.Message,
                        "https://www.rfc-editor.org/rfc/rfc7231#section-6.6.1",
                        context.Response.StatusCode = StatusCodes.Status500InternalServerError
                    )
                };
​
                await problemDetailsService.WriteAsync(new ProblemDetailsContext
                {
                    HttpContext = context,
                    ProblemDetails =
                    {
                        Title = details.Title,
                        Detail = details.Detail,
                        Type = details.Type,
                        Status = details.StatusCode
                    }
                });
            }
        }
    });
});
​
در اینجا نحوه‌ی کار با سرویس توکار IProblemDetailsService و سپس دسترسی به IExceptionHandlerFeature و استثنای صادر شده را مشاهده می‌کنید. پس از آن بر اساس نوع و اطلاعات این استثناء، می‌توان یک ProblemDetails مخصوص را تولید و در خروجی ثبت کرد.
مطالب
کار با Kendo UI DataSource
Kendo UI DataSource جهت تامین داده‌های سمت کلاینت ویجت‌های مختلف KendoUI طراحی شده‌است و به عنوان یک اینترفیس استاندارد قابل استفاده توسط تمام کنترل‌های داده‌ای Kendo UI کاربرد دارد. Kendo UI DataSource امکان کار با منابع داده محلی، مانند اشیاء و آرایه‌های جاوا اسکریپتی و همچنین منابع تامین شده از راه دور، مانند JSON، JSONP و XML را دارد. به علاوه توسط آن می‌توان اعمال ثبت، ویرایش و حذف اطلاعات، به همراه صفحه بندی، گروه بندی و مرتب سازی داده‌ها را کنترل کرد.


استفاده از منابع داده محلی
در ادامه مثالی را از نحوه‌ی استفاده از یک منبع داده محلی جاوا اسکریپتی، مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var cars = [
                { "Year": 2000, "Make": "Hyundai", "Model": "Elantra" },
                { "Year": 2001, "Make": "Hyundai", "Model": "Sonata" },
                { "Year": 2002, "Make": "Toyota", "Model": "Corolla" },
                { "Year": 2003, "Make": "Toyota", "Model": "Yaris" },
                { "Year": 2004, "Make": "Honda", "Model": "CRV" },
                { "Year": 2005, "Make": "Honda", "Model": "Accord" },
                { "Year": 2000, "Make": "Honda", "Model": "Accord" },
                { "Year": 2002, "Make": "Kia", "Model": "Sedona" },
                { "Year": 2004, "Make": "Fiat", "Model": "One" },
                { "Year": 2005, "Make": "BMW", "Model": "M3" },
                { "Year": 2008, "Make": "BMW", "Model": "X5" }
            ];

            var carsDataSource = new kendo.data.DataSource({
                data: cars
            });

            carsDataSource.read();
            alert(carsDataSource.total());
        });
    </script>
در اینجا cars آرایه‌ای از اشیاء جاوا اسکریپتی بیانگر ساختار یک خودرو است. سپس برای معرفی آن به Kendo UI، کار با مقدار دهی خاصیت data مربوط به new kendo.data.DataSource شروع می‌شود.
ذکر new kendo.data.DataSource به تنهایی به معنای مقدار دهی اولیه است و در این حالت منبع داده مورد نظر، استفاده نخواهد شد. برای مثال اگر متد total آن‌را جهت یافتن تعداد عناصر موجود در آن فراخوانی کنید، صفر را بازگشت می‌دهد. برای شروع به کار با آن، نیاز است ابتدا متد read را بر روی این منبع داده مقدار دهی شده، فراخوانی کرد.


استفاده از منابع داده راه دور

در برنامه‌های کاربردی، عموما نیاز است تا منبع داده را از یک وب سرور تامین کرد. در اینجا نحوه‌ی خواندن اطلاعات JSON بازگشت داده شده از جستجوی توئیتر را مشاهده می‌کنید:
    <script type="text/javascript">
        $(function () {
            var twitterDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "http://search.twitter.com/search.json",
                        dataType: "jsonp",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET',
                        data: { q: "#kendoui" }
                    },
                    schema: { data: "results" }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
در قسمت transport، جزئیات تبادل اطلاعات با سرور راه دور مشخص می‌شود؛ برای مثال url ارائه دهنده‌ی سرویس، dataType بیانگر نوع داده مورد انتظار و data کار مقدار دهی پارامتر مورد انتظار توسط سرویس توئیتر را انجام می‌دهد. در اینجا چون صرفا عملیات خواندن اطلاعات صورت می‌گیرد، خاصیت read مقدار دهی شده‌است.
در قسمت schema مشخص می‌کنیم که اطلاعات JSON بازگشت داده شده توسط توئیتر، در فیلد results آن قرار دارد.


کار با منابع داده OData

علاوه بر فرمت‌های یاد شده، Kendo UI DataSource امکان کار با اطلاعاتی از نوع OData را نیز دارا است که تنظیمات ابتدایی آن به صورت ذیل است:
    <script type="text/javascript">
            var moviesDataSource = new kendo.data.DataSource({
                type: "odata",
                transport: {
                    read: "http://demos.kendoui.com/service/Northwind.svc/Orders"
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                }
            });
        });
    </script>
همانطور که ملاحظه می‌کنید، تنظیمات ابتدایی آن اندکی با حالت remote data پیشین متفاوت است. در اینجا ابتدا نوع داده‌ی بازگشتی مشخص می‌شود و در قسمت transport، خاصیت read آن، آدرس سرویس را دریافت می‌کند.


یک مثال: دریافت اطلاعات از ASP.NET Web API

یک پروژه‌ی جدید ASP.NET را آغاز کنید. تفاوتی نمی‌کند که Web forms باشد یا MVC؛ از این جهت که مباحث Web API در هر دو یکسان است.
سپس یک کنترلر جدید Web API را به نام ProductsController با محتوای زیر ایجاد کنید:
using System.Collections.Generic;
using System.Web.Http;

namespace KendoUI02
{
    public class Product
    {
        public int Id { set; get; }
        public string Name { set; get; }
    }

    public class ProductsController : ApiController
    {
        public IEnumerable<Product> Get()
        {
            var products = new List<Product>();
            for (var i = 1; i <= 100; i++)
            {
                products.Add(new Product { Id = i, Name = "Product " + i });
            }
            return products;
        }
    }
}
در این مثال، هدف صرفا ارائه یک خروجی ساده JSON از طرف سرور است.
در ادامه نیاز است تعریف مسیریابی ذیل نیز به فایل Global.asax.cs برنامه اضافه شود تا بتوان به آدرس api/products در سایت، دسترسی یافت:
using System;
using System.Web.Http;
using System.Web.Routing;

namespace KendoUI02
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
        }
    }
}

در ادامه فایلی را به نام Index.html (یا در یک View و یا یک فایل aspx دلخواه)، محتوای ذیل را اضافه کنید:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>Kendo UI: Implemeting the Grid</title>

    <link href="styles/kendo.common.min.css" rel="stylesheet" type="text/css" />
    <link href="styles/kendo.default.min.css" rel="stylesheet" type="text/css" />
    <script src="js/jquery.min.js" type="text/javascript"></script>
    <script src="js/kendo.all.min.js" type="text/javascript"></script>
</head>
<body>

    <div id="report-grid"></div>
    <script type="text/javascript">
        $(function () {
            var productsDataSource = new kendo.data.DataSource({
                transport: {
                    read: {
                        url: "api/products",
                        dataType: "json",
                        contentType: 'application/json; charset=utf-8',
                        type: 'GET'
                    }
                },
                error: function (e) {
                    alert(e.errorThrown.stack);
                },
                pageSize: 5,
                sort: { field: "Id", dir: "desc" }
            });

            $("#report-grid").kendoGrid({
                dataSource: productsDataSource,
                autoBind: true,
                scrollable: false,
                pageable: true,
                sortable: true,
                columns: [
                    { field: "Id", title: "#" },
                    { field: "Name", title: "Product" }
                ]
            });
        });
    </script>
</body>
</html>
- ابتدا فایل‌های اسکریپت و CSS مورد نیاز Kendo UI اضافه شده‌اند.
- گرید صفحه، در محل div ایی با id مساوی report-grid تشکیل خواهد شد.
- سپس DataSource ایی که به آدرس api/products اشاره می‌کند، تعریف شده و در آخر productsDataSource را توسط یک kendoGrid نمایش داده‌ایم.
- نحوه‌ی تعریف productsDataSource، در قسمت استفاده از منابع داده راه دور ابتدای بحث توضیح داده شد. در اینجا فقط دو خاصیت pageSize و sort نیز به آن اضافه شده‌اند. این دو خاصیت بر روی نحوه‌ی نمایش گرید نهایی تاثیر گذار هستند. pageSize تعداد رکورد هر صفحه را مشخص می‌کند و sort نحوه‌ی مرتب سازی را بر اساس فیلد Id و در حالت نزولی قرار می‌دهد.
- در ادامه، ابتدایی‌ترین حالت کار با kendoGrid را ملاحظه می‌کنید.
- تنظیم dataSource و autoBind: true (حالت پیش فرض)، سبب خواهند شد تا به صورت خودکار، اطلاعات JSON از مسیر api/products خوانده شوند.
- سه خاصیت بعدی صفحه بندی و مرتب سازی خودکار ستون‌ها را فعال می‌کنند.
- در آخر هم دو ستون گرید، بر اساس نام‌های خواص کلاس Product تعریف شده‌اند.


سورس کامل این قسمت را از اینجا می‌توانید دریافت کنید:
KendoUI02.zip
 
مطالب
روشی برای محدود کردن API ها که هر درخواست با یک Key جدید و منحصر به فرد قابل فراخوانی باشد ( Time-based One-time Password )
TOTPیک الگوریتمی است که از ساعت برای تولید رمزهای یکبارمصرف استفاده میکند. به این صورت که در هر لحظه یک کد منحصر به فرد تولید خواهد شد. اگر با برنامه Google Authenticator کار کرده باشید این مفهوم برایتان اشناست. 
در این مطلب میخواهیم سناریویی را پیاده سازی کنیم که برای فراخوانی API‌ها باید یک رمز منحصر به فرد همراه توکن ارسال کنند. برای انجام این کار هر کاربر و یا کلاینتی که بخواهد از API استفاده کند در ابتدا باید لاگین کند و بعد از لاگین یک ClientSecret به طول 16 کاراکتر به همراه توکن به او ارسال میکنیم. از ClientSecret برای رمزنگاری کد ارسال شده ( TOTP ) استفاده میکنیم. مانند Public/Private key یک Key ثابت در سمت سرور و یک Key ثابت در برنامه موبایل وجود دارد و هنگام رمزنگاری TOTP علاوه بر Key موجود در موبایل و سرور از ClientSecret خود کاربر هم استفاده میکنیم تا TOTP بوجود آمده کاملا منحصر به فرد باشد. ( مقدار TOTP را با استفاده از دو کلید Key و ClientSecret رمزنگاری میکنیم ).
در این مثال تاریخ فعلی را ( UTC )  قبل از فراخوانی API در موبایل میگیریم و Ticks آن را در یک مدل ذخیره میکنیم. سپس مدل که شامل تایم فعلی میباشد را سریالایز میکنیم و به string تبدیل میکنیم سپس رشته بدست آمده را با استفاده از الگوریتم AES رمزنگاری میکنیم با استفاده از Key ثابت و ClientSecret. سپس این مقدار بدست آمده را در هدر Request-Key قرار میدهیم. توجه داشته باشید باید ClientSecret هم در یک هدر دیگر به سمت سرور ارسال شود زیرا با استفاده از این ClientSecret عملیات رمزگشایی مهیا میشود. به عنوان مثال ساعت فعلی به صورت Ticks به این صورت میباشد "637345971787256752 ". این عدد از نوع long است و با گذر زمان مقدار آن بیشتر میشود. در نهایت مدل نهایی سریالایز شده به این صورت است :
{"DateTimeUtcTicks":637345812872371593}
و مقدار رمزنگاری شده برابر است با :
g/ibfD2M3uE1RhEGxt8/jKcmpW2zhU1kKjVRC7CyrHiCHkdaAmLOwziBATFnHyJ3
مدل ارسالی شامل یک پراپرتی به نام DateTimeUtcTicks است که از نوع long میباشد و این مدل را قبل از فراخوانی API ایجاد میکند و مقدار رمزنگاری شده بدست آمده را در هدر Request-Key قرار میدهد به همراه ClientSecret در هدر مربوط به ClientSecret. این عمل در سمت موبایل باید انجام شود و در سمت سرور باید با استفاده از ClientSecret ارسال شده و Key ثابت در سرور این هدر را رمزگشایی کنند. چون این کار زیاد وقت گیر نیست و نهایتا یک دقیقه اختلاف زمان بین درخواست ارسال شده و زمان دریافت درخواست در سرور وجود دارد, در سمت سرور مقدار بدست آمده ( مقدار ارسال شده DateTimeUtcTicks که رمزگشایی شده است ) را اینگونه حساب میکنیم که مقدار ارسال شده از یک دقیقه قبل زمان فعلی باید بیشتر یا مساوی باشد و از تاریخ فعلی باید کوچکتر مساوی باشد. به این صورت 
var dateTimeNow = DateTime.UtcNow;
var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
var expireTimeTo = dateTimeNow.Ticks;

string clientSecret = httpContext.Request.Headers["ClientSecret"].ToString();
string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
و در نهایت اگر مقدار ارسال شده در بین این بازه باشد به معنی معتبر بودن درخواست میباشد در غیر این صورت باید خطای مربوطه را به کاربر نمایش دهیم. در ادامه برای پیاده  پیاده سازی این سناریو از یک Middleware استفاده کرده‌ایم که ابتدا بررسی میکند آیا درخواست ارسال شده حاوی هدر Request-Key و ClientSecret میباشد یا خیر؟ اگر هدر خالی باشد یا مقدار هدر نال باشد، خطای 403 را به کاربر نمایش میدهیم. برای جلوگیری از استفاده‌ی مجدد از هدر رمزنگاری شده، هنگامیکه اولین درخواست به سمت سرور ارسال میشود، رشته‌ی رمزنگاری شده را در کش ذخیره میکنیم و اگر مجددا همان رشته را ارسال کند، اجازه‌ی دسترسی به API را به او نخواهیم داد. کش را به مدت 2 دقیقه نگه میداریم؛ چون برای هر درخواست نهایتا یک دقیقه اختلاف زمانی را در نظر گرفته‌ایم. 
 در ادامه اگر رشته‌ی رمزنگاری شده در کش موجود باشد، مجددا پیغام "Forbidden: You don't have permission to call this api" را به کاربر نمایش میدهیم؛ زیرا به این معناست که رشته‌ی رمزنگاری شده قبلا ارسال شده است. سپس رشته رمزنگاری شده را رمزگشایی میکنیم و به مدل ApiLimiterDto دیسریالایز میکنیم و بررسی میکنیم که مقدار Ticks ارسال شده از طرف موبایل، از یک دقیقه قبل بیشتر بوده و از زمان حال کمتر باشد. اگر در بین این دو بازه باشد، یعنی درخواست معتبر هست و اجازه فراخوانی API را دارد؛ در غیر این صورت پیغام 403 را به کاربر نمایش میدهیم.
مدل ApiLimiterDto   :
public class ApiLimiterDto
{
    public long DateTimeUtcTicks { get; set; }
}
میان افزار :
public class ApiLimiterMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IDistributedCache _cache;

    public ApiLimiterMiddleware(RequestDelegate next, IDistributedCache cache)
    {
        _next = next;
        _cache = cache;
    }
    private const string requestKey = "Request-Key";
    private const string clientSecretHeader = "ClientSecret";
    public async Task InvokeAsync(HttpContext httpContext)
    {
        if (!httpContext.Request.Headers.ContainsKey(requestKey) || !httpContext.Request.Headers.ContainsKey(clientSecretHeader))
        {
            await WriteToReponseAsync();
            return;
        }

        var requestKeyHeader = httpContext.Request.Headers[requestKey].ToString();
        string clientSecret = httpContext.Request.Headers[clientSecretHeader].ToString();
        if (string.IsNullOrEmpty(requestKeyHeader) || string.IsNullOrEmpty(clientSecret))
        {
            await WriteToReponseAsync();
            return;
        }
        //اگر کلید در کش موجود بود یعنی کاربر از کلید تکراری استفاده کرده است
        if (_cache.GetString(requestKeyHeader) != null)
        {
            await WriteToReponseAsync();
            return;
        }
        var dateTimeNow = DateTime.UtcNow;
        var expireTimeFrom = dateTimeNow.AddMinutes(-1).Ticks;
        var expireTimeTo = dateTimeNow.Ticks;

        string decryptedRequestHeader = AesProvider.Decrypt(requestKeyHeader, clientSecret);
        var requestKeyData = System.Text.Json.JsonSerializer.Deserialize<ApiLimiterDto>(decryptedRequestHeader);

        if (requestKeyData.DateTimeUtcTicks >= expireTimeFrom && requestKeyData.DateTimeUtcTicks <= expireTimeTo)
        {
            //ذخیره کلید درخواست در کش برای جلوگیری از استفاده مجدد از کلید
            await _cache.SetAsync(requestKeyHeader, Encoding.UTF8.GetBytes("KeyExist"), new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(2)
            });
            await _next(httpContext);
        }
        else
        {
            await WriteToReponseAsync();
            return;
        }

        async Task WriteToReponseAsync()
        {
            httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
            await httpContext.Response.WriteAsync("Forbidden: You don't have permission to call this api");
        }
    }
}
برای رمزنگاری و رمزگشایی، یک کلاس را به نام AesProvider ایجاد کرده‌ایم که عملیات رمزنگاری و رمزگشایی را فراهم میکند.
public static class AesProvider
{
    private static byte[] GetIV()
    {
        //این کد ثابتی است که باید در سمت سرور و موبایل موجود باشد
        return encoding.GetBytes("ThisIsASecretKey");
    }
    public static string Encrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESEncrypt = aes.CreateEncryptor(aes.Key, aes.IV);
            byte[] buffer = encoding.GetBytes(plainText);
            string encryptedText = Convert.ToBase64String(AESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length));
            return encryptedText;
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when encrypting");
        }
    }
    private static RijndaelManaged GetRijndael(string key)
    {
        return new RijndaelManaged
        {
            KeySize = 128,
            BlockSize = 128,
            Padding = PaddingMode.PKCS7,
            Mode = CipherMode.CBC,
            Key = encoding.GetBytes(key),
            IV = GetIV()
        };
    }
    private static readonly Encoding encoding = Encoding.UTF8;
    public static string Decrypt(string plainText, string key)
    {
        try
        {
            var aes = GetRijndael(key);
            ICryptoTransform AESDecrypt = aes.CreateDecryptor(aes.Key, aes.IV);
            byte[] buffer = Convert.FromBase64String(plainText);

            return encoding.GetString(AESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length));
        }
        catch (Exception)
        {
            throw new Exception("an error occurred when decrypting");
        }
    }
}
متد Decrypt و Encrypt یک ورودی به نام key دریافت میکنند که از هدر ClientSecret دریافت میشود. در سمت سرور عملا عمل Decrypt انجام میشود و Encrypt برای این مثال در سمت سرور کاربردی ندارد. 
برای رمزنگاری با استفاده از روش AES، چون از 128 بیت استفاده کرده‌ایم، باید طول متغییر key برابر 16 کاراکتر باشد و IV هم باید کمتر یا برابر 16 کاراکتر باشد.
در نهایت برای استفاده از این میان افزار میتوانیم از MiddlewareFilter استفاده کنیم که برای برخی از api‌های مورد نظر از آن استفاده کنیم.
کلاس ApiLimiterPipeline :
public class ApiLimiterPipeline
{
    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<ApiLimiterMiddleware>();
    }
}
نحوه استفاده از میان افزار برای یک اکشن خاص :
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
    [MiddlewareFilter(typeof(ApiLimiterPipeline))]
    public async Task<IActionResult> Get()
    {
        return Ok("Hi");
    }
}

مطالب
Blazor 5x - قسمت 17 - کار با فرم‌ها - بخش 5 - آپلود تصاویر
از زمان Blazor 5x، پشتیبانی توکار از آپلود فایل‌ها، به آن اضافه شده‌است و پیش از آن می‌بایستی از کامپوننت‌های ثالث استفاده می‌شد. در این قسمت نحوه‌ی استفاده از کامپوننت آپلود فایل‌های Blazor را بررسی می‌کنیم. همچنین یک نمونه مثال، از فرم‌های master-details را نیز با هم مرور خواهیم کرد.



افزودن فیلد آپلود تصاویر، به فرم ثبت اطلاعات یک اتاق

در ادامه به کامپوننت Pages\HotelRoom\HotelRoomUpsert.razor که تا این قسمت آن‌را تکمیل کرده‌ایم مراجعه کرده و فیلد جدید InputFile را ذیل قسمت ثبت توضیحات، اضافه می‌کنیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
</div>

@code
{
    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {

    }
}
- ذکر ویژگی multiple در اینجا سبب می‌شود تا بتوان بیش از یک فایل را هربار انتخاب و آپلود کرد.
- در این کامپوننت، رویداد OnChange، پس از تغییر مجموعه‌ی فایل‌های اضافه شده‌ی به آن، فراخوانی می‌شود و آرگومانی از نوع InputFileChangeEventArgs را دریافت می‌کند.


افزودن لیست فایل‌های انتخابی به HotelRoomDTO

تا اینجا اگر به BlazorServer.Models\HotelRoomDTO.cs مراجعه کنیم (کلاسی که مدل UI فرم ثبت اطلاعات اتاق را فراهم می‌کند)، امکان افزودن لیست تصاویر انتخابی به آن وجود ندارد. به همین جهت در این کلاس، تغییر زیر را اعمال می‌کنیم:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace BlazorServer.Models
{
    public class HotelRoomDTO
    {
        // ... 
        public virtual ICollection<HotelRoomImageDTO> HotelRoomImages { get; set; } = new List<HotelRoomImageDTO>();
    }
}
HotelRoomImageDTO را در قسمت قبل اضافه کردیم. متناظر با ICollection فوق، چنین خاصیتی در موجودیت HotelRoom که از نوع <ICollection<HotelRoomImage است نیز تعریف شده‌است تا بتوان به ازای هر اتاق، مشخصات تعدادی تصویر را در بانک اطلاعاتی ذخیره کرد.


تکمیل متد رویدادگردان HandleImageUpload

در ادامه، لیست فایل‌ها‌ی انتخاب شده‌ی توسط کاربر را دریافت کرده و آن‌ها را آپلود می‌کنیم:
@inject IHotelRoomService HotelRoomService
@inject NavigationManager NavigationManager
@inject IJSRuntime JsRuntime
@inject IFileUploadService FileUploadService
@inject IWebHostEnvironment WebHostEnvironment

@code
{
    // ...

    private async Task HandleImageUpload(InputFileChangeEventArgs args)
    {
        var files = args.GetMultipleFiles(maximumFileCount: 5);
        if (args.FileCount == 0 || files.Count == 0)
        {
            return;
        }

        var allowedExtensions = new List<string> { ".jpg", ".png", ".jpeg" };
        if(!files.Any(file => allowedExtensions.Contains(Path.GetExtension(file.Name), StringComparer.OrdinalIgnoreCase)))
        {
            await JsRuntime.ToastrError("Please select .jpg/.jpeg/.png files only.");
            return;
        }

        foreach (var file in files)
        {
            var uploadedImageUrl = await FileUploadService.UploadFileAsync(file, WebHostEnvironment.WebRootPath, "Uploads");
            HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
        }
    }
}
- در اینجا نیاز به تزریق چند سرویس جدید هست؛ مانند IFileUploadService که در قسمت قبل تکمیل کردیم و سرویس توکار IWebHostEnvironment. به همین جهت به فایل BlazorServer.App\_Imports.razor مراجعه کرده و فضاهای نام متناظر زیر را اضافه می‌کنیم:
@using Microsoft.AspNetCore.Hosting
@using System.Linq
@using System.IO
برای مثال سرویس IWebHostEnvironment که از آن برای دسترسی به WebRootPath یا محل قرارگیری پوشه‌ی wwwroot استفاده می‌کنیم، در فضای نام Microsoft.AspNetCore.Hosting قرار دارد و یا متد Path.GetExtension در فضای نام System.IO و متد الحاقی Contains با دو پارامتر استفاده شده، در فضای نام System.Linq قرار دارند.
- متد ()args.GetMultipleFiles، امکان دسترسی به فایل‌های انتخابی توسط کاربر را میسر می‌کند که خروجی آن از نوع <IReadOnlyList<IBrowserFile است. در قسمت قبل، سرویس آپلود فایل‌هایی را که تکمیل کردیم، امکان آپلود یک IBrowserFile را به سرور میسر می‌کند. اگر متد ()GetMultipleFiles را بدون پارامتری فراخوانی کنیم، حداکثر 10 فایل را قبول می‌کند و اگر تعداد بیشتری انتخاب شده باشد، یک استثناء را صادر خواهد کرد.
- سپس بر اساس پسوند فایل‌های دریافتی، آن‌ها را صرفا به فایل‌های تصویری محدود کرده‌ایم.
- در آخر، لیست فایل‌های دریافتی را یکی یکی به سرور آپلود کرده و Url دسترسی به آن‌ها را به لیست HotelRoomImages اضافه می‌کنیم. فایل‌های آپلود شده در پوشه‌ی BlazorServer.App\wwwroot\Uploads قابل مشاهده هستند.


نمایش فایل‌های انتخاب شده‌ی توسط کاربر


در ادامه می‌خواهیم پس از آپلود فایل‌ها، آن‌ها را در ذیل کامپوننت InputFile نمایش دهیم. برای اینکار در ابتدا به فایل wwwroot\css\site.css مراجعه کرده و شیوه نامه‌ی نمایش تصاویر و عناوین آن‌ها را اضافه می‌کنیم:
.room-image {
  display: block;
  width: 100%;
  height: 150px;
  background-size: cover !important;
  border: 3px solid green;
  position: relative;
}

.room-image-title {
  position: absolute;
  top: 0;
  right: 0;
  background-color: green;
  color: white;
  padding: 0px 6px;
  display: inline-block;
}
سپس بر روی لیست HotelRoomModel.HotelRoomImages که در متد HandleImageUpload آن‌را تکمیل کردیم، حلقه‌ای را ایجاد کرده و تصاویر را بر اساس RoomImageUrl آن‌ها، نمایش می‌دهیم:
<div class="form-group">
    <InputFile OnChange="HandleImageUpload" multiple></InputFile>
    <div class="row">
    @if (HotelRoomModel.HotelRoomImages.Count > 0)
    {
        var serial = 1;
        foreach (var roomImage in HotelRoomModel.HotelRoomImages)
        {
            <div class="col-md-2 mt-3">
                <div class="room-image" style="background: url('@roomImage.RoomImageUrl') 50% 50%; ">
                   <span class="room-image-title">@serial</span>
                </div>
                <button type="button" class="btn btn-outline-danger btn-block mt-4">Delete</button>
            </div>
            serial++;
        }
    }
    </div>
</div>

ذخیره سازی اطلاعات تصاویر آپلودی یک اتاق در بانک اطلاعاتی

تا اینجا موفق شدیم تصاویر انتخابی کاربر را آپلود کرده و همچنین لیست آن‌ها را نیز نمایش دهیم. در ادامه نیاز است تا این اطلاعات را در بانک اطلاعاتی ثبت کنیم. به همین جهت ابتدا سرویس IHotelRoomImageService را که در قسمت قبل تکمیل کردیم، به کامپوننت جاری تزریق می‌کنیم و سپس با استفاده از متد CreateHotelRoomImageAsync، رکوردهای تصویر متناظر با اتاق ثبت شده را اضافه می‌کنیم:
// ...
@inject IHotelRoomImageService HotelRoomImageService


@code
{
    // ...

    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages)
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
}
در حین آپلود فایل‌ها، فقط خاصیت RoomImageUrl را مقدار دهی کردیم:
HotelRoomModel.HotelRoomImages.Add(new HotelRoomImageDTO { RoomImageUrl = uploadedImageUrl });
در اینجا RoomId هر imageDto را نیز بر اساس Id واقعی اتاق ثبت شده‌ی جاری، تکمیل کرده و سپس آن‌را به CreateHotelRoomImageAsync ارسال می‌کنیم.

محل فراخوانی AddHotelRoomImageAsync فوق، در متد HandleHotelRoomUpsert است که در قسمت‌های قبل تکمیل کردیم. در اینجا پس از ثبت اطلاعات اتاق در بانک اطلاعاتی است که به Id آن دسترسی پیدا می‌کنیم:
private async Task HandleHotelRoomUpsert()
    {
       // ...

       // Create Mode
       var createdRoomDto = await HotelRoomService.CreateHotelRoomAsync(HotelRoomModel);
       await AddHotelRoomImageAsync(createdRoomDto);
       await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` created successfully.");

       // ... 
    }
اکنون اگر اطلاعات اتاق جدیدی را تکمیل کرده و تصاویری را نیز به آن انتساب دهیم، با کلیک بر روی دکمه‌ی ثبت، ابتدا اطلاعات این اتاق در بانک اطلاعاتی ثبت شده و Id آن به‌دست می‌آید، سپس رکوردهای تصویر آن جداگانه ذخیره خواهند شد.

یک نکته: در انتهای بحث خواهیم دید که اینکار غیرضروری است و با وجود رابطه‌ی one-to-many تعریف شده‌ی توسط EF-Core، اگر لیست HotelRoomImages موجودیت اتاق تعریف شده و در حال ثبت نیز مقدار دهی شده باشد، به صورت خودکار جزئی از این رابطه و تنها در یک رفت و برگشت، ثبت می‌شود. یعنی همان متد CreateHotelRoomAsync، قابلیت ثبت خودکار اطلاعات خاصیت HotelRoomImages موجودیت اتاق را نیز دارا است.


نمایش تصاویر یک اتاق، در حالت ویرایش رکورد آن

تا اینجا فقط حالت ثبت یک رکورد جدید را پوشش دادیم. در این حالت اگر به لیست اتاق‌های ثبت شده مراجعه کرده و بر روی دکمه‌ی edit یکی از آن‌ها کلیک کنیم، به صفحه‌ی ویرایش رکورد منتقل خواهیم شد؛ اما این صفحه، فاقد اطلاعات تصاویر منتسب به آن رکورد است.
علت اینجا است که در حین ویرایش اطلاعات، در متد OnInitializedAsync، هرچند اطلاعات یک اتاق را از بانک اطلاعاتی دریافت کرده و آن‌را تبدیل به Dto آن می‌کنیم که سبب نمایش جزئیات هر خاصیت در فیلد متصل به آن در فرم جاری می‌شود:
    protected override async Task OnInitializedAsync()
    {
        if (Id.HasValue)
        {
            // Update Mode
            Title = "Update";
            HotelRoomModel = await HotelRoomService.GetHotelRoomAsync(Id.Value);
        }
        // ...
    }
اما چون یک رابطه‌ی one-to-many بین اتاق و تصاویر آن برقرار است، نیاز است این رابطه را از طریق eager-loading و فراخوانی متد Include، واکشی کنیم تا اینبار زمانیکه GetHotelRoomAsync فراخوانی می‌شود، به همراه اطلاعات navigation property لیست تصاویر اتاق (HotelRoomImages) نیز باشد.
بنابراین به فایل BlazorServer\BlazorServer.Services\HotelRoomService.cs مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
namespace BlazorServer.Services
{
    public class HotelRoomService : IHotelRoomService
    {
        // ...
 
        public IAsyncEnumerable<HotelRoomDTO> GetAllHotelRoomsAsync()
        {
            return _dbContext.HotelRooms
                        .Include(x => x.HotelRoomImages)
                        .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                        .AsAsyncEnumerable();
        }

        public Task<HotelRoomDTO> GetHotelRoomAsync(int roomId)
        {
            return _dbContext.HotelRooms
                            .Include(x => x.HotelRoomImages)
                            .ProjectTo<HotelRoomDTO>(_mapperConfiguration)
                            .FirstOrDefaultAsync(x => x.Id == roomId);
        }
    }
}
در اینجا تنها تغییری که صورت گرفته، استفاده از متد Include(x => x.HotelRoomImages) است؛ تا هنگامیکه اطلاعات یک اتاق را واکشی می‌کنیم، به صورت خودکار اطلاعات تصاویر مرتبط به آن نیز واکشی گردد و سپس توسط AutoMapper، به Dto آن انتساب داده شود (یعنی انتساب HotelRoomImages موجودیت اتاق، به همین خاصیت در DTO آن). این انتساب، سبب به روز رسانی خودکار UI نیز می‌شود. یعنی برای نمایش تصاویر مرتبط با یک اتاق، همان کدهای قبلی که پیشتر داشتیم، هنوز هم کار می‌کنند.


افزودن تصاویر جدید، در حین ویرایش یک رکورد

پس از نمایش لیست تصاویر منتسب به یک اتاق در حال ویرایش، اکنون می‌خواهیم در همین حالت اگر کاربر تصویر جدیدی را انتخاب کرد، این تصویر را نیز به لیست تصاویر ثبت شده‌ی در بانک اطلاعاتی اضافه کنیم. برای اینکار نیز به متد HandleHotelRoomUpsert مراجعه کرده و از متد AddHotelRoomImageAsync در قسمت به روز رسانی آن استفاده می‌کنیم:
private async Task HandleHotelRoomUpsert()
{
   //...

   // Update Mode
   var updatedRoomDto = await HotelRoomService.UpdateHotelRoomAsync(HotelRoomModel.Id, HotelRoomModel);
   await AddHotelRoomImageAsync(updatedRoomDto);
   await JsRuntime.ToastrSuccess($"The `{HotelRoomModel.Name}` updated successfully.");

   //...
}
مشکل! اگر از این روش استفاده کنیم، هربار به روز رسانی اطلاعات یک جدول، به همراه ثبت رکوردهای تکراری نمایش داده شده‌ی در حالت ویرایش هم خواهند بود. برای مثال فرض کنید سه تصویر را به یک اتاق انتساب داده‌اید. در حالت ویرایش، ابتدا این سه تصویر نمایش داده می‌شوند. بنابراین در لیست HotelRoomModel.HotelRoomImages وجود خواهند داشت. اکنون کاربر دو تصویر جدید دیگر را هم به این لیست اضافه می‌کند. در زمان ثبت، در متد AddHotelRoomImageAsync، بررسی نمی‌کنیم که این تصویر اضافه شده، جدید است یا خیر  و یا همان سه تصویر ابتدای کار نمایش فرم در حالت ویرایش هستند. به همین جهت رکوردها، تکراری ثبت می‌شوند.
برای رفع این مشکل می‌توان در متد AddHotelRoomImageAsync، جدید بودن یک تصویر را بر اساس RoomId آن بررسی کرد. اگر این RoomId مساوی صفر بود، یعنی تازه به لیست اضافه شده‌است و حاصل بارگذاری اولیه‌ی فرم ویرایش اطلاعات نیست:
    private async Task AddHotelRoomImageAsync(HotelRoomDTO roomDto)
    {
        foreach (var imageDto in HotelRoomModel.HotelRoomImages.Where(x => x.RoomId == 0))
        {
            imageDto.RoomId = roomDto.Id;
            await HotelRoomImageService.CreateHotelRoomImageAsync(imageDto);
        }
    }
در قسمت بعد، کدهای حذف اطلاعات اتاق‌ها و تصاویر مرتبط با هر کدام را نیز تکمیل خواهیم کرد.


یک نکته: متد AddHotelRoomImageAsync اضافی است!

چون از AutoMapper استفاده می‌کنیم، در ابتدای متد ثبت یک اتاق، کار نگاشت DTO، به موجودیت متناظر با آن انجام می‌شود:
public async Task<HotelRoomDTO> CreateHotelRoomAsync(HotelRoomDTO hotelRoomDTO)
{
   var hotelRoom = _mapper.Map<HotelRoom>(hotelRoomDTO);
یعنی در اینجا چون خاصیت مجموعه‌ای HotelRoomImages موجود در HotelRoomDTO با نمونه‌ی مشابه آن در HotelRoom هم نام است، به صورت خودکار توسط AutoMapper به آن انتساب داده می‌شود و چون رابطه‌ی one-to-many در EF-Core تنظیم شده، همینقدر که hotelRoom حاصل، به همراه HotelRoomImages از پیش مقدار مقدار دهی شده‌است، به صورت خودکار آن‌ها را جزئی از اطلاعات همین اتاق ثبت می‌کند.
مقدار دهی RoomId یک تصویر، در اینجا غیرضروری است؛ چون RoomId و Room، به عنوان کلید خارجی این رابطه تعریف شده‌اند که در اینجا Room یک تصویر، دقیقا همین اتاق در حال ثبت است و EF Core در حین ثبت نهایی، آن‌را به صورت خودکار در تمام تصاویر مرتبط نیز مقدار دهی می‌کند.
یعنی نیازی به چندین بار رفت و برگشت تعریف شده‌ی در متد AddHotelRoomImageAsync نیست و اساسا نیازی به آن نیست؛ نه برای ثبت و نه برای ویرایش اطلاعات!


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-17.zip
مطالب
ساخت منوهای چند سطحی در ASP.NET MVC
پیش نیاز مطلب جاری مطالب زیر می‌باشند:
1- EF Code First #8
2- مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code First
3- نگاهی به اجزای تعاملی Twitter Bootstrap 

هدف از مطلب جاری نحوه نمایش منوی‌های چند سطحی می‌باشد، ابتدا مثال کامل زیر را در نظر بگیرید :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Menu.Models.Entities
{
    public class Category
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int? ParentId { get; set; }
        public virtual Category Parent { get; set; }
        public virtual ICollection<Category> Children { get; set; }
    }
}

public class MyContext : DbContext
{
        public DbSet<Category> Category { get; set; }
 
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<Category>()
                        .HasOptional(x => x.Parent)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ParentId)
                        .WillCascadeOnDelete(false);
 
            base.OnModelCreating(modelBuilder);
        }
}

همانطور که ملاحظه می‌کنید، مدل ما شامل مشخصات گروه محصولات می‌باشد که به صورت خود ارجاع دهنده (خاصیت Parent به همین کلاس اشاره میکند) تعریف شده است. در مورد خواص مدل‌های خود ارجاع دهنده، مطالبی را در سایت مطالعه کردید (خواص مربوط در مطالب گفته شده دقیقاً به همان صورت می‌باشد و نیازی به توضیح اضافه‌تری نیست).
هدف از این بحث، نحوه نمایش گروه محصولات داخل منو به صورت چند سطحی می‌باشد، جهت نمایش می‌بایست از تکنیک recursive function استفاده کنید، ابتدا در نظر داشته باشید که ساختار منوی تشکیل شده می‌بایست بدین صورت باشد :
 

این حالت می‌تواند تا n سطح پیش برود، حال نحوه نمایش در View مربوطه باید به صورت زیر باشد :

@using Menu.Helper
@model IEnumerable<.Models.Entities.Category>
@ShowTree(Model)
 
@helper ShowTree(IEnumerable<Menu.Models.Entities.Category> categories)
{
    foreach (var item in categories)
    {
    <li class="@(item.Children.Any() ? "dropdown-submenu" : "")">
 
        @Html.ActionLink(item.Name, actionName: "Category", controllerName: "Product", routeValues: new { Id = item.Id, productName = item.Name.ToSeoUrl() }, htmlAttributes: null)
        @if (item.Children.Any())
        {
            <ul>
                @ShowTree(item.Children)
            </ul>
                }
    </li>
 
        }
}
توجه داشته باشید که رندر نهایی توسط Bootstrap انجام شده است. ساختار منو همانطور که ملاحظه می‌کنید با استفاده از کلاس‌های drop-down که از کلاس‌های پیش فرض بوت استرپ می‌باشد تشکیل شده است همچنین کلاس dropdown-submenu که از نسخه 2 به بعد بوت استرپ موجود می‌باشد، استفاده شده است.

یک نکته :
در خط 9 این مورد را که آیا آیتم جاری فرزندی دارد چک کرده ایم اگر داشته باشد کلاس dropdown-submenu  را به li جاری اضافه میکند.