static string[] QueryCustomers (Expression<Func<Purchase, bool>> purchaseCriteria) { var data = new MyDataContext(); var query = from c in data.Customers.AsExpandable() where c.Purchases.Any (purchaseCriteria.Compile()) select c.Name; return query.ToArray(); }
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک IDP ثالث در IdentityServer استفاده کنیم، نیاز است در ابتدا برنامهی IDP خود را به آن معرفی و در آنجا ثبت کنیم. برای این منظور مراحل زیر را طی خواهیم کرد:
1- مراجعه به developer console گوگل و ایجاد یک پروژهی جدید
https://console.developers.google.com
در صفحهی باز شده، بر روی دکمهی select project در صفحه و یا لینک select a project در نوار ابزار آن کلیک کنید. در اینجا دکمهی new project و یا create را مشاهده خواهید کرد. هر دوی این مفاهیم به صفحهی زیر ختم میشوند:
در اینجا نامی دلخواه را وارد کرده و بر روی دکمهی create کلیک کنید.
2- فعالسازی API بر روی این پروژهی جدید
در ادامه بر روی لینک Enable APIs And Services کلیک کنید و سپس google+ api را جستجو نمائید.
پس از ظاهر شدن آن، این گزینه را انتخاب و در صفحهی بعدی، آنرا با کلیک بر روی دکمهی enable، فعال کنید.
3- ایجاد credentials
در اینجا بر روی دکمهی create credentials کلیک کرده و در صفحهی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
• Which API are you using? – Google+ API • Where will you be calling the API from? – Web server (e.g. node.js, Tomcat) • What data will you be accessing? – User data
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
https://localhost:6001/signin-google
سپس در ذیل این صفحه بر روی دکمهی «Create OAuth 2.0 Client ID» کلیک کنید تا به صفحهی «Set up the OAuth 2.0 consent screen» بعدی هدایت شوید. در اینجا دو گزینهی آنرا به صورت زیر تکمیل کنید:
- Email address: همان آدرس ایمیل واقعی شما است.
- Product name shown to users: یک نام دلخواه است. نام برنامهی خود را برای نمونه ImageGallery وارد کنید.
برای ادامه بر روی دکمهی Continue کلیک نمائید.
4- دریافت credentials
در پایان این گردش کاری، به صفحهی نهایی «Download credentials» میرسیم. در اینجا بر روی دکمهی download کلیک کنید تا ClientId و ClientSecret خود را توسط فایلی به نام client_id.json دریافت نمائید.
سپس بر روی دکمهی Done در ذیل صفحه کلیک کنید تا این پروسه خاتمه یابد.
تنظیم برنامهی IDP برای استفادهی از محتویات فایل client_id.json
پس از پایان عملیات ایجاد یک برنامهی جدید در گوگل و فعالسازی Google+ API در آن، یک فایل client_id.json را دریافت میکنیم که اطلاعات آن باید به صورت زیر به فایل آغازین برنامهی IDP اضافه شود:
الف) تکمیل فایل src\IDP\DNT.IDP\appsettings.json
{ "Authentication": { "Google": { "ClientId": "xxxx", "ClientSecret": "xxxx" } } }
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
namespace DNT.IDP { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddAuthentication() .AddGoogle(authenticationScheme: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = Configuration["Authentication:Google:ClientId"]; options.ClientSecret = Configuration["Authentication:Google:ClientSecret"]; }); }
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
public const string ExternalCookieAuthenticationScheme = "idsrv.external";
آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آنها
اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامهها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحهی لاگین IDP ما اضافه شدهاست:
در اینجا با کلیک بر روی دکمهی گوگل، به صفحهی لاگین آن که به همراه نام برنامهی ما است و انتخاب اکانتی از آن هدایت میشویم:
پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخداده، ما را به سمت صفحهی ثبت اطلاعات کاربر جدید هدایت میکند. این تنظیمات را در قسمت قبل ایجاد کردیم:
namespace DNT.IDP.Controllers.Account { [SecurityHeaders] [AllowAnonymous] public class ExternalController : Controller { public async Task<IActionResult> Callback() { var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme); var returnUrl = result.Properties.Items["returnUrl"] ?? "~/"; var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user = AutoProvisionUser(provider, providerUserId, claims); var returnUrlAfterRegistration = Url.Action("Callback", new { returnUrl = returnUrl }); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration" , new { returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId }); return Redirect(continueWithUrl); }
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد میشود.
اتصال کاربر وارد شدهی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است
تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحهی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد میکنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت میشود، دیگر صفحهی ثبت نام و تکمیل اطلاعات را مشاهده نمیکند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شدهاست، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفتهاست. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); foreach (var claim in claims) { _logger.LogInformation($"External provider[{provider}] info-> claim:{claim.Type}, value:{claim.Value}"); }
External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name, value:Vahid N. External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname, value:Vahid External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname, value:N. External provider[Google] info-> claim:urn:google:profile, value:https://plus.google.com/105013528531611201860 External provider[Google] info-> claim:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, value:my.name@gmail.com
[HttpGet] public async Task<IActionResult> Callback() { // ... var (user, provider, providerUserId, claims) = await FindUserFromExternalProvider(result); if (user == null) { // user wasn't found by provider, but maybe one exists with the same email address? if (provider == "Google") { // email claim from Google var email = claims.FirstOrDefault(c => c.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); if (email != null) { var userByEmail = await _usersService.GetUserByEmailAsync(email.Value); if (userByEmail != null) { // add Google as a provider for this user await _usersService.AddUserLoginAsync(userByEmail.SubjectId, provider, providerUserId); // redirect to ExternalLoginCallback var continueWithUrlAfterAddingUserLogin = Url.Action("Callback", new {returnUrl = returnUrl}); return Redirect(continueWithUrlAfterAddingUserLogin); } } } var returnUrlAfterRegistration = Url.Action("Callback", new {returnUrl = returnUrl}); var continueWithUrl = Url.Action("RegisterUser", "UserRegistration", new {returnUrl = returnUrlAfterRegistration, provider = provider, providerUserId = providerUserId}); return Redirect(continueWithUrl); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشهی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آنرا اجرا کنید تا برنامهی IDP راه اندازی شود.
- در آخر به پوشهی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آنرا اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحهی login نام کاربری را User 1 و کلمهی عبور آنرا password وارد کنید.
یک اپلیکیشن با SQL Membership بسازید
حال با استفاده از ابزار ASP.NET Configuration دو کاربر جدید بسازید: oldAdminUser و oldUser.
نقش جدیدی با نام Admin بسازید و کاربر oldAdminUser را به آن اضافه کنید.
بخش جدیدی با نام Admin در سایت خود بسازید و فرمی بنام Default.aspx به آن اضافه کنید. همچنین فایل web.config این قسمت را طوری پیکربندی کنید تا تنها کاربرانی که در نقش Admin هستند به آن دسترسی داشته باشند. برای اطلاعات بیشتر به این لینک مراجعه کنید.
پنجره Server Explorer را باز کنید و جداول ساخته شده توسط SQL Membership را بررسی کنید. اطلاعات اصلی کاربران که برای ورود به سایت استفاده میشوند، در جداول aspnet_Users و aspnet_Membership ذخیره میشوند. دادههای مربوط به نقشها نیز در جدول aspnet_Roles ذخیره خواهند شد. رابطه بین کاربران و نقشها نیز در جدول aspnet_UsersInRoles ذخیره میشود، یعنی اینکه هر کاربری به چه نقش هایی تعلق دارد.
برای مدیریت اساسی سیستم عضویت، مهاجرت جداول ذکر شده به سیستم جدید ASP.NET Identity کفایت میکند.
مهاجرت به Visual Studio 2013
- برای شروع ابتدا Visual Studio Express 2013 for Web یا Visual Studio 2013 را نصب کنید.
- حال پروژه ایجاد شده را در نسخه جدید ویژوال استودیو باز کنید. اگر نسخه ای از SQL Server Express را روی سیستم خود نصب نکرده باشید، هنگام باز کردن پروژه پیغامی به شما نشان داده میشود. دلیل آن وجود رشته اتصالی است که از SQL Server Express استفاده میکند. برای رفع این مساله میتوانید SQL Express را نصب کنید، و یا رشته اتصال را طوری تغییر دهید که از LocalDB استفاده کند.
- فایل web.config را باز کرده و رشته اتصال را مانند تصویر زیر ویرایش کنید.
- پنجره Server Explorer را باز کنید و مطمئن شوید که الگوی جداول و دادهها قابل رویت هستند.
- سیستم ASP.NET Identity با نسخه 4.5 دات نت فریم ورک و بالاتر سازگار است. پس نسخه فریم ورک پروژه را به آخرین نسخه (4.5.1) تغییر دهید.
پروژه را Build کنید تا مطمئن شوید هیچ خطایی وجود ندارد.
نصب پکیجهای NuGet
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
مهاجرت دیتابیس فعلی به سیستم ASP.NET Identity
در پنجره کوئری باز شده، تمام محتویات فایل Migrations.sql را کپی کنید. سپس اسکریپت را با کلیک کردن دکمه Execute اجرا کنید.
ممکن است با اخطاری مواجه شوید مبنی بر آنکه امکان حذف (drop) بعضی از جداول وجود نداشت. دلیلش آن است که چهار عبارت اولیه در این اسکریپت، تمام جداول مربوط به Identity را در صورت وجود حذف میکنند. از آنجا که با اجرای اولیه این اسکریپت چنین جداولی وجود ندارند، میتوانیم این خطاها را نادیده بگیریم. حال پنجره Server Explorer را تازه (refresh) کنید و خواهید دید که پنج جدول جدید ساخته شده اند.
لیست زیر نحوه Map کردن اطلاعات از جداول SQL Membership به سیستم Identity را نشان میدهد.
- aspnet_Roles --> AspNetRoles
- aspnet_Users, aspnet_Membership --> AspNetUsers
- aspnet_UsersInRoles --> AspNetUserRoles
ساختن مدلها و صفحات عضویت
کلاس User باید کلاس IdentityUser را که در اسمبلی Microsoft.AspNet.Identity.EntityFramework وجود دارد گسترش دهد. خاصیت هایی را تعریف کنید که نماینده الگوی جدول AspNetUser هستند. خواص ID, Username, PasswordHash و SecurityStamp در کلاس IdentityUser تعریف شده اند، بنابراین این خواص را در لیست زیر نمیبینید.
public class User : IdentityUser { public User() { CreateDate = DateTime.Now; IsApproved = false; LastLoginDate = DateTime.Now; LastActivityDate = DateTime.Now; LastPasswordChangedDate = DateTime.Now; LastLockoutDate = DateTime.Parse("1/1/1754"); FailedPasswordAnswerAttemptWindowStart = DateTime.Parse("1/1/1754"); FailedPasswordAttemptWindowStart = DateTime.Parse("1/1/1754"); } public System.Guid ApplicationId { get; set; } public string MobileAlias { get; set; } public bool IsAnonymous { get; set; } public System.DateTime LastActivityDate { get; set; } public string MobilePIN { get; set; } public string Email { get; set; } public string LoweredEmail { get; set; } public string LoweredUserName { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime CreateDate { get; set; } public System.DateTime LastLoginDate { get; set; } public System.DateTime LastPasswordChangedDate { get; set; } public System.DateTime LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } }
حال برای دسترسی به دیتابیس مورد نظر، نیاز به یک DbContext داریم. اسمبلی Microsoft.AspNet.Identity.EntityFramework کلاسی با نام IdentityDbContext دارد که پیاده سازی پیش فرض برای دسترسی به دیتابیس ASP.NET Identity است. نکته قابل توجه این است که IdentityDbContext آبجکتی از نوع TUser را میپذیرد. TUser میتواند هر کلاسی باشد که از IdentityUser ارث بری کرده و آن را گسترش میدهد.
در پوشه Models کلاس جدیدی با نام ApplicationDbContext بسازید که از IdentityDbContext ارث بری کرده و از کلاس User استفاده میکند.
public class ApplicationDbContext : IdentityDbContext<User> { }
مدیریت کاربران در ASP.NET Identity توسط کلاسی با نام UserManager انجام میشود که در اسمبلی Microsoft.AspNet.Identity.EntityFramework قرار دارد. چیزی که ما در این مرحله نیاز داریم، کلاسی است که از UserManager ارث بری میکند و آن را طوری توسعه میدهد که از کلاس User استفاده کند.
در پوشه Models کلاس جدیدی با نام UserManager بسازید.
public class UserManager : UserManager<User> { }
کلمه عبور کاربران بصورت رمز نگاری شده در دیتابیس ذخیره میشوند. الگوریتم رمز نگاری SQL Membership با سیستم ASP.NET Identity تفاوت دارد. هنگامی که کاربران قدیمی به سایت وارد میشوند، کلمه عبورشان را توسط الگوریتمهای قدیمی SQL Membership رمزگشایی میکنیم، اما کاربران جدید از الگوریتمهای ASP.NET Identity استفاده خواهند کرد.
کلاس UserManager خاصیتی با نام PasswordHasher دارد. این خاصیت نمونه ای از یک کلاس را ذخیره میکند، که اینترفیس IPasswordHasher را پیاده سازی کرده است. این کلاس هنگام تراکنشهای احراز هویت کاربران استفاده میشود تا کلمههای عبور را رمزنگاری/رمزگشایی شوند. در کلاس UserManager کلاس جدیدی بنام SQLPasswordHasher بسازید. کد کامل را در لیست زیر مشاهده میکنید.
public class SQLPasswordHasher : PasswordHasher { public override string HashPassword(string password) { return base.HashPassword(password); } public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) { string[] passwordProperties = hashedPassword.Split('|'); if (passwordProperties.Length != 3) { return base.VerifyHashedPassword(hashedPassword, providedPassword); } else { string passwordHash = passwordProperties[0]; int passwordformat = 1; string salt = passwordProperties[2]; if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase)) { return PasswordVerificationResult.SuccessRehashNeeded; } else { return PasswordVerificationResult.Failed; } } } //This is copied from the existing SQL providers and is provided only for back-compat. private string EncryptPassword(string pass, int passwordFormat, string salt) { if (passwordFormat == 0) // MembershipPasswordFormat.Clear return pass; byte[] bIn = Encoding.Unicode.GetBytes(pass); byte[] bSalt = Convert.FromBase64String(salt); byte[] bRet = null; if (passwordFormat == 1) { // MembershipPasswordFormat.Hashed HashAlgorithm hm = HashAlgorithm.Create("SHA1"); if (hm is KeyedHashAlgorithm) { KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm; if (kha.Key.Length == bSalt.Length) { kha.Key = bSalt; } else if (kha.Key.Length < bSalt.Length) { byte[] bKey = new byte[kha.Key.Length]; Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length); kha.Key = bKey; } else { byte[] bKey = new byte[kha.Key.Length]; for (int iter = 0; iter < bKey.Length; ) { int len = Math.Min(bSalt.Length, bKey.Length - iter); Buffer.BlockCopy(bSalt, 0, bKey, iter, len); iter += len; } kha.Key = bKey; } bRet = kha.ComputeHash(bIn); } else { byte[] bAll = new byte[bSalt.Length + bIn.Length]; Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length); Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length); bRet = hm.ComputeHash(bAll); } } return Convert.ToBase64String(bRet); } }
دقت کنید تا فضاهای نام System.Text و System.Security.Cryptography را وارد کرده باشید.
متد EncodePassword کلمه عبور را بر اساس پیاده سازی پیش فرض SQL Membership رمزنگاری میکند. این الگوریتم از System.Web گرفته میشود. اگر اپلیکیشن قدیمی شما از الگوریتم خاصی استفاده میکرده است، همینجا باید آن را منعکس کنید. دو متد دیگر نیز بنامهای HashPassword و VerifyHashedPassword نیاز داریم. این متدها از EncodePassword برای رمزنگاری کلمههای عبور و تایید آنها در دیتابیس استفاده میکنند.
سیستم SQL Membership برای رمزنگاری (Hash) کلمههای عبور هنگام ثبت نام و تغییر آنها توسط کاربران، از PasswordHash, PasswordSalt و PasswordFormat استفاده میکرد. در روند مهاجرت، این سه فیلد در ستون PasswordHash جدول AspNetUsers ذخیره شده و با کاراکتر '|' جدا شده اند. هنگام ورود کاربری به سایت، اگر کله عبور شامل این فیلدها باشد از الگوریتم SQL Membership برای بررسی آن استفاده میکنیم. در غیر اینصورت از پیاده سازی پیش فرض ASP.NET Identity استفاده خواهد شد. با این روش، کاربران قدیمی لازم نیست کلمههای عبور خود را صرفا بدلیل مهاجرت اپلیکیشن ما تغییر دهند.
کلاس UserManager را مانند قطعه کد زیر بروز رسانی کنید.
public UserManager() : base(new UserStore<User>(new ApplicationDbContext())) { this.PasswordHasher = new SQLPasswordHasher(); }
ایجاد صفحات جدید مدیریت کاربران
- فایلهای Register.aspx.cs و Login.aspx.cs از کلاس UserManager استفاده میکنند. این ارجاعات را با کلاس UserManager جدیدی که در پوشه Models ساختید جایگزین کنید.
- همچنین ارجاعات استفاده از کلاس IdentityUser را به کلاس User که در پوشه Models ساختید تغییر دهید.
- لازم است توسعه دهنده مقدار ApplicationId را برای کاربران جدید طوری تنظیم کند که با شناسه اپلیکیشن جاری تطابق داشته باشد. برای این کار میتوانید پیش از ساختن حسابهای کاربری جدید در فایل Register.aspx.cs ابتدا شناسه اپلیکیشن را بدست آورید و اطلاعات کاربر را بدرستی تنظیم کنید.
private Guid GetApplicationID() { using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["ApplicationServices"].ConnectionString)) { string queryString = "SELECT ApplicationId from aspnet_Applications WHERE ApplicationName = '/'"; //Set application name as in database SqlCommand command = new SqlCommand(queryString, connection); command.Connection.Open(); var reader = command.ExecuteReader(); while (reader.Read()) { return reader.GetGuid(0); } return Guid.NewGuid(); } }
var currentApplicationId = GetApplicationID(); User user = new User() { UserName = Username.Text, ApplicationId=currentApplicationId, …};
خطای data binding
((System.ComponentModel.ISupportInitialize)(this.axAcroPDF1)).EndInit();
Cannot create an instance of "ViewModel".
public bool IsDesignTime { get { return (Application.Current == null) || (Application.Current.GetType() == typeof(Application)); } } //Constructor public ViewModelClass() { if(IsDesignTime == false) { //Your Code } }
برای نمونه در متد ذیل، میزان حجم مصرفی در یک پوشه بازگشت داده میشود:
public async Task<long> GetDirectorySize(string path, string searchPattern) { if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any()) return 0; else return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length))); }
AsyncTaskMethodBuilder<long>.Create()
باید دقت داشت که Task، یک نوع ارجاعی است و استفادهی از آن به معنای تخصیص حافظهاست. اما زمانیکه قسمتی از کد کاملا همزمان اجرا میشود و یا مقداری کش شده را بازگشت میدهد، این تخصیص حافظهی اضافی، خصوصا اگر در حلقهها بکار گرفته شود، هزینهبر خواهد بود.
امکان تعریف خروجیهای سفارشی متدهای async در C# 7.0
در C# 7 میتوان خروجیهای سفارشی را جهت متدهای async تعریف کرد و پیشنیاز اصلی آن پیاده سازی متد GetAwater است. برای مثال <System.Threading.Tasks.ValueTask<T یک چنین نوع سفارشی را ارائه میدهد. در این حالت، متد ابتدای بحث را میتوان به صورت ذیل بازنویسی کرد:
public async ValueTask<long> GetDirectorySize(string path, string searchPattern) { if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any()) return 0; else return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern, SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length))); }
همانطور که از نام ValueTask نیز مشخص است، یک struct است؛ برخلاف Task و تخصیص حافظهی آن بر روی stack بجای heap صورت میگیرد. به این ترتیب با کاهش فشار بر روی GC، در حلقههایی که خروجی value type دارند، با اندازه گیریهای انجام شده، کارآیی تا 50 درصد هم میتواند بهبود یابد.
برای کامپایل قطعه کد فوق و تامین نوع جدید ValueTask، نیاز به نصب بستهی نیوگت ذیل نیز میباشد:
PM> install-package System.Threading.Tasks.Extensions
مکانیزم Eventing
PM> Install-Package DNTFrameworkCore
public class TaskEditingBusinessEventHandler : BusinessEventHandler<EditingBusinessEvent<TaskModel, int>> { private readonly ILogger<TaskEditingBusinessEventHandler> _logger; public TaskEditingBusinessEventHandler(ILogger<TaskEditingBusinessEventHandler> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public override Task<Result> Handle(EditingBusinessEvent<TaskModel, int> @event) { foreach (var model in @event.Models) { _logger.LogInformation($"Title changed from: {model.OriginalValue.Title} to: {model.NewValue.Title}"); } return Task.FromResult(Ok()); } }
کار با پیادهسازی واسط جنریک IBusinessEventHandler یا ارثبری از کلاس جنریک BusinessEventHandler آغاز میشود؛ سپس نیاز است Type Parameter متناظر را نیز مشخص کنیم. برای این منظور در تکه کد بالا از رخداد جنریک EditingBusinessEvent استفاده شده است. همچنین همانطور که ملاحظه میکنید، نیاز است نوع Model مورد نظر نیز مشخص شده باشد؛ در اینجا از TaskModel به عنوان Model/DTO عملیات CUD موجودیت Task استفاده شده است.
رخدادهای Creating/Created/Deleting/Deleted دارای خصوصیتی بنام Models هستند که نوع آن IEnumerable<TModel> میباشد. ولی این خصوصیت در رخدادهای Editing/Edited از نوع IEnumerable<ModifiedModel<TModel>> میباشد؛ در این صورت به مقادیر موجود در بانک اطلاعاتی و همچنین مقادیری که توسط استفاده کننده از سرویس جاری به عنوان آرگومان به متد ویرایش ارسال شده است، دسترسی خواهیم داشت.
public class ModifiedModel<TValue> { public TValue NewValue { get; set; } public TValue OriginalValue { get; set; } }
استفاده از سرویسهای موجودیتها
OOP : Everything is an object CRUD-based thinking : Everything is CRUD
public class ItemCategoryCreatedBusinessEventHandler : IBusinessEventHandler<CreatedBusinessEvent<ItemCategoryModel, int>> { private readonly ISaleMethodService _saleMethodService; public TaskEditingBusinessEventHandler(ISaleMethodService saleMethodService) { _saleMethodService = saleMethodService ?? throw new ArgumentNullException(nameof(saleMethodService)); } public override Task<Result> Handle(CreatedBusinessEvent<ItemCategoryModel, int> @event) { var methods = _saleMethodService.FindAsnc(); foreach (var method in methods) { foreach (var model in @event.Models) { method.ItemCategories.Add(new SaleMethodItemCategoryModel { ItemCategoryId = model.Id, TrackingState = TrackingState.Added; }); } } return _saleMethodService.EditAsync(methods); }
git log --oneline
Install-Module -Name Microsoft.PowerShell.Crescendo
$Configuration = @{ '$schema' = "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11" Commands = @() } $parameters = @{ Verb = "Get" Noun = "GitLog" OriginalName = "git" } $Configuration.Commands += New-CrescendoCommand @parameters $Configuration | ConvertTo-Json -Depth 3 | Out-File ./git-ps.json
{ "Commands": [ { "Verb": "Get", "Noun": "GitLog", "OriginalName": "git", "OriginalCommandElements": null, "Platform": [ "Windows", "Linux", "MacOS" ], "Elevation": null, "Aliases": null, "DefaultParameterSetName": null, "SupportsShouldProcess": false, "ConfirmImpact": null, "SupportsTransactions": false, "NoInvocation": false, "Description": null, "Usage": null, "Parameters": [], "Examples": [], "OriginalText": null, "HelpLinks": null, "OutputHandlers": null } ], "$schema": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11" }
"": "https://aka.ms/PowerShell/Crescendo/Schemas/2021-11",
اکنون باید این فایل Configuration را به Crescendo معرفی کنیم تا cmdlet را برایمان تولید کند. اینکار را توسط Export-CrescendoModule انجام خواهیم داد:
Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1
با اجرای دستور فوق، فایلهای git.psm1 و همچنین git.psd1 تولید خواهند شد. نیاز به بررسی فایلهای جنریت شده نیست؛ چون تنها جایی که با آن باید در ارتباط باشیم، همان فایل JSON ابتدای بحث است که در ادامه آن را بررسی خواهیم کرد. اما قبل از آن اجازه دهید ماژول تولید شده را Import کنیم و دستور Get-GitLog را وارد کنیم:
PP /> Import-Module ./git-ps.psd1 PS /> Get-GitLog usage: git [-v | --version] [-h | --help] [-C <path>] [-c <name>=<value>] [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path] [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare] [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>] [--super-prefix=<path>] [--config-env=<name>=<envvar>] <command> [<args>] These are common Git commands used in various situations: start a working area (see also: git help tutorial) clone Clone a repository into a new directory init Create an empty Git repository or reinitialize an existing one work on the current change (see also: git help everyday) add Add file contents to the index mv Move or rename a file, a directory, or a symlink restore Restore working tree files rm Remove files from the working tree and from the index examine the history and state (see also: git help revisions) bisect Use binary search to find the commit that introduced a bug diff Show changes between commits, commit and working tree, etc grep Print lines matching a pattern log Show commit logs show Show various types of objects status Show the working tree status grow, mark and tweak your common history branch List, create, or delete branches commit Record changes to the repository merge Join two or more development histories together rebase Reapply commits on top of another base tip reset Reset current HEAD to the specified state switch Switch branches tag Create, list, delete or verify a tag object signed with GPG collaborate (see also: git help workflows) fetch Download objects and refs from another repository pull Fetch from and integrate with another repository or a local branch push Update remote refs along with associated objects 'git help -a' and 'git help -g' list available subcommands and some concept guides. See 'git help <command>' or 'git help <concept>' to read about a specific subcommand or concept. See 'git help git' for an overview of the system.
همانطور که مشاهده میکنید، خروجی دستور git، نمایش داده شدهاست. دلیل آن نیز این است که در فایل configuration، هیچ آرگومانی را به عنوان ورودی آن تعیین نکردهایم. برای اضافه کردن آرگومانهای موردنظر باید پراپرتی OrginalCommandElements را مقدار دهی کنیم:
"OriginalCommandElements": ["log", "--oneline"],
بنابراین با فراخوانی دستور Get-GitLog، در اصل دستور git log —oneline فراخوانی خواهد شد:
PS /> Get-GitLog e9590e8 init
اما تا اینجا نیز خروجی به صورت رشتهایی است. برای داشتن یک خروجی Object، باید پراپرتی OutputHandlers را از Configuration، تغییر دهیم:
"OutputHandlers": [ { "ParameterSetName": "Default", "Handler": "$args[0] | ForEach-Object { $hash, $message = $_.Split(' ', 2) ; [PSCustomObject]@{ Hash = $hash; Message = $message } }" } ]
در اینجا توسط args$ به خروجی کامند اصلی دسترسی خواهیم داشت. این خروجی را سپس با کمک ForEach-Object، به یک شیء با پراپرتیهای Hash و Message تبدیل کردهایم. در اینجا فقط میخواستم روال تهیه یک آبجکت را از کامندهایی که خروجی JSON ندارند، نشان دهم؛ اما خوشبختانه توسط پرچم pretty در git log، امکان تهیهی خروجی JSON را نیز داریم:
git log --pretty=format:'{"commit": "%h", "author": "%an", "date": "%ad", "message": "%s"}'
در نتیجه عملاً نیازی به split کردن نیست و بجای آن میتوانیم به صورت مستقیم، خروجی را توسط ConvertFrom-Json پارز کنیم:
"OutputHandlers": [ { "ParameterSetName": "Default", "Handler": "$args[0] | ConvertFrom-Json" } ]
همچنین درون فایل schema با کمک پراپرتی Parameters، امکان تعریف پارامتر را نیز برای کامند Get-GitLog خواهیم داشت. به عنوان مثال میتوانیم فلگ reverse را نیز به کامند اصلی از طریق PowerShell ارسال کنیم:
"Parameters": [ { "Name": "reverse", "OriginalName": "--reverse", "ParameterType": "switch", "Description": "Reverse the order of the commits in the output." } ],
دقت داشته باشیم که با هربار تغییر فایل schema باید توسط دستور Export-CrescendoModule ماژول موردنظر را تولید کنید:
Export-CrescendoModule -Configuration ./git-ps.json -ModuleName ./git-ps.psm1 Import-Module ./git-ps.psd1
در نهایت cmdletمان به این صورت قابل استفاده خواهد بود:
System.Action<object>
مثال :
namespace Listing_03 { class Listing_03 { static void Main(string[] args) { // use an Action delegate and a named method Task task1 = new Task(new Action<object>(printMessage),"First task"); // use an anonymous delegate Task task2 = new Task(delegate (object obj) { printMessage(obj); }, "Second Task"); // use a lambda expression and a named method // note that parameters to a lambda don't need // to be quoted if there is only one parameter Task task3 = new Task((obj) => printMessage(obj), "Third task"); // use a lambda expression and an anonymous method Task task4 = new Task((obj) => { printMessage(obj); }, "Fourth task"); task1.Start(); task2.Start(); task3.Start(); task4.Start(); // wait for input before exiting Console.WriteLine("Main method complete. Press enter to finish."); Console.ReadLine(); } static void printMessage(object message) { Console.WriteLine("Message: {0}", message); } } }
using System; using System.Threading.Tasks; namespace Listing_04 { class Listing_04 { static void Main(string[] args) { string[] messages = { "First task", "Second task", "Third task", "Fourth task" }; foreach (string msg in messages) { Task myTask = new Task(obj => printMessage((string)obj), msg); myTask.Start(); } // wait for input before exiting Console.WriteLine("Main method complete. Press enter to finish."); Console.ReadLine(); } static void printMessage(string message) { Console.WriteLine("Message: {0}", message); } } }
Main method complete. Press enter to finish. Message: Second task Message: Fourth task Message: First task Message: Third task
عملگرهای تولید، برای ما توالی ایجاد میکنند و تفاوتهای عمدهای با سایر عملگرهای پرس و جو دارند که در بخش زیر به آنها اشاره میکنیم:
1- هیچ توالی ورودی را دریافت نمیکنند.
2- این عملگرها بصورت متد الحاقی پیاده سازی نشدهاند و بصورت متدهای استاتیک در کلاس Enumerable قرار گرفتهاند.
امضاء زیر مربوط به متد Empty میباشد:
public static IEnumerable<TResult> Empty<TResult>()
Empty
عملگر Empty یک توالی بدون عنصر (Empty) را بر اساس نوع مشخص شده، ایجاد میکند.
در کد زیر نحوه ایجاد یک توالی خالی از نوع Ingredient نشان داده شده است.
IEnumerable<Ingredient> ingredients = Enumerable.Empty<Ingredient>(); Console.WriteLine(ingredients.Count());
0
معادل عملگر Empty، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
Range
عملگر پرس و جوی Range، یک توالی از مقادیر صحیح متوالی را برای ما ایجاد میکند. اولین پارامتر این عملگر عنصر آغاز کننده توالی است و دومین پارامتر این عملگر تعداد کل عناصر توالی تولید شده، با احتساب عنصر اول خواهد بود.
مثال:
IEnumerable<int> fiveToTen = Enumerable.Range(5,6); foreach (var num in fiveToTen) { Console.WriteLine(num); }
5 6 7 8 9 10
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Range، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
Repeat
عملگر پرس و جوی Repeat یک عدد را به تعداد بار مشخصی در توالی خروجی تکرار میکند.
مثال:
IEnumerable<int> fiveToTen = Enumerable.Repeat(42, 6); foreach (var num in fiveToTen) { Console.WriteLine(num); }
42 42 42 42 42 42
معادل عملگر Repeat، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگرهای کمی (Quantifier Operators)
عملگرهای Quantifier یک توالی ورودی را گرفته، آن را ارزیابی کرده و یک مقدار منطقی را باز میگردانند.
عملگر Contains
عملگر Contains عناصر یک توالی را ارزیابی میکند و در صورتیکه مقدار مورد نظر ما در توالی وجود داشته باشد، ارزش True باز میگرداند.
مثال:
int[] nums = {1, 2, 3}; bool isTowThere = nums.Contains(2); bool isFiveThere = nums.Contains(5); Console.WriteLine(isTowThere); Console.WriteLine(isFiveThere);
True False
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Contains، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Any
عملگر Any دو امضاء مختلف را دارد:
1- اولین امضاء: در صورتیکه توالی شامل حداقل یک عنصر باشد، ارزش True بازگردانده میشود.
2- دومین امضاء: یک عبارت پیش بینی را قبول میکند. در صورتیکه حداقل یکی از عناصر توالی، عبارت پیش بینی را تامین کند، ارزش صحیح باز گردانده میشود.
مثال: بررسی امضاء اول عملگر Any
int[] nums = { 1, 2, 3 }; IEnumerable<int> noNums = Enumerable.Empty<int>(); Console.WriteLine(nums.Any()); Console.WriteLine(noNums.Any());
True False
int[] nums = { 1, 2, 3 }; bool areAnyEvenNumbers = nums.Any(x => x % 2 == 0); Console.WriteLine(areAnyEvenNumbers);
True
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Any، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر All
عملگر پرس و جوی All، یک عبارت پیش بینی را دریافت میکند و عناصر توالی ورودی را بر مبنای آن ارزیابی میکند تا مشخص شود همه عناصر، شرط پیش بینی را تامین میکنند.
در کد زیر بررسی میکنیم که آیا همه عناصر توالی مواد غذایی، جزء مواد غذایی کم چرب میباشند یا خیر .
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient { Name = "Egg", Calories = 100 }, new Ingredient { Name = "Milk", Calories = 150 }, new Ingredient { Name = "Flour", Calories = 50 }, new Ingredient { Name = "Butter", Calories = 400 } }; bool isLowFatRecipe = ingredients.All(x => x.Calories < 200); Console.WriteLine(isLowFatRecipe);
False
نکته : عملگر All به محض پیدا کردن عنصری که شرط مشخص شده را نقض کند، ارزش False را باز میگرداند و ادامه بررسی عناصر باقی مانده را متوقف میکند.
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Any، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر SequenceEqual
عملگر SequenceEqual دو توالی را با هم مقایسه کرده و در صورتیکه عناصر هر دو توالی برابر و ترتیب قرار گیری آنها نیز یکسان باشند، ارزش True باز گردانده میشود.
مثال:
IEnumerable<int> sequence1 = new[] {1, 2, 3}; IEnumerable<int> sequence2 = new[] { 1, 2, 3 }; bool isSeqEqual = sequence1.SequenceEqual(sequence2); Console.WriteLine(isSeqEqual);
True
در صورتی که دو توالی عناصر یکسانی داشته باشند، ولی ترتیب قرار گیری عناصر با هم یکسان نباشند، عملگر ارزش False را باز میگرداند.
مثال :
IEnumerable<int> sequence1 = new[] { 1, 2, 3 }; IEnumerable<int> sequence2 = new[] { 3, 2, 1 }; bool isSeqEqual = sequence1.SequenceEqual(sequence2); Console.WriteLine(isSeqEqual);
False
معادل عملگر SequenceEqual، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگرهای تجمیع/تجمعی Aggregate Operators
عملگرهای Aggregate یک توالی ورودی را دریافت و یک مقدار عددی (Scalar Value) را باز میگردانند. مقدار بازگردانده شده، حاصل یک عملیات محاسباتی میباشد.
لیستی از عملگرهای تجمیع ( Aggregate Operators ):
• Count
• LongCount
• Sum
• Min
• Max
• Average
• Aggregate
عملگر Count
عملگر Count، تعداد عناصر توالی ورودی را باز میگرداند. عملگر Count، دو امضاء مختلف دارد. یکی از این امضاءها یک عبارت پیش بینی را میپذیرد.
کد زیر، امضاء اول عملگر Count را نشان میدهد:
int[] nums = { 1, 2, 3 }; int numberOfElements = nums.Count(); Console.WriteLine(numberOfElements);
3
وقتی عبارت پیش بینی بکار گرفته میشود، عملگر Count تنها عناصری را که شرط را تامین کنند، شمارش میکند.
در کد زیر عملگر Count، همه عناصر زوج توالی ورودی را شمارش میکند:
int[] nums = { 1, 2, 3 }; int numberOfEvenElements = nums.Count(x => x % 2 == 0); Console.WriteLine(numberOfEvenElements);
1
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Count ، کلمهی کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر LongCount
این عملگر مثل عملگر Count عمل میکند، اما با این تفاوت که خروجی آن به جای نوع int از نوع long میباشد. این عملگر برای شمارش توالیهای ورودی بسیار بزرگ مورد استفاده قرار میگیرد.
پیاده سازی توسط عبارتهای جستجو
معادل عملگر LongCount، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Sum
این عملگر مجموع تمامی عناصر یک توالی را باز میگرداند.
در کد زیر جمع عناصر یک توالی از نوع int را مشاهده میکنید:
int[] nums = { 1, 2, 3 }; int total = nums.Sum(); Console.WriteLine(total);
6
عملگر Sum میتواند بر روی توالیهایی از نوع <IEnumerable<T و بر روی اعضای عددی آنها اعمال شود.
مثال:
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient { Name = "Egg", Calories = 100 }, new Ingredient { Name = "Milk", Calories = 150 }, new Ingredient { Name = "Flour", Calories = 50 }, new Ingredient { Name = "Butter", Calories = 400 } }; int totalCalories = ingredients.Sum(x => x.Calories); Console.WriteLine(totalCalories);
1200
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Sum، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Average
این عملگر میانگین عناصر توالیهای عددی را محاسبه میکند.
مثال:
int[] nums = { 1, 2, 3 }; var avg = nums.Average(); Console.WriteLine(avg);
2
همانطور که در کد بالا مشاهده میکنید، نوع متغیر avg صراحتا مشخص نشده و از نوع var استفاده شده است. تابع average بر اساس توالی ورودی، انواع مختلفی از نوع دادههای عددی را به خروجی ارسال میکند (double,float,decimal).
همانند عملگر Sum، عملگر Average میتواند بر روی اعضای عددی توالیهایی که از نوع<IEnumarable<T هستند، اعمال شود.
مثال:
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient { Name = "Egg", Calories = 100 }, new Ingredient { Name = "Milk", Calories = 150 }, new Ingredient { Name = "Flour", Calories = 50 }, new Ingredient { Name = "Butter", Calories = 400 } }; var avgCalories = ingredients.Average(x => x.Calories); Console.WriteLine(avgCalories);
240
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Average، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Min
عملگر Min کوچکترین عنصر توالی را باز میگرداند.
مثال:
int[] nums = { 3, 2, 1 }; var smallest = nums.Min(); Console.WriteLine(smallest);
1
مثال:
Ingredient[] ingredients = { new Ingredient { Name = "Sugar", Calories = 500 }, new Ingredient { Name = "Egg", Calories = 100 }, new Ingredient { Name = "Milk", Calories = 150 }, new Ingredient { Name = "Flour", Calories = 50 }, new Ingredient { Name = "Butter", Calories = 400 } }; var smallestCalories = ingredients.Min(x => x.Calories); Console.WriteLine(smallestCalories);
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Min ، کلمه کلیدی در عبارتهای جستجو وجود ندارد.ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
عملگر Max
عملگر Max بزرگترین عنصر توالی را باز میگرداند.
مثال:
int[] nums = { 1 ,3, 2 }; var largest = nums.Max(); Console.WriteLine(largest);
3
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Max، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
Aggregate
عملگرهای تجمعی که تا اینجا معرفی شدند، تنها یک کار را انجام میدادند. اما عملگر Aggregate امکان تعریف یک پرس و جوی تجمیع سفارشی و پیشرفتهتر را که بر روی توالی ورودی اعمال میشود نیز مهیا میکند.
عملگر Aggregate دو نسخه دارد:
1- نسخهای که اجازه استفاده از یک عدد را به عنوان مقدار Seed، به ما میدهد (مقدار آغازین یا Seed).
2- نسخهای که از عنصر ابتدایی توالی به عنوان مقدار Seed استفاده میکند.
هر دو نسخه این عملگر به یک تابع انباره (accumulator function) جهت نگهداری نتیجه نیاز دارند.
کد زیر شبیه سازی عملگر Sum توسط عملگر Aggregate میباشد:
int[] nums = {1, 2, 3}; var result = nums.Aggregate(0, (currentElement, runningTotal) => runningTotal + currentElement); Console.WriteLine(result);
6
کد زیر شبیه سازی عملیات فاکتوریل را با در نظر گرفتن عنصر اول توالی، به عنوان مقدار Seed نشان میدهد:
int[] nums = { 1, 2, 3 ,4,5}; var result = nums.Aggregate((runningProduct, nextfactor) => runningProduct * nextfactor); Console.WriteLine(result);
120
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Aggregate، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
روش تعریف یک module initializer
در مثال زیر، قالب ابتدایی یک ModuleInitializer را مشاهده میکنید:
namespace CS9Features { using System.Runtime.CompilerServices; internal static class TestModuleInitializer { [ModuleInitializer] public static void MyModuleInitializer() { // put your module initializer here } } }
- باید استاتیک باشد.
- باید بدون پارامتر باشد.
- باید خروجی آن void باشد.
- نباید به صورت جنریک تعریف شود.
- این متد باید در همان اسمبلی، قابل دسترسی باشد؛ یعنی سطح دسترسی آن باید یا public و یا internal باشد.
- نباید local function باشد.
میتوان بیش از یک ModuleInitializer را در یک اسمبلی تعریف کرد
به مثال زیر دقت کنید:
namespace CS9Features { using System.Runtime.CompilerServices; internal static class TestModuleInitializer { [ModuleInitializer] public static void MyModuleInitializer1() { // put your module initializer here } [ModuleInitializer] public static void MyModuleInitializer2() { // put your module initializer here } } }
این مورد یکی از مهمترین تفاوتهای module initializerها با سازندههای static است. ترتیب اجرای سازندههای static مشخص نیست و بر اساس کدهای کلاینت و زمان دسترسی به کلاسهای مختلف، سازندهی استاتیک کلاس A میتواند پس از سازندهی استاتیک کلاس B اجرا شود و یا برعکس. اما همواره نحوهی اجرای module initializerها مشخص و ترتیبی است و همچنین نیازی به فراخوانی آنها توسط هیچ کلاینتی نیست.
موارد کاربرد module initializerها
نمونهی بسیار پرکاربرد module initializer ها، اجرای کدهایی پیش از شروع به اجرای آزمونهای خودکار یک برنامهاست؛ مانند کدهایی که یک بانک اطلاعاتی را ایجاد و مقدار دهی اولیه میکنند و پس از آن قرار است آزمایشهای برنامه بر روی این بانک اطلاعاتی مشخص، اجرا شوند.