using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { public class JwtInfo { public IEnumerable<Claim> Claims { set; get; } public DateTime? ExpirationDateUtc { set; get; } public bool IsExpired { set; get; } public IEnumerable<string> Roles { set; get; } } /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static JwtInfo ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = getBase64WithoutPadding(payload); foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes)) { if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array) { foreach (var itemValue in element.EnumerateArray()) { claims.Add(new Claim(keyValue.Key, itemValue.ToString())); } } else { claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString())); } } var roles = getRoles(claims); var expirationDateUtc = getDateUtc(claims, "exp"); var isExpired = getIsExpired(expirationDateUtc); return new JwtInfo { Claims = claims, Roles = roles, ExpirationDateUtc = expirationDateUtc, IsExpired = isExpired }; } private static IList<string> getRoles(IList<Claim> claims) => claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList(); private static byte[] getBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } private static bool getIsExpired(DateTime? expirationDateUtc) => !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow); private static DateTime? getDateUtc(IList<Claim> claims, string type) { var exp = claims.SingleOrDefault(claim => claim.Type == type); if (exp == null) { return null; } var expValue = getTimeValue(exp.Value); if (expValue == null) { return null; } var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); return dateTimeEpoch.AddSeconds(expValue.Value); } private static long? getTimeValue(string claimValue) { if (long.TryParse(claimValue, out long resultLong)) return resultLong; if (float.TryParse(claimValue, out float resultFloat)) return (long)resultFloat; if (double.TryParse(claimValue, out double resultDouble)) return (long)resultDouble; return null; } } }
try { // some code to check ... } catch (InvalidOperationException ex) { // do your handling for invalid operation ... } catch (IOException ex) { // do your handling for IO error ... }
catch (SomeException ex) when (someConditionIsMet) { // Your handler logic }
علاوه بر این در اینجا میتوان چندین بدنهی catch مجزا را به ازای یک نوع استثنای مشخص به همراه whenهای متفاوتی نیز تعریف کرد و از این لحاظ محدودیتی وجود ندارد. فقط در این حالت باید به تقدم و تاخرها دقت داشت. برای نمونه در مثال ذیل، ترکیب چندین شرط متفاوت را بر اساس یک نوع مشخص استثناء، مشاهده میکنید. در اینجا اگر برای نمونه شرط ذکر شدهی در قسمت when مربوط به catch اولی صادق باشد، همینجا کار خاتمه مییابد و سایر catchها بررسی نمیشوند:
catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { } catch (SomeDependencyException ex) { }
catch (SomeDependencyException ex) { } catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { }
لاگ کردن استثناءها در C# 6 بدون مدیریت آنها
به مثال ذیل دقت کنید:
try { DoSomethingThatMightFail(s); } catch (Exception ex) when (Log(ex, "An error occurred")) { // this catch block will never be reached } ... static bool Log(Exception ex, string message, params object[] args) { Debug.Print(message, args); return false; }
تفاوت C# 6 - Exception Filtering با if/else نوشتن در بدنهی catch چیست؟
تا اینجا به این نتیجه رسیدیم که کدهای if/else دار داخل بدنهی catch کدهای قدیمی را مانند کد ذیل:
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) { if (we.Status == WebExceptionStatus.NameResolutionFailure) { //handle DNS error return; } if (we.Status == WebExceptionStatus.ConnectFailure) { //handle connection error return; } throw; }
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) when (we.Status == WebExceptionStatus.NameResolutionFailure) { //Handle NameResolutionFailure Separately } catch (WebException we) when (we.Status == WebExceptionStatus.ConnectFailure) { //Handle ConnectFailure Separately }
به این معنا که exception filters سبب Stack unwinding نمیشوند. با هربار ورود به بدنهی catch، اصطلاحا عملیات Stack unwinding صورت میگیرد. یعنی اطلاعات stack مربوط به متدهای پیش از فراخوانی متدی که سبب بروز استثناء شدهاست، از بین میروند. به این ترتیب تشخیص مقادیر متغیرهایی که سبب بروز این استثناء شدهاند نیز میسر نخواهد بود و دیگر نمیتوان با قطعیت عنوان کرد که چه مقادیری و چه اطلاعاتی سبب بروز این مشکل شدهاند. اما در حالت exception filters در قسمت when آن هنوز وارد بدنهی catch نشدهایم. در اینجا دسترسی کاملی به اطلاعات stack جاری و مقادیر متغیرهای محلی که سبب بروز این استثناء شدهاند وجود دارد.
تفاوت stack با stack trace چیست؟ stack قطعهای از حافظهاست که اطلاعاتی در مورد نحوهی فراخوانی متدها، آدرس بازگشتی آنها، آرگومان و همچنین متغیرهای محلی آنها را دارا است. اما stack trace تنها یک رشتهاست و بیانگر نام متدهایی است که هم اکنون بر روی stack قرار دارند. احتمالا پیشتر خوانده بودید که فراخوانی throw داخل بدنهی catch سبب حفظ stack trace میشود و اگر throw ex صورت گیرد، این اطلاعات از دست میروند و بازنویسی میشوند. اما در C# 6 امکان حفظ کل اطلاعات stack به همراه exception filtering میسر شدهاست.
مزایای بارگذاری متادیتا از طریق یک فایل جاوااسکریپتی
در ابتدا فایل metadata.js را در مسیر Scripts ایجاد نمایید و ارجاعی به آن را در layaout.cshtml قرار دهید:
<script src="~/Scripts/metadata.js"></script>
کارهایی را که باید انجام بدهیم:
private static void WriteMetadata() { var metadata = new EFContextProvider<ApplicationDbContext>().Metadata();//1.1 var fileName =HttpContext.Current.Server.MapPath("~/Scripts/metadata/metadata.js"); const string prefix = "var metadata = JSON.stringify(";//1.2 const string postfix = ");"; using (var writer = new StreamWriter(fileName)) { writer.WriteLine(prefix + metadata + postfix);//1.3 } }
var serviceName = 'breeze/northwind'; // your service root here function createEntityManager() { var dataService = new breeze.DataService({ serviceName: serviceName, hasServerMetadata: false // 2.1 }); var metadataStore = new breeze.MetadataStore({ namingConvention: camelCaseConvention // if you use this convention }); // initialize it from the application's metadata variable metadataStore.importMetadata(metadata);//2.2 return new breeze.EntityManager({ dataService: dataService, metadataStore: metadataStore });//2.3 }
حذف تمامی تگهای یک عبارت HTML
این تابع و عبارت باقاعده به کار رفته در آن هنگام جستجو بر روی یک فایل html که حاوی انبوهی از تگها است میتواند مفید باشد و یا جهت حذف هر نوع فرمت اعمالی به یک متن.
private static readonly Regex _htmlRegex = new Regex("<.*?>", RegexOptions.Compiled);
/// <summary>
/// حذف تمامی تگهای موجود
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanTags(string html)
{
return _htmlRegex.Replace(html, string.Empty);
}
حذف یک تگ ویژه بدون حذف محتویات آن
فرض کنید میخواهید تمام تگهای script بکار رفته در یک محتوای html را حذف کنید.
private static readonly Regex _contentRegex = new Regex(@"<\/?script[^>]*?>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// تنها حذف یک تگ ویژه
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanScriptTags(string html)
{
return _contentRegex.Replace(html, string.Empty);
}
حذف یک تگ خاص به همراه محتویات آن تگ
فرض کنید میخواهیم در محتوای html دریافتی اثری از تگها و کدهای جاوا اسکریپتی یافت نشود.
private static readonly Regex _safeStrRegex = new Regex(@"<script[^>]*?>[\s\S]*?<\/script>",
RegexOptions.Compiled | RegexOptions.IgnoreCase);
/// <summary>
/// حذف یک تگ ویژه به همراه محتویات آن
/// </summary>
/// <param name="html">ورودی اچ تی ام ال</param>
/// <returns></returns>
public static string CleanScriptsTagsAndContents(string html)
{
return _safeStrRegex.Replace(html, "");
}
و اگر فرض کنیم که متدهای فوق در کلاسی به نام CRegExHelper قرار گرفتهاند، کلاس آزمون واحد آن به صورت زیر میتواند باشد:
using NUnit.Framework;
namespace testWinForms87
{
[TestFixture]
public class CTestRegExHelper
{
#region Methods (3)
// Public Methods (3)
[Test]
public void TestCleanScriptsTagsAndContents()
{
Assert.AreEqual(
CRegExHelper.CleanScriptsTagsAndContents("data1 <script> ... </script> data2"),
"data1 data2");
}
[Test]
public void TestCleanScriptTags()
{
Assert.AreEqual(
CRegExHelper.CleanScriptTags("<b>data1</b> <script> ... </script> data2"),
"<b>data1</b> ... data2");
}
[Test]
public void TestCleanTags()
{
Assert.AreEqual(
CRegExHelper.CleanTags("<b>data</b>"),
"data");
}
#endregion Methods
}
}
// Named function function add(x, y) { return x + y; } // Anonymous function let myAdd = function(x, y) { return x+y; };
let z = 100; function addToZ(x, y) { return x + y + z; }
function add(x: number, y: number): number { return x + y; } let myAdd = function(x: number, y: number): number { return x+y; };
function PublicationMessage(year: number): string { return 'Date published: ' + year; }
let publishFunc: (someYear: number) => string;
publishFunc = PublicationMessage; let message: string = publishFunc(2016);
همچنین میتوان function type را به صورت inline نیز تعریف کرد:
let myAdd: (baseValue:number, increment:number) => number = function(x, y) { return x + y; };
Optional and Default Parameters
در جاوا اسکریپت تمامی پارامترهای یک تابع اختیاری هستند. اما TypeScript کمی متفاوت است. یعنی در حالت پیشفرض، ذکر تمامی پارامترها ضروری است؛ مگر اینکه پارامترهای موردنیاز را به صورت اختیاری تعیین کنید. به طور مثال در تابع زیر دو پارامتر را تعریف کردهایم:
function CreateCustomer(name: string, age?: number) {}
همانطور که مشاهده میکنید با افزودن علامت سوال بعد از نام پارامتر، توانستهایم آن را به صورت اختیاری تعریف کنیم. نکتهایی که در اینجا وجود دارد این است که تمامی پارامترهای optional، حتماً باید بعد از پارامترهای required تعریف شوند.
برای تعیین مقدار پیشفرض برای هر پارامتر نیز میتوانیم به اینصورت عمل کنیم:
function GetBookByTitle(title: string = 'C# 6.0 in a Nutshell') {}
default parameters در صورتیکه بعد از required parameters آورده شوند، به عنوان optional در نظر گرفته میشوند. یعنی در اینحالت لزومی به گذاشتن علامت سوال، بعد از نام پارامتر نیست. نکتهی قابل توجهایی که در استفاده از default parameters وجود دارد این است که علاوه بر رشتهها میتوان عبارات (expressions) را نیز به آنها اختصاص داد:
function GetBookByTitle(title: string = GetMostPopularBooks()) {}
Rest Parameters
rest parameters به شما این امکان را میدهند تا به تعداد نامحدودی پارامتر به یک تابع ارسال کنید:
function GetBooksReadForCust(name: string, ...bookIDs: number[]) {}
تابع فوق دو پارامتر را از ورودی دریافت میکند. پارامتر دوم این تابع به صورت rest تعریف شده است. یعنی برای پارامتر دوم میتوانیم هر تعداد پارامتری را به این تابع ارسال کنیم. همچنین برای نوع این پارامتر، یک آرایه از نوع number را تعیین کردهایم. یعنی پارامترهای دریافتی، درون یک آرایه از نوع number ذخیره خواهند شد. در ES 5 برای داشتن این چنین قابلیتی از شیء arguments استفاده میکردیم. یعنی تابع فوق را میبایستی اینگونه مینوشتیم:
function GetBooksReadForCust(name) { var bookIDs = []; for (var _i = 1; _i < arguments.length; _i++) { bookIDs[_i - 1] = arguments[_i]; } }
استفاده از this
درک this در جاوا اسکریپت، در ابتدا باعث مقداری سردرگمی میشود. یعنی مقدار آن در زمان فراخوانی تابع، ست خواهد شد. یعنی در هر بلاک از کد، وضعیتهای متفاوتی را ارائه میدهد. به عنوان مثال درون callback مربوط به تابع setInterval در تابع زیر میخواهیم به مقدار متغیر publishDate دسترسی داشته باشم:
function Book() { let self = this; self.publishDate = 2016; setInterval(function() { console.log(self.publishDate); }, 1000); }
همانطور که مشاهده میکنید برای دسترسی به این پراپرتی، مقدار this را درون یک متغیر با نام self، در ابتدا تعریف کردهایم. زیرا استفادهی مستقیم از this.publishDate درون callback به چیز دیگری اشاره میکند. این روش در ES 5 خیلی رایج است. اما با استفاده از Arrow Functions به راحتی میتوانیم به this در هر جایی دسترسی داشته باشیم. بنابراین کد فوق را میتوانیم به این صورت بازنویسی کنیم:
function Book() { this.publishDate = 2016; setInterval(() => { console.log(this.publishDate); }, 1000); }
در واقع Arrow Function در پشت صحنه کار capture کردن this را برایمان انجام خواهد داد.
Function overloads
قابلیت function overloading در بیشتر typed languageها در دسترس میباشد. همانطور که میدانید این قابلیت جهت تعریف امضاءهای مختلف برای یک تابع استفاده میشود. یعنی ایجاد توابعی با یک نام، اما با انواع متفاوت. از آنجائیکه TypeScript به جاوا اسکریپت کامپایل میشود، در نتیجه جاوا اسکریپت فاقد نوع (type) است. پس در زمان کامپایل نوعها برداشته خواهند شد. بنابراین داشتن توابعی همنام باعث بروز مشکلاتی خواهد شد. برای داشتن نسخههای مختلفی از یک تابع میتوانیم تعاریف موردنیازمان را ارائه داده، اما تنها یک پیادهسازی داشته باشیم. برای مثال میخواهیم یک overload دیگر برای تابع زیر داشته باشیم:
function GetTitles(author: string) : string[];
تابع فوق یک رشته را از ورودی دریافت کرده و در نهایت یک آرایه از رشتهها را بر میگرداند. برای overload دیگر این تابع میخواهیم به جای دریافت رشته، یک boolean از ورودی دریافت کنیم:
function GetTitles(available: boolean) : string[];
همانطور که مشاهده میکنید، هیچکدام از overloadهای فوق پیادهسازیایی ندارند. در واقع تا اینجا به TypeScript گفتهایم که نیاز به دو نسخه از تابع GetTitles خواهیم داشت. اکنون میتوانیم یک پیادهسازی کلی برای دو overload فوق داشته باشیم:
function GetTitles(bookProperty: any) : string[] { if(typeof bookProperty == 'string') { // some code } else if (typeof bookProperty == 'boolean') { // some code } return result; }
همانطور که عنوان شد، تنها پیادهسازی فوق را برای تمامی overloadها خواهیم داشت. در نتیجه اینبار نوع پارامتر ورودی را any تعریف کردهایم. سپس درون بدنهی تابع، نوع پراپرتی را توسط typeof تشخیص دادهایم. بنابراین برای فراخوانی هر یک از overloadها، میتوانیم کدهای خاصی را اجرا کنیم.
reCAPTCHA چیست؟
استفاده آسان و امنیت بالا، جملهای میباشد که گوگل در سرتیتر تعریف آن جای داده که البته عنوان «من روبات نیستم» در سرویس استفاده شدهاست. reCAPTCHA یک سرویس رایگان برای وب سایتهای شما در جهت حفظ آن در برابر روباتهای مخرب است و از موتور تجزیه و تحلیل پیشرفتهی تشخیص انسان در برابر روباتها استفاده مینماید. reCAPTCHA را میتوان به صورت ماژول در بلاگ و یا فرمهای ثبت نام و ... جای داد که فقط با یک کلیک هویت سنجی انجام خواهد شد. گاها ممکن است بجای کلیک از شما سوالی پرسیده شود که در این صورت میبایستی تصاویر مرتبط با آن سوال را تیک زده باشید. دلیل استفاده از reCAPTCHA:
- گزارش روزانه از وضعیت موفقیت آمیز بودن هویت سنجی
- سهولت استفاده برای کاربران
- سهولت استفاده جهت برنامه نویسان
- دسترسی پذیری مناسب بدلیل وجود سؤالات تصویری و تلفظ و پخش عبارت بصورت صوتی
- امنیت بالا
آیا میتوان قالب reCAPTCHA را تغییر داد؟
جواب این سوال بله میباشد. این سرویس در دو قالب سفید و مشکی ارائه شدهاست که به صورت پیش فرض قالب سفید آن انتخاب میشود. در تصویر زیر قالبهای این سرویس را مشاهده خواهید کرد.
زبانهای پشتیبانی شده در این سرویس:
اضافه نمودن reCAPTCHA به سایت:
اگر قبلا در گوگل ثبت نام نمودهاید کافیست وارد این سایت شوید و بر روی Get reCAPTCHA کلیک نمائید؛ در غیر اینصورت میبایستی یک حساب کاربری ایجاد نماید. بعد از ورود، به کنترل پنل هدایت خواهید شد. در نمای اول به تصویر زیر برخورد خواهید کرد که از شما ثبت سایت جدید را خواستار است:
نام، دامنه سایت و مالک را وارد و ثبت نام نماید.
پس از آنکه بر روی دکمهی ثبت نام کلیک نمودید، برای شما دو کلید جدید را ثبت مینماید که منحصر به سایت شماست. Site Key رشته ای را داراست که در کدهای HTML قرار خواهد گرفت و کلید بعدی Secret Key میباشد. ارتباط سایت شما با گوگل میبایستی به صورت محرمانه محفوظ بماند.
گامهای لازم جهت نمایش سرویس در سایت:
- دستورات سمت کاربر
- دستورات سمت سرور
دستورات سمت کاربر:
کد زیر را در قبل از بسته شدن تک <head/> قرار دهید:
<script src='https://www.google.com/recaptcha/api.js'></script>
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>
نکته: مقدار data-sitekey برابر است با رشته Site Key که گوگل برای شما ثبت نمود.
دستورات سمت سرور:
وقتی کاربر فرم حاوی کپچا را که به صورت صحیح هویت سنجی آن انجام شده باشد به سمت سرور ارسال کند، به عنوان بخشی از دادهی ارسال شده، یک رشته با نام g-recaptcha-response با دستور Request دریافت خواهید کرد که به منظور بررسی اینکه آیا گوگل تایید کرده است که کاربر، یک درخواست POST ارسال نموداست. با این پارامترها یک مقدار json برگشت داده خواهد شد که میبایستی کلاسی متناظر با آن جهت خواندن ساخته شود.
با استفاده از کد زیر مقدار برگشتی Json را در کلاس مپ مینمائیم:using System.Collections.Generic; using Newtonsoft.Json; namespace BaseConfig.Security.Captcha { public class RepaptchaResponse { [JsonProperty("success")] public bool Success { get; set; } [JsonProperty("error-codes")] public List<string> ErrorCodes { get; set; } } }
با استفاده از کلاس زیر درخواستی به گوگل ارسال شده و در صورتیکه با خطا مواجه شود با استفاده از دستور switch به آن دسترسی خواهیم یافت.
using System.Configuration; using System.Net; using Newtonsoft.Json; namespace BaseConfig.Security.Captcha { public class ReCaptcha { public static string _secret; static ReCaptcha() { _secret = ConfigurationManager.AppSettings["ReCaptchaGoogleSecretKey"]; } public static bool IsValid(string response) { //secret that was generated in key value pair var client = new WebClient(); var reply = client.DownloadString($"https://www.google.com/recaptcha/api/siteverify?secret={_secret}&response={response}"); var captchaResponse = JsonConvert.DeserializeObject<RepaptchaResponse>(reply); // when response is false check for the error message if (!captchaResponse.Success) { //if (captchaResponse.ErrorCodes.Count <= 0) return View(); //var error = captchaResponse.ErrorCodes[0].ToLower(); //switch (error) //{ // case ("missing-input-secret"): // ViewBag.Message = "The secret parameter is missing."; // break; // case ("invalid-input-secret"): // ViewBag.Message = "The secret parameter is invalid or malformed."; // break; // case ("missing-input-response"): // ViewBag.Message = "The response parameter is missing."; // break; // case ("invalid-input-response"): // ViewBag.Message = "The response parameter is invalid or malformed."; // break; // default: // ViewBag.Message = "Error occured. Please try again"; // break; //} return false; } // Captcha is valid return true; } } }
تابع IsValid از نوع برگشتی Boolean بوده و خطایی برگشت داده نخواهد شد و از این جهت به صورت کامنت برای شما گذاشته شده که میتوان متناظر با کد نویسی آن را تغییر دهید.
در اکشن زیر مقدار response برسی میشود تا خالی نباشد و همچنین مقدار آن را میتوان با استفاده از تابع IsValid در کلاس ReCaptcha به سمت گوگل فرستاد.
// // POST: /Account/Login [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public virtual async Task<ActionResult> Login(LoginPageModel model, string returnUrl) { var response = Request["g-recaptcha-response"]; if (response != null && ReCaptcha.IsValid(response)) { // } }
گاها اتفاق میافتد که از دستورات Ajax برای ارسال اطلاعات به سمت سرور استفاده میشود که در این صورت لازم است بعد از پایان عملیات، کپچا ریفرش گردد. برای این کار میتوان از دستور جاوا اسکریپتی زیر استفاده نمود. در صورتیکه صفحه Postback شود، کپچا مجددا ریفرش خواهد شد.
/** * * @param {} data * @returns {} */ function Success(data) { grecaptcha.reset(); }
تا اینجا موفق شدیم تا فرم ارسالی همراه کپچا را به سمت سرور ارسال کنیم. اما ممکن است در یک صفحه از چند کپچا استفاده شود که در این صورت میبایستی دستورات سمت کاربر تغییر نمایند.
برای این کار دستور
<div data-sitekey="6LdHGgwTAAAAAClKFhGthRrjBXh5AUGd4eWNCQq7"></div>
<script> var recaptcha1; var recaptcha2; var myCallBack = function () { //Render the recaptcha1 on the element with ID "recaptcha1" recaptcha1 = grecaptcha.render('recaptcha1', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); //Render the recaptcha2 on the element with ID "recaptcha2" recaptcha2 = grecaptcha.render('recaptcha2', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); //Render the recaptcha3 on the element with ID "recaptcha3" recaptcha2 = grecaptcha.render('recaptcha3', { 'sitekey': '6Lf9FQwTAAAAAE6XlDqrey24K4xJOPM5nNVBmNO9', 'theme': 'light' }); }; </script>
برای نمایش کپچا، تگهای div با id متناظر با recaptcha1, recaptcha2, recaptcha3 ( در این مثال از سه کپچا در صفحه استفاده شده است ) در صفحه قرار خواهند گرفت.
<div id="recaptcha1"></div> <div id="recaptcha2"></div> <div id="recaptcha3"></div>
کار ما تمام شد. حال اگر پروژه را اجرا نمائید، در صفحه سه کپچا مشاهده خواهید کرد.
چند زبانه کردن کپچا:
برای چند زبانه کردن کافیست با مراجعه به این لینک و یا استفاده از تصویر بالا ( زبانهای پشتیبانی ) مقدار آن زبان را برابر با پراپرتی hl که به صورت کوئری استرینگ برای گوگل ارسال میگردد، استفاده نمود. کد زیر نمونهی استفاده شده برای زبانهای انگلیسی و فارسی میباشد که با ریسورس مقدار دهی میشود.<script src='https://www.google.com/recaptcha/api.js?hl=@(App_GlobalResources.CP.CurrentAbbrivation)'></script>
در صورتی که از فایل ریسوس استفاده نمیکنید میتوان به صورت مستقیم مقدار دهی نمائید:
<script src='https://www.google.com/recaptcha/api.js?hl=fa'></script>
برای دوستانی که با تکنولوژی ASP.Net کار میکنند، این روال هم برای آنها هم صادق میباشد.
برای دریافت پروژه اینجا کلیک نمائید.
var items = (from p in context.Categories
select new
{
Id = p.Id,
Name = p.Name,
DateAdded = EF.Property<DateTime>(p, "DateAdded")
}).ToList();
var cList = context.Categories .OrderBy(b => EF.Property<DateTime>(b, "DateAdded")).ToList(); foreach (var cat in cList) { Console.Write("Category Name: " + cat.CategoryName); Console.WriteLine(" Created: " + context.Entry(cat).Property("DateAdded").CurrentValue); }
using System; namespace TestUsing { public class MyResource : IDisposable { public void DoWork() { throw new ArgumentException("A"); } public void Dispose() { throw new ArgumentException("B"); } } public static class TestClass { public static void Test() { using (MyResource r = new MyResource()) { throw new ArgumentException("C"); r.DoWork(); } } } }
try { TestClass.Test(); } catch (Exception ex) { Console.WriteLine(ex.Message); }
پاسخ: برخلاف تصور (که احتمالا C است؛ چون قبل از فراخوانی متد DoWork سبب بروز استثناء شده است)، فقط B را در خروجی مشاهده خواهیم کرد!
و این دقیقا مشکلی است که در حین کار با کتابخانه iTextSharp برای اولین بار با آن مواجه شدم. روش استفاده متداول از iTextSharp به نحو زیر است:
using (var pdfDoc = new Document(PageSize.A4)) { //todo: ... }
و خلاصه نتایج هم این است:
اگر به ثبت جزئیات خطاهای سیستم اهمیت میدهید (یکی از مهمترین مزیتهای دات نت نسبت به بسیاری از فریم ورکهای مشابه که حداکثر خطای 0xABC12EF را نمایش میدهند)، از using استفاده نکنید! using در پشت صحنه به try/finally ترجمه میشود و بهتر است این مورد را دستی نوشت تا اینکه کامپایلر اینکار را به صورت خودکار انجام دهد.
در اینجا باز هم به یک سری کد تکراری try/finally خواهیم رسید و همانطور که در مباحث کاربردهای Action و Func در این سایت ذکر شد، میتوان آنرا تبدیل به کدهایی با قابلیت استفاده مجدد کرد. یک نمونه از پیاده سازی آنرا در این سایت «C# Using Blocks can Swallow Exceptions » میتوانید مشاهده کنید که خلاصه آن کلاس زیر است:
using System; namespace Guard { public static class SafeUsing { public static void SafeUsingBlock<TDisposable>(this TDisposable disposable, Action<TDisposable> action) where TDisposable : IDisposable { disposable.SafeUsingBlock(action, d => d); } internal static void SafeUsingBlock<TDisposable, T>(this TDisposable disposable, Action<T> action, Func<TDisposable, T> unwrapper) where TDisposable : IDisposable { try { action(unwrapper(disposable)); } catch (Exception actionException) { try { disposable.Dispose(); } catch (Exception disposeException) { throw new AggregateException(actionException, disposeException); } throw; } disposable.Dispose(); } } }
new Document(PageSize.A4).SafeUsingBlock(pdfDoc => { //todo: ... });
public static string GetClaimValue(this IPrincipal currentPrincipal, string key) { var identity = currentPrincipal.Identity as ClaimsIdentity; if (identity == null) return null; var claim = identity.Claims.FirstOrDefault(c => c.Type == key); return claim?.Value; }
public override Task<ClaimsIdentity> CreateUserIdentityAsync(User user) { return _userManager.GenerateUserIdentityAsync(user); }
case SignInStatus.Success: var user = await _userManager.FindByNameAsync(model.UserName); await _signInManager.CreateUserIdentityAsync(user); return RedirectToLocal(returnUrl);
using System.ComponentModel.DataAnnotations; namespace NotifyPropertyChangedGenerator.Demo; public enum Gender { NotSpecified, [Display(Name = "مرد")] Male, [Display(Name = "زن")] Female }
public static class Extensions { public static string GetDisplayName(this Enum value) { if (value is null) throw new ArgumentNullException(nameof(value)); var attribute = value.GetType().GetField(value.ToString())? .GetCustomAttributes<DisplayAttribute>(false).FirstOrDefault(); if (attribute is null) return value.ToString(); return attribute.GetType().GetProperty("Name")?.GetValue(attribute, null)?.ToString(); } }
یعنی در اصل متد کمکی که برای اینکار نیاز داریم، چنین خروجی را دارد:
namespace NotifyPropertyChangedGenerator.Demo { public static class GenderExtensions { public static string GetDisplayName(this Gender @enum) { return @enum switch { Gender.NotSpecified => "NotSpecified", Gender.Male => "مرد", Gender.Female => "زن", _ => throw new ArgumentOutOfRangeException(nameof(@enum)) }; } } }
با ارائهی Source Generators، مشکلات یاد شده دیگر وجود ندارند. یعنی کار تولید متدهای اختصاصی برای هر enum، خودکار است و همچنین به روز رسانی آنی آنها با هر تغییری در enumها نیز پیشبینی شدهاست.
تهیهی تولید کنندهی خودکار کدی که نام نمایشی enumها را به صورت از پیش محاسبه شده ارائه میدهد
در قسمت قبل، با روش تهیه و استفاده از Source Generators آشنا شدیم. در اینجا نیز از همان قالب، در جهت تولید کد متد الحاقی GetDisplayName فوق، استفاده خواهیم کرد. یعنی هدف رسیدن به کلاس GenderExtensions فوق و متد GetDisplayName آن، در زمان کامپایل برنامه و به صورت خودکار است:
[Generator] public class EnumExtensionsGenerator : ISourceGenerator { public void Initialize(GeneratorInitializationContext context) {} public void Execute(GeneratorExecutionContext context) { var compilation = context.Compilation; foreach (var syntaxTree in compilation.SyntaxTrees) { var semanticModel = compilation.GetSemanticModel(syntaxTree); var immutableHashSet = syntaxTree.GetRoot() .DescendantNodesAndSelf() .OfType<EnumDeclarationSyntax>() .Select(enumDeclarationSyntax => semanticModel.GetDeclaredSymbol(enumDeclarationSyntax)) .OfType<ITypeSymbol>() /*.Where(typeSymbol => typeSymbol.GetAttributes().Any( attributeData => string.Equals(attributeData.AttributeClass?.Name, "GenerateExtensions", StringComparison.Ordinal) ))*/ .ToImmutableHashSet(); foreach (var typeSymbol in immutableHashSet) { var source = GenerateEnumExtensions(typeSymbol); context.AddSource($"{typeSymbol.Name}Extensions.cs", source); } } }
در اینجا در متد Execute، دسترسی کاملی را به اطلاعات تهیه شدهی توسط کامپایلر داریم. توسط آن تمام Enumهای برنامه را یا همان EnumDeclarationSyntax را در اینجا، یافته و سپس حلقهای را بر روی اطلاعات آنها تشکیل داده و برای تک تک آنها، توسط متد GenerateEnumExtensions، کد معادل کلاس GenderExtensions را که در این مطلب معرفی شد، تولید میکنیم. در پایان کار نیز این کد را توسط متد AddSource، به کامپایلر معرفی خواهیم کرد تا بلافاصله در IDE ظاهر شده و قابلیت استفاده را پیدا کند.
یک نکته: اگر میخواهید صرفا enumهای خاصی در این بین بررسی شوند، میتوانید کدهای یک Attribute سفارشی را مثلا با نام فرضی [GenerateExtensions] در همینجا توسط متد context.AddSource به مجموعه سورسها اضافه کنید و سپس بر اساس نام آن، در قسمت Where ایی که کامنت شدهاست، تنها اطلاعات مدنظر را فیلتر و پردازش کنید.
متدی هم که ابتدا کلاس Extensions را بر اساس نام هر Enum موجود تولید و سپس بدنهی متد GetDisplayName اختصاصی آنرا تکمیل میکند، به صورت زیر است:
private string GenerateEnumExtensions(ITypeSymbol typeSymbol) { return $@"namespace {typeSymbol.ContainingNamespace} {{ public static class {typeSymbol.Name}Extensions {{ public static string GetDisplayName(this {typeSymbol.Name} @enum) {{ {GenerateExtensionMethodBody(typeSymbol)} }} }} }}"; } private static string GenerateExtensionMethodBody(ITypeSymbol typeSymbol) { var sb = new StringBuilder(); sb.Append(@"return @enum switch { "); foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>()) { var displayAttribute = fieldSymbol.GetAttributes() .FirstOrDefault(attributeData => string.Equals(attributeData.AttributeClass?.Name, "DisplayAttribute", StringComparison.Ordinal)); if (displayAttribute is null) { sb.AppendLine( $@" {typeSymbol.Name}.{fieldSymbol.Name} => ""{fieldSymbol.Name}"","); } else { var displayAttributeName = displayAttribute.NamedArguments .FirstOrDefault(x => string.Equals(x.Key, "Name", StringComparison.Ordinal)) .Value; sb.AppendLine( $@" {typeSymbol.Name}.{fieldSymbol.Name} => ""{displayAttributeName.Value}"","); } } sb.Append( @" _ => throw new ArgumentOutOfRangeException(nameof(@enum)) };"); return sb.ToString(); }
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: SourceGeneratorTests-part3.zip
سؤال: چگونه میتوان کدهای تولید شدهی توسط یک Source Generator را ذخیره کرد؟
Source Generators به صورت پیشفرض هیچ فایلی را بر روی دیسک سخت ذخیره نمیکنند و تمام عملیات آنها در حافظه انجام میشود. اگر علاقمند به مطالعهی این خروجیهای خودکار، به صورت فایلهای واقعی هستید، نیاز به انجام تغییرات زیر در فایل csproj پروژهی مصرف کنندهی Source Generator است:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net6.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup> <Target Name="CleanSourceGeneratedFiles" BeforeTargets="BeforeBuild" DependsOnTargets="$(BeforeBuildDependsOn)"> <RemoveDir Directories="$(CompilerGeneratedFilesOutputPath)" /> </Target> <ItemGroup> <!-- Exclude the output of source generators from the compilation --> <Compile Remove="$(CompilerGeneratedFilesOutputPath)/**/*.cs" /> <Content Include="$(CompilerGeneratedFilesOutputPath)/**" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" /> </ItemGroup> </Project>
- EmitCompilerGeneratedFiles سبب ثبت فایلهای خودکار تولید شده، بر روی دیسک سخت میشود که قالب مسیر پیشفرض ذخیره سازی آن به صورت زیر است:
{BaseIntermediateOutpath}/generated/{Assembly}/{SourceGeneratorName}/{GeneratedFile}
- چون این فایلهای cs. جدید ثبت شدهی بر روی دیسک سخت، مجددا وارد پروسهی کامپایل میشوند و خود Source Generator هم یک نمونهی از آنهارا پیشتر به کامپایلر معرفی کردهاست، برنامه دیگر به علت وجود اطلاعات تکراری، کامپایل نخواهد شد. به همین جهت نیاز است تا قسمت Compile Remove فوق را نیز معرفی کرد تا کامپایلر از پوشهی Generated تنظیمی، صرفنظر کند.
- اطلاعات موجود در پوشهی Generated، فقط یکبار تولید میشوند؛ صرفنظر از اطلاعات موجود در حافظه که همیشه به روز است. به همین جهت اگر میخواهید نمونههای به روز شدهی آنها را نیز بر روی دیسک سخت داشته باشید، نیاز به قسمت RemoveDir تنظیمی وجود دارد.