public class MemberTest : IClassFixture<TestStartup> { private readonly TestStartup testStartup; private readonly DatabaseSeedService seedService; public MemberTest(TestStartup testStartup) { this.testStartup = testStartup; this.seedService = this.testStartup.GetService<DatabaseSeedService>(); } [Fact] public async Task Add_InsertMember_ReturnOKResponse() { // ... } }
اعمال کنترل دسترسی پویا در پروژههای ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider { private readonly LazyConcurrentDictionary<string, AuthorizationPolicy> _policies = new LazyConcurrentDictionary<string, AuthorizationPolicy>(); public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : base(options) { } public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { //... } }
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName) { //... var policy = _policies.GetOrAdd(policyName, name => { var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(','); return new AuthorizationPolicyBuilder() .RequireClaim(CustomClaimTypes.Permission, permissionNames) .Build(); }); return policy; }
برای مثال لینکهای http://www.site.com و http://www.site.com/index.htm دو هش متفاوت را تولید میکنند اما در عمل یکی هستند. نمونهی دیگر، لینکهای http://www.site.com/index.htm و http://www.site.com/index.htm#section1 هستند که فقط اصطلاحا در یک fragment با هم تفاوت دارند و از این دست لینکهایی که باید در حین ثبت یکی درنظر گرفته شوند، زیاد هستند و اگر علاقمند به مرور آنها هستید، میتوانید به صفحهی URL Normalization در ویکیپدیا مراجعه کنید.
اگر نکات این صفحه را تبدیل به یک کلاس کمکی کنیم، به کلاس ذیل خواهیم رسید:
using System; using System.Web; namespace OPMLCleaner { public static class UrlNormalization { public static bool AreTheSameUrls(this string url1, string url2) { url1 = url1.NormalizeUrl(); url2 = url2.NormalizeUrl(); return url1.Equals(url2); } public static bool AreTheSameUrls(this Uri uri1, Uri uri2) { var url1 = uri1.NormalizeUrl(); var url2 = uri2.NormalizeUrl(); return url1.Equals(url2); } public static string[] DefaultDirectoryIndexes = new[] { "default.asp", "default.aspx", "index.htm", "index.html", "index.php" }; public static string NormalizeUrl(this Uri uri) { var url = urlToLower(uri); url = limitProtocols(url); url = removeDefaultDirectoryIndexes(url); url = removeTheFragment(url); url = removeDuplicateSlashes(url); url = addWww(url); url = removeFeedburnerPart(url); return removeTrailingSlashAndEmptyQuery(url); } public static string NormalizeUrl(this string url) { return NormalizeUrl(new Uri(url)); } private static string removeFeedburnerPart(string url) { var idx = url.IndexOf("utm_source=", StringComparison.Ordinal); return idx == -1 ? url : url.Substring(0, idx - 1); } private static string addWww(string url) { if (new Uri(url).Host.Split('.').Length == 2 && !url.Contains("://www.")) { return url.Replace("://", "://www."); } return url; } private static string removeDuplicateSlashes(string url) { var path = new Uri(url).AbsolutePath; return path.Contains("//") ? url.Replace(path, path.Replace("//", "/")) : url; } private static string limitProtocols(string url) { return new Uri(url).Scheme == "https" ? url.Replace("https://", "http://") : url; } private static string removeTheFragment(string url) { var fragment = new Uri(url).Fragment; return string.IsNullOrWhiteSpace(fragment) ? url : url.Replace(fragment, string.Empty); } private static string urlToLower(Uri uri) { return HttpUtility.UrlDecode(uri.AbsoluteUri.ToLowerInvariant()); } private static string removeTrailingSlashAndEmptyQuery(string url) { return url .TrimEnd(new[] { '?' }) .TrimEnd(new[] { '/' }); } private static string removeDefaultDirectoryIndexes(string url) { foreach (var index in DefaultDirectoryIndexes) { if (url.EndsWith(index)) { url = url.TrimEnd(index.ToCharArray()); break; } } return url; } } }
برای مثال اگر یک فایل OPML چنین ساختار XML ایی را داشته باشد:
<?xml version="1.0" encoding="utf-8"?> <opml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1.0"> <body> <outline text="آی تی ایرانی"> <outline type="rss" text="فید کلی آخرین نظرات، مطالب، اشتراکها و پروژههای .NET Tips" title="فید کلی آخرین نظرات، مطالب، اشتراکها و پروژههای .NET Tips" xmlUrl="https://www.dntips.ir/Feed/LatestChanges" htmlUrl="https://www.dntips.ir/" /> </outline> </body> </opml>
using System.Xml.Serialization; namespace OPMLCleaner { [XmlType(TypeName="outline")] public class Opml { [XmlAttribute(AttributeName="text")] public string Text { get; set; } [XmlAttribute(AttributeName = "title")] public string Title { get; set; } [XmlAttribute(AttributeName = "type")] public string Type { get; set; } [XmlAttribute(AttributeName = "xmlUrl")] public string XmlUrl { get; set; } [XmlAttribute(AttributeName = "htmlUrl")] public string HtmlUrl { get; set; } } }
var document = XDocument.Load("it-92-03-01.opml"); var results = (from node in document.Descendants("outline") where node.Attribute("htmlUrl") != null && node.Parent.Attribute("text") != null && node.Parent.Attribute("text").Value == "آی تی ایرانی" select new Opml { HtmlUrl = (string)node.Attribute("htmlUrl"), Text = (string)node.Attribute("text"), Title = (string)node.Attribute("title"), Type = (string)node.Attribute("type"), XmlUrl = (string)node.Attribute("xmlUrl") }).ToList();
using System.Collections.Generic; namespace OPMLCleaner { public class OpmlCompare : EqualityComparer<Opml> { public override bool Equals(Opml x, Opml y) { return UrlNormalization.AreTheSameUrls(x.HtmlUrl, y.HtmlUrl); } public override int GetHashCode(Opml obj) { return obj.HtmlUrl.GetHashCode(); } } }
var distinctResults = results.Distinct(new OpmlCompare()).ToList();
تزریق خودکار وابستگیها در برنامههای ASP.NET MVC
بیت کوین در سی شارپ
بازنویسی متد مقدار دهی اولیهی کاربر ادمین در ASP.NET Core Identity توسط متد HasData در EF Core
پیاده سازی UnitOfWork به وسیله MEF
یک تجربه
سالها پیش یکی از همکاران تعریف میکردند که یک شرکت نرم افزاری برای مشاوره معماری نرم افزار از ایشان دعوت به همکاری کرده است. پس از مراجعه به شرکت متوجه شدند که تیم اصلی برنامه نویسان درگیر تولید ORM ای برای پروژه جدید شرکت هستند که برای تولید این ابزار بیش از 4 ماه را وقت صرف کردهاند؛ اما در مراحل نهایی کار دچار مشکلات زیادی شده اند. به نحوی که از ایشان برای کمک به رفع مشکل ORM ( به جای تولید نرم افزار مشتری) دعوت کردهاند.
در آن زمان یادم هست که EF 5 (که تقریبا نسخه سوم بعد از 3.5 و 4 میباشد - جزئیات در اینجا) توسط مایکروسافت ارائه شده بود. همچنین NHibernate هم همزمان با EFها (تاریخچه نسخهها در اینجا) قابل دسترسی بودهاست. با این حال تیم فنی به این دلیل که کوئریهای تولیدی توسط EF کند هستند، اقدام به ساخت ORM کرده بودند. جالب اینکه با بررسی بیشتر مشخص شدهاست که حجم دادههای پروژه در بدترین حالت در یک جدول به 5 هزار رکورد میرسد.
4 ماه صرف وقت و هزینه تیم 2 نفره برای طراحی و پیاده سازی و تست ORM ای که در نهایت به دلیل مشکلات Performance کنار گذاشته شد و از EF استفاده کردند. شاید در این 4 ماه میتوانستند 30 درصد پروژه اصلی را پیاده سازی کنند.
شاید بتوان 3 دلیل عمده «فنی» شکست برخی از پروژههای نرم افزاری در ایران را به شرح زیر عنوان کرد:
- عدم استفاده مناسب از ابزارها و راهکارهای موجود و انجام دوباره کاری
- استفاده غیر ضروری و عجولانه از تکنولوژیهای جدید (بدون داشتن نیروی کار مسلط)
- پایین بودن سطح فنی و بهروز نبودن برخی از برنامه نویسان ایرانی
متن باز (Open Source)
با پیشرفت توسعه نرم افزار و تمایل شرکتهای بزرگ دنیا به تولید کامپوننتهای متن باز (Open Source) ریسک استفاده از این نوع ابزارها نیز کمتر شده است. بطوریکه درصورت نیاز میتوان کامپوننت را برای پروژهها سفارش سازی کرد.
شاید کمتر کسی باور میکرد که روزی شرکت مایکروسافت محصولات خود را Open Source کند. اما امروز، در سال 2017 میلادی، شرکت مایکروسافت اقدامات مهمی را در این زمینه انجام داده است که میتوانید جزئیات پروژههای متن باز این غول کامپیوتری دنیا را در اینجا و همچنین اینجا ملاحظه کنید.
یک سناریو
فرض کنید یک پروژه تحت وب را شروع کرده اید. بدون در نظر گرفتن جزئیات پروژه میتوان گفت به ابزارهای زیر نیاز خواهید داشت:
ابزار | مثال |
ORM | EF , NHibernate , Dapper , LLBLGEN |
IOC COntainer | Unity , StructureMap , Autofac , Castle.Windsor, LightInject , Ninject |
Report Tools | CrsytalReport , Stimusoft , DevExpress Report, Telerik Report Tools, EasyReport |
UI Component | Telerik , JqueryUI , Bootsrap ,CompnentArt, ComponentOne |
Error Logger | ELMAH , NLog , log4net |
Mapper Tools | AutoMapper , ValueInjecter |
ملاحظات استفاده از ابزارها
توجه به چند نکته در استفاده از ابزارها و کتابخانههای آماده ضروری میباشد، بدین شرح:
- ابزار مورد نیاز را با R&D (تحقیق و توسعه) انتخاب کنید. ابزارهایی که در پروژههای واقعی استفاده شدهاند، بسیار مناسب میباشند.
- توجه داشته باشیدکه استفاده از چندین ابزار باعث ایجاد تداخل در پروژه نشود (این مورد معمولا در کامپوننتهای UI مانند JqueryUI و Bootsrtap اتفاق میافتد)
- مستندات مربوط به ابزارها را حتما مطالعه کنید. لطفا بدون تسلط از ابزاری استفاده نکنید.
گاهی پیش میآید که یک برنامه نویس بدون مطالعه مستندات مربوط به یک IOC Container از آن ابزار استفاده میکند و در Register اولیه ویژگی LifeCycle مربوط به Context را با حالت Singleton مقداردهی میکند. بدین ترتیب پس از نیم ساعت، پروژه به دلیل آنچه که میتوان "چاقی Context" نامید، DONE یا حداقل کند میشود که رفع این مشکل ساعتها زمان میبرد.
درصورت امکان از ابزارها بصورت مستقیم استفاده نکنید. یک لایه واسط مخصوص خودتان را برای تنظیمات کلی ابزارها تهیه کنید که در آینده به دردتان خواهد خورد! (بیشتر در سمت سرور)
فرض کنید در پروژه WPF از کامپوننتهای زیبای DevExpress استفاده میکنید. به ازای هر کامپوننت یک کلاس به پروژه اضافه کنید که از کلاس اصلی آن کامپوننت Devexspress ارث میبرد و در لایه UI خود از کلاس جدید خود استفاده کنید. با این کار میتوانید ویژگیهای عمومی کامپوننتها را یکبار برای کل پروژه اعمال کنید.
نتیجه گیری
اگر بخواهیم چرخ را اختراع نکنیم و از تجربیات موفق موجود استفاده کنیم، میتوان نتیجه گرفت که استفاده از ابزارهای آماده برای توسعه نرم افزار با رعایت دستورالعمل استفاده امری مفید میباشد. اما باید توجه داشته باشیم که استفاده از هر ابزاری به هرقیمتی در هرپروژهای، حرفه ای نیست. همهی راهکارها، ابزراها و تکنولوژیهای مورد استفاده باید در راستای هدف اصلی «تولید و تحویل به موقع نرم افزار با کیفیت به مشتری» باشد؛ هدفی که در بسیاری از موارد فراموش شده و بیشتر زمان پروژه، صرف کارهای غیر ضروری میشود.
public abstract class APrototype : ICloneable { public string Name { get; set; } public string Health { get; set; } } public class Prototype : APrototype { public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}", Name, Health); } }
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK" }; Console.WriteLine(p1.ToString());
public abstract class APrototype : ICloneable { public string Name { get; set; } public string Health { get; set; } public abstract object Clone(); } public class Prototype : APrototype { public override object Clone() { return this.MemberwiseClone() as APrototype; } public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}", Name, Health); } }
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK" }; Prototype p2 = p1.Clone() as Prototype; Console.WriteLine(p1.ToString()); Console.WriteLine(p2.ToString());
public abstract class APrototype : ICloneable { public string Name { get; set; } public string Health { get; set; } public AdditionalDetails Detail { get; set; } public abstract object Clone(); } public class AdditionalDetails { public string Height { get; set; } } public class Prototype : APrototype { public override object Clone() { return this.MemberwiseClone() as APrototype; } public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}, Height: {2}", Name, Health, Detail.Height); } }
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } }; Prototype p2 = p1.Clone() as Prototype; p2.Detail.Height = "200"; Console.WriteLine(p1.ToString()); Console.WriteLine(p2.ToString());
public abstract class APrototype : ICloneable { public string Name { get; set; } public string Health { get; set; } //This is a ref type public AdditionalDetails Detail { get; set; } public abstract APrototype ShallowClone(); public abstract object Clone(); } public class AdditionalDetails { public string Height { get; set; } } public class Prototype : APrototype { public override object Clone() { Prototype cloned = MemberwiseClone() as Prototype; //We need to deep copy each ref types in order to prevent shallow copy cloned.Detail = new AdditionalDetails { Height = this.Detail.Height }; return cloned; } //Shallow copy will copy ref type's address instead of their value, so any changes in cloned object or source object will take effect on both objects public override APrototype ShallowClone() { return this.MemberwiseClone() as APrototype; } public override string ToString() { return string.Format("Player name: {0}, Health statuse: {1}, Height: {2}", Name, Health, Detail.Height); } }
Prototype p1 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } }; Prototype p2 = p1.Clone() as Prototype; p2.Detail.Height = "200"; Console.WriteLine("<This is Deep Copy>"); Console.WriteLine(p1.ToString()); Console.WriteLine(p2.ToString()); Prototype p3 = new Prototype { Name = "Vahid", Health = "OK", Detail = new AdditionalDetails { Height = "100" } }; Prototype p4 = p3.ShallowClone() as Prototype; p4.Detail.Height = "200"; Console.WriteLine("\n<This is Shallow Copy>"); Console.WriteLine(p3.ToString()); Console.WriteLine(p4.ToString());
متدی تحت عنوان ValidateEmail را تصور کنید. این متد از حیث بازگشت نتیجه به عنوان خروجی میتواند به اشکال مختلفی پیاده سازی شود که در ادامه مشاهده میکنیم:
متد ValidateEmail با خروجی Boolean
public bool ValidateEmail(string email) { var valid = true; if (string.IsNullOrWhiteSpace(email)) { valid = false; } var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; } var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; } return valid; }
همانطور که در تکه کد زیر مشخص میباشد، استفاده کننده از متد بالا، امکان بررسی خروجی آن را در قالب یک شرط خواهد داشت و علاوه بر اینکه پیاده سازی آن ساده میباشد، خوانایی کد را نیز بالا میبرد؛ ولی با این حال نمیتوان متوجه شد مشکل اصلی آدرس ایمیل ارسالی به عنوان آرگومان، دقیقا چیست.
var email = "email@example.com"; var isValid = ValidateEmail(email); if(isValid) { //do something }
متد ValidateEmail با صدور استثناء
public void ValidateEmail(string email) { if (string.IsNullOrWhiteSpace(email)) throw new ArgumentNullException(nameof(email)); var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) throw new ArgumentException("email is not in a correct format"); var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) throw new ArgumentException("email does not include a valid domain.") }
روش بالا هم جواب میدهد ولی بهتر است کلاس Exception سفارشی به عنوان مثال ValidationException برای این قضیه در نظر گرفته شود تا بتوان وهلههای صادر شده از این نوع را در لایههای بالاتر مدیریت کرد.
متد ValidateEmail با چندین خروجی
برای این منظور چندین راه حل پیش رو داریم.
با استفاده از پارامتر out:
public bool ValidateEmail(string email, out string message) { var valid = true; message = string.Empty; if (string.IsNullOrWhiteSpace(email)) { valid = false; message = "email is null."; } if (valid) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { valid = false; message = "email is not in a correct format"; } } if (valid) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { valid = false; message = "email does not include a valid domain."; } } return valid; }
var email = "email@example.com"; var isValid = ValidateEmail(email, out string message); if (isValid) { //do something }
Tuple<bool, List<string>> result = Tuple.Create<bool, List<string>>(true, new List<string>());
public class OperationResult { public bool Success { get; set; } public IList<string> Messages { get; } = new List<string>(); public void AddMessage(string message) { Messages.Add(message); } }
public OperationResult ValidateEmail(string email) { var result = new OperationResult(); if (string.IsNullOrWhiteSpace(email)) { result.Success = false; result.AddMessage("email is null."); } if (result.Success) { var isValidFormat = true;//todo: using RegularExpression if (!isValidFormat) { result.Success = false; result.AddMessage("email is not in a correct format"); } } if (result.Success) { var isRealDoamin = true;//todo: Code here that confirms whether domain exists. if (!isRealDoamin) { result.Success = false; result.AddMessage("email does not include a valid domain."); } } return result; }
این بار خروجی متد مذکور از نوع OperationResult ای میباشد که هم موفقیت آمیز بودن یا عدم آن را مشخص میکند و همچنین امکان دسترسی به لیست پیغامهای مرتبط با اعتبارسنجیهای انجام شده، وجود دارد.
استفاده از Exception برای نمایش پیغام برای کاربر نهایی
با صدور یک استثناء و مدیریت سراسری آن در بالاترین (خارجی ترین) لایه و نمایش پیغام مرتبط با آن به کاربر نهایی، میتوان از آن به عنوان ابزاری برای ارسال هر نوع پیغامی به کاربر نهایی استفاده کرد. اگر قوانین تجاری با موفقیت برآورده نشدهاند یا لازم است به هر دلیلی یک پیغام مرتبط با یک اعتبارسنجی تجاری را برای کاربر نمایش دهید، این روش بسیار کارساز میباشد و با یکبار وقت گذاشتن برای توسعه زیرساخت برای این موضوع به عنوان یک Cross Cutting Concern تحت عنوان Exception Management آزادی عمل زیادی در ادامه توسعه سیستم خود خواهید داشت.
به عنوان مثال داشتن یک کلاس Exception سفارشی تحت عنوان UserFriendlyException در این راستا یک الزام میباشد.
[Serializable] public class UserFriendlyException : Exception { public string Details { get; private set; } public int Code { get; set; } public UserFriendlyException() { } public UserFriendlyException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public UserFriendlyException(string message) : base(message) { } public UserFriendlyException(int code, string message) : this(message) { Code = code; } public UserFriendlyException(string message, string details) : this(message) { Details = details; } public UserFriendlyException(int code, string message, string details) : this(message, details) { Code = code; } public UserFriendlyException(string message, Exception innerException) : base(message, innerException) { } public UserFriendlyException(string message, string details, Exception innerException) : this(message, innerException) { Details = details; } }
و همچنین لازم است در بالاترین لایه سیستم خود به عنوان مثال برای یک پروژه ASP.NET MVC یا ASP.NET Core MVC میتوان یک ExceptionFilter سفارشی نیز تهیه کرد که هم به صورت سراسری استثناءهای سفارشی شما را مدیریت کند و همچنین خروجی مناسب Json برای استفاده در سمت کلاینت را نیز مهیا کند. به عنوان مثال برای درخواستهای Ajax ای لازم است در سمت کلاینت نیز پاسخهای رسیده از سمت سرور به صورت سراسری مدیریت شوند و برای سایر درخواستها همان نمایش صفحات خطای پیغام مرتبط با استثناء رخ داده شده کفایت میکند.
یک مدل پیشنهادی برای تهیه خروجی مناسب برای ارسال جزئیات استثنا رخ داده در درخواستهای Ajax ای
[Serializable] public class MvcAjaxResponse : MvcAjaxResponse<object> { public MvcAjaxResponse() { } public MvcAjaxResponse(bool success) : base(success) { } public MvcAjaxResponse(object result) : base(result) { } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) : base(error, unAuthorizedRequest) { } } [Serializable] public class MvcAjaxResponse<TResult> : MvcAjaxResponseBase { public MvcAjaxResponse(TResult result) { Result = result; Success = true; } public MvcAjaxResponse() { Success = true; } public MvcAjaxResponse(bool success) { Success = success; } public MvcAjaxResponse(ErrorInfo error, bool unAuthorizedRequest = false) { Error = error; UnAuthorizedRequest = unAuthorizedRequest; Success = false; } /// <summary> /// The actual result object of AJAX request. /// It is set if <see cref="MvcAjaxResponseBase.Success" /> is true. /// </summary> public TResult Result { get; set; } } public class MvcAjaxResponseBase { public string TargetUrl { get; set; } public bool Success { get; set; } public ErrorInfo Error { get; set; } public bool UnAuthorizedRequest { get; set; } public bool __mvc { get; } = true; }
[Serializable] public class ErrorInfo { public int Code { get; set; } public string Message { get; set; } public string Detail { get; set; } public Dictionary<string, string> ValidationErrors { get; set; } public ErrorInfo() { } public ErrorInfo(string message) { Message = message; } public ErrorInfo(int code) { Code = code; } public ErrorInfo(int code, string message) : this(message) { Code = code; } public ErrorInfo(string message, string details) : this(message) { Detail = details; } public ErrorInfo(int code, string message, string details) : this(message, details) { Code = code; } }
public async Task CheckIsDeactiveAsync(long id) { if (await _organizationalUnits.AnyAsync(a => a.Id == id && !a.IsActive).ConfigureAwait(false)) throw new UserFriendlyException("واحد سازمانی جاری غیرفعال میباشد."); }
روش نام گذاری متدهایی که امکان بازگشت خروجی Null را دارند
public User GetById(long id);
[Serializable] public class EntityNotFoundException : Exception { public Type EntityType { get; set; } public object Id { get; set; } public EntityNotFoundException() { } public EntityNotFoundException(string message) : base(message) { } public EntityNotFoundException(string message, Exception innerException) : base(message, innerException) { } public EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext context) : base(serializationInfo, context) { } public EntityNotFoundException(Type entityType, object id) : this(entityType, id, null) { } public EntityNotFoundException(Type entityType, object id, Exception innerException) : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) { EntityType = entityType; Id = id; } }
یک مثال واقعی
public async Task<UserOrganizationalUnitInfo> GetCurrentOrganizationalUnitInfoOrNullAsync(long userId) { return (await _setting.GetSettingValueForUserAsync( UserSettingNames.CurrentOrganizationalUnitInfo, userId).ConfigureAwait(false)) .FromJsonString<UserOrganizationalUnitInfo>(); }