اشتراکها
اشتراکها
معرفی Microsoft.com/Learn
Today we’re announcing the release of our new learning website called Microsoft Learn – the best place to start learning Microsoft technologies.
ما میتوانیم دو پایگاه داده SQL Server را با راههای مختلفی همگام سازی کنیم، امروز در مورد چارچوب Microsoft Sync Framework صحبت میکنیم. از Microsoft Sync Framework برای همگام سازی دو پایگاه داده که در دو سرور مختلف قرار دارند استفاده میشود. در این مقاله ما در مورد چگونگی همگام سازی پایگاههای داده با استفاده از این فریم ورک بحث میکنیم.
اشتراکها
تنظیمات اولیه VS 2015
ارتقاء به ASP.NET Core 3.0
نام بستهی npm سمت کلاینت مخصوص SignalR Core که در ابتدا aspnet/signalr-client@ بود و بعد شد aspnet/signalr@، از این پس microsoft/signalr@ شدهاست!
npm install @microsoft/signalr --save
ارتقاء به ASP.NET Core 3.0
نام بستهی npm سمت کلاینت مخصوص SignalR Core که در ابتدا aspnet/signalr-client@ بود و بعد شد aspnet/signalr@، از این پس microsoft/signalr@ شدهاست!
npm install @microsoft/signalr --save
مطالب
امن سازی برنامههای ASP.NET Core توسط IdentityServer 4x - قسمت دوازدهم- یکپارچه سازی با اکانت گوگل
در مطلب قبلی «استفاده از تامین کنندههای هویت خارجی»، نحوهی استفاده از اکانتهای ویندوزی کاربران یک شبکه، به عنوان یک تامین کنندهی هویت خارجی بررسی شد. در ادامه میخواهیم از اطلاعات اکانت گوگل و IDP مبتنی بر OAuth 2.0 آن به عنوان یک تامین کنندهی هویت خارجی دیگر استفاده کنیم.
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک 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 کلیک کرده و در صفحهی بعدی، این سه گزینه را با مقادیر مشخص شده، تکمیل کنید:
سپس در ذیل این صفحه بر روی دکمهی «What credentials do I need» کلیک کنید تا به صفحهی پس از آن هدایت شوید. اینجا است که مشخصات کلاینت OAuth 2.0 تکمیل میشوند. در این صفحه، سه گزینهی آنرا به صورت زیر تکمیل کنید:
• نام: همان مقدار پیشفرض آن
• Authorized JavaScript origins: آنرا خالی بگذارید.
• Authorized redirect URIs: این مورد همان callback address مربوط به IDP ما است که در اینجا آنرا با آدرس زیر مقدار دهی خواهیم کرد.
این آدرس، به آدرس IDP لوکال ما اشاره میکند و مسیر 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
در اینجا مقادیر خواص client_secret و client_id موجود در فایل client_id.json دریافت شدهی از گوگل را به صورت فوق به فایل appsettings.json اضافه میکنیم.
ب) تکمیل اطلاعات گوگل در کلاس آغازین برنامه
خود ASP.NET Core از تعریف اطلاعات اکانتهای Google, Facebook, Twitter, Microsoft Account و OpenID Connect پشتیبانی میکند که در اینجا نحوهی تنظیم اکانت گوگل آنرا مشاهده میکنید.
- authenticationScheme تنظیم شده باید یک عبارت منحصربفرد باشد.
- همچنین SignInScheme یک چنین مقداری را در اصل دارد:
از این نام برای تشکیل قسمتی از نام کوکی که اطلاعات اعتبارسنجی گوگل در آن ذخیره میشود، کمک گرفته خواهد شد.
آزمایش اعتبارسنجی کاربران توسط اکانت گوگل آنها
اکنون که تنظیمات اکانت گوگل به پایان رسید و همچنین به برنامه نیز معرفی شد، برنامهها را اجرا کنید. مشاهده خواهید کرد که امکان لاگین توسط اکانت گوگل نیز به صورت خودکار به صفحهی لاگین IDP ما اضافه شدهاست:
در اینجا با کلیک بر روی دکمهی گوگل، به صفحهی لاگین آن که به همراه نام برنامهی ما است و انتخاب اکانتی از آن هدایت میشویم:
پس از آن، از طرف گوگل به صورت خودکار به IDP (همان آدرسی که در فیلد Authorized redirect URIs وارد کردیم)، هدایت شده و callback رخداده، ما را به سمت صفحهی ثبت اطلاعات کاربر جدید هدایت میکند. این تنظیمات را در قسمت قبل ایجاد کردیم:
اگر خروجی متد FindUserFromExternalProvider آن null باشد، یعنی کاربری که از سمت تامین کنندهی هویت خارجی/گوگل به برنامهی ما وارد شدهاست، دارای اکانتی در سمت IDP نیست. به همین جهت او را به سمت صفحهی ثبت نام کاربر هدایت میکنیم.
در اینجا نحوهی اصلاح اکشن متد Callback را جهت هدایت یک کاربر جدید به صفحهی ثبت نام و تکمیل اطلاعات مورد نیاز IDP را مشاهده میکنید.
returnUrl ارسالی به اکشن متد RegisterUser، به همین اکشن متد جاری اشاره میکند. یعنی کاربر پس از تکمیل اطلاعات و اینبار نال نبودن user او، گردش کاری جاری را ادامه خواهد داد و به برنامه با این هویت جدید وارد میشود.
اتصال کاربر وارد شدهی از طریق یک IDP خارجی به اکانتی که هم اکنون در سطح IDP ما موجود است
تا اینجا اگر کاربری از طریق یک IDP خارجی به برنامه وارد شود، او را به صفحهی ثبت نام کاربر هدایت کرده و پس از دریافت اطلاعات او، اکانت خارجی او را به اکانتی جدید که در IDP خود ایجاد میکنیم، متصل خواهیم کرد. به همین جهت بار دومی که این کاربر به همین ترتیب وارد سایت میشود، دیگر صفحهی ثبت نام و تکمیل اطلاعات را مشاهده نمیکند. اما ممکن است کاربری که برای اولین بار از طریق یک IDP خارجی به سایت ما وارد شدهاست، هم اکنون دارای یک اکانت دیگری در سطح IDP ما باشد؛ در اینجا فقط اتصالی بین این دو صورت نگرفتهاست. بنابراین در این حالت بجای ایجاد یک اکانت جدید، بهتر است از همین اکانت موجود استفاده کرد و صرفا اتصال UserLogins او را تکمیل نمود.
به همین جهت ابتدا نیاز است لیست Claims بازگشتی از گوگل را بررسی کنیم:
در اینجا پس از فراخوانی FindUserFromExternalProvider، لیست Claims بازگشت داده شدهی توسط IDP خارجی را لاگ میکنیم که در حالت استفادهی از گوگل چنین خروجی را دارد:
بنابراین اگر بخواهیم بر اساس این claims بازگشتی از گوگل، کاربر جاری در بانک اطلاعاتی خود را بیابیم، فقط کافی است اطلاعات claim مخصوص emailaddress آنرا مورد استفاده قرار دهیم:
در اینجا ابتدا بررسی شدهاست که آیا کاربر جاری واکشی شدهی از بانک اطلاعاتی نال است؟ اگر بله، اینبار بجای هدایت مستقیم او به صفحهی ثبت کاربر و تکمیل مشخصات او، مقدار email این کاربر را از لیست claims بازگشتی او از طرف گوگل، استخراج میکنیم. سپس بر این اساس اگر کاربری در بانک اطلاعاتی وجود داشت، تنها اطلاعات تکمیلی UserLogin او را که در اینجا خالی است، به اکانت گوگل او متصل میکنیم. به این ترتیب دیگر کاربر نیازی نخواهد داشت تا به صفحهی ثبت اطلاعات تکمیلی هدایت شود و یا اینکه بیجهت رکورد User جدیدی را مخصوص او به بانک اطلاعاتی اضافه کنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشهی 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 وارد کنید.
ثبت یک برنامهی جدید در گوگل
اگر بخواهیم از گوگل به عنوان یک 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 وارد کنید.
یکی از مواردی که فشاری بر روی garbage collector را بالا میبرد، تخصیصهای حافظهی مخفی یا Hidden allocations هستند که سبب تخصیصهای حافظهی کوچک و عموما پر تعدادی بر روی heap میشوند. برای نمونه به مثال ذیل دقت کنید و سعی کنید تعداد تخصیصهای حافظهی آن را حدس بزنید:
در این مثال ... سه تخصیص حافظهی کوچک رخ میدهد. از این جهت که متد Console.WriteLine ایی که در اینجا استفاده میشود، در نهایت به یک چنین کدی کامپایل خواهد شد:
در این مثال بر روی تمام پارامترهای int دریافتی، عملیات boxing (تبدیل یا cast) به object صورت میگیرد و عملیات boxing، یک نوع allocation است که نتیجهی آن بر روی heap ذخیره میگردد.
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
این مثال، یک چنین نمایش بصری دارد:
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
در این مثال هربار که متد Sum فراخوانی شود، یکبار دیگر IEnumerable آن تخصیص خواهد یافت که در تصویر ذیل با enumerator allocation مشخص شدهاست:
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET
public static void PrintSum(int a, int b) { Console.WriteLine("Sum of a {0} b {1} is {2}", a, b, a + b); }
Console::WriteLine(string, object, object, object)
روشی برای نمایان ساختن تخصیصهای حافظهی نهان در ویژوال استودیو
اگر از ReSharper استفاده میکنید، افزونهی «Heap Allocations Viewer» آن و یا اگر از VS 2015 و Roslyn استفاده کنید، افزونهی «Roslyn Clr Heap Allocation Analyzer» آن، سبب نمایان شدن allocationهای مخفی میشوند. برای مثال قطعه کد فوق یک چنین نمایشی را پیدا میکند:
در اینجا در ذیل هر سه موردی که عملیات boxing allocation رخ داده، یک خط قرمز کشیده است. یکی از روشهایی که میتواند boxing allocation فوق را حذف کند، بکار گیری متد ToString بر روی مقادیر int است:
همانطور که مشاهده میکنید، اینبار دیگر خبری از خطوط قرمز، ذیل پارامترهای متد Console.WriteLine نیست. باید دقت داشت که ToString نیز سبب تخصیص حافظه میشود، اما اینبار دیگر int32 آن بر روی heap ذخیره نمیگردد. به عبارتی هر دو حالت سبب تخصیص حافظهی یک رشتهی جدید میشوند؛ اما در حالت اول علاوه بر این شیء جدید، شیء int32 نیز بر روی heap ذخیره میگردد.
تشخیص تخصیص اشیاء مخفی با افزونههای Heap Allocations Viewer
نمونهی دیگر پر کاربرد این نوع بهینه سازیها را در مثال ذیل میتوان مشاهده کرد:
public static void PrintA(int a) { Console.WriteLine("a is " + a); }
اینبار یک خط زرد رنگ ظاهر شده به همراه یک خط قرمز رنگ. خط قرمز رنگ را پیشتر بررسی کردیم و علت وجودی آن Boxing allocation ایی است که رخ میدهد. خط زرد رنگ در ذیل + ظاهر شدهاست و عنوان میکند که عملیات جمع زدن رشتهها، سبب تخصیص حافظهی یک شیء جدید میشود. رشتهها در دات نت immutable هستند. به همین جهت هر تغییری در آنها، سبب تخصیص یک شیء جدید میشود. بنابراین در همین مثال ساده، دو تخصیص حافظهی مخفی وجود دارند. مورد جمع زدن را با بکارگیری string.Format و مشکل boxing را با ToString میتوان برطرف کرد:
public static void PrintA(int a) { Console.WriteLine("a is {0}", a.ToString()); }
منابع دیگری که سبب تخصیصهای حافظهی مخفی میشوند
تا اینجا دو مورد از منابع متداول تخصیصهای حافظهی مخفی را بررسی کردیم. اما این لیست شامل موارد ذیل نیز میشود:
1) فراخوانی متدهایی با پارامترهایی از نوع param همیشه سبب تخصیص حافظهای جهت تشکیل یک آرایهی در برگیرندهی پارامترهای ارسالی میشود.
2) متدهایی که پارامتر از نوع IEnumerable دارند:
public static int Sum(IEnumerable<int> list) { var sum = 0; foreach (var number in list) { sum += number; } return sum; }
برای حل این مشکل فقط کافی است IEnumerable را با List تعویض کنید.
3) کار با LINQ نیز سبب تخصیصهای حافظهی قابل توجهی است. برای مثال در کد پایهی Roslyn، برای رسیدن به حداکثر کارآیی، بسیاری از الگوریتمها را با روشهای غیر LINQ پیاده سازی کردهاند. البته برای تیمی مانند Roslyn رسیدن به یک چنین کارآیی جهت رقابت با سایر محصولات مشابه ضروری بودهاست و گرنه در بسیاری از کارهای متداول، استفاده از LINQ به خوانایی هر چه بیشتر کدها کمک شایانی میکند.
برای مطالعهی بیشتر
Roslyn code base – performance lessons - part 2
Unusual Ways of Boosting Up App Performance. Boxing and Collections
On performance in .NET