مطالب
بررسی تفاوت کلید اصلی و کلید یکتا
کلید اصلی ( Primary Key):
به‌منظور تشخیص هر رکورد در یک جدول بانک اطلاعاتی از کلید اصلی استفاده می‌کنیم. هر جدول بانک اطلاعاتی باید یک کلید اصلی داشته باشد. برای تعریف کلید اصلی در هر جدول از کلمه‌ی کلیدی Primary Key بعد از نام ستون استفاده می‌کنیم.

کلید یکتا ( Unique Key):
ستون با محدودیت (constraint) کلید یکتا تنها می‌تواند دربرگیرنده ارزش‌هایی یکتا باشد. برای تعریف یک ستون بصورت یکتا (unique) بعد از نام ستون از کلمه کلیدی UNIQUE استفاده می‌کنیم. 

مثالی از نحوه ایجاد کلید اصلی و کلید یکتا:
اسکریپت زیر مثالی از نحوه‌ی پیاده سازی این دو ویژگی، در بانک اطلاعاتی می‌باشد: 
CREATE DATABASE DNTSampleDB
GO
USE DNTSampleDB
GO
CREATE TABLE Cars(
ID int PRIMARY KEY,
Name VARCHAR(255) NOT NULL,
NumberPlate VARCHAR(255) UNIQUE,--شماره پلاک
Model INT);

نمایی از ساختار بانک اطلاعاتی ایجاد شده :

در این مرحله تعدادی رکورد فرضی به این جدول اضافه می‌کنیم :
INSERT INTO Cars                       
VALUES
(1,'Mazda','ABC 123',199),
(2,'Mazda','ABC 345',207),
(3,'Mazda','ABC 758',305),
(4,'Mazda','ABC 741',306),
(5,'Mazda','ABC 356',124)
نمایی از وضعیت اطلاعات ثبت شده در جدول : 


بررسی شباهت‌های بین کلید اصلی و کلید یکتا :
ستون‌هایی که بصورت کلید اصلی و یا کلید یکتا تعریف می‌شوند، نمی‌توانند ارزش‌های تکراری را درون خود ذخیره کنند.
ثبت رکورد با شماره Id تکراری 2 موجب بروز خطای زیر می‌شود:
INSERT INTO Cars                       
VALUES
(2,'Mazda','ABC 123111',199)

این خطا به وضوح محدودیت کلید اصلی را اعلام می‌کند و اجازه‌ی ثبت ارزش تکراری را در فیلد ID، نمی‌دهد و همچنین تلاش برای ثبت رکوردی با ارزش تکراری در ستونی که با محدودیت کلید یکتا است، با خطا مواجه خواهد شد:
INSERT INTO Cars                       
VALUES
(6,'Mazda','ABC 741',200)

از آنجائیکه کلید اصلی و کلید یکتا، هر دو اجازه‌ی ثبت ارزش‌های تکراری را برای ستون‌های خود نمی‌دهند، می‌توان از آنها برای بازیابی رکورد‌ها استفاده کرد؛ زیرا مطمئن هستیم به ازای این ارزش، تنها و تنها یک رکورد در بانک اطلاعاتی ثبت شده‌است.

تفاوت‌های کلید اصلی و کلید یکتا:
1- یک جدول تنها می‌تواند یک کلید اصلی داشته باشد؛ اما امکان تعریف چندین کلید یکتا در یک جدول وجود دارد.
2- کلید اصلی نمی‌تواند ارزش null داشته باشد و بصورت پیش فرض (Not Null) است؛ اما کلید‌های یکتا می‌توانند ارزش null داشته باشند.

3- بصورت پیش فرض شاخص خوشه‌ای (clustered index) برای کلید اصلی و شاخص غیر خوشه ای (non-clustered Index) برای کلید‌های یکتا ایجاد می‌شود .
برای مشاهده‌ی شاخص‌های برای جدول ایجاد شده‌ی فوق، پروسیجر زیر را اجرا کنید:
USE DNTSampleDB
GO
sp_help cars
خروجی دستور فوق:

 
اشتراک‌ها
پیش نمایش MAUI و اجرای یک مثال

At Microsoft Build 2021 .NET MAUI Preview 4 was launched. This version is already pretty complete and starts to reflect what the end result is going to look like. In this video I will show you the new .NET MAUI template in Visual Studio 2019 and walk you through the Weather 21 demo app.  

پیش نمایش MAUI و اجرای یک مثال
مطالب
آشنایی با Jaeger
 در سال‌های اخیر، معماری میکروسرویس، یکی از محبوب‌ترین روش‌ها برای طراحی نرم‌افزار بوده‌است. جهت بهبود کارآیی، رفع خطا، درک  عملکرد سیستم در محیط عملیاتی و  نمایش چگونگی فراخوانی سرویس‌ها توسط یکدیگر می‌توانیم از ابزار‌های distributed tracing استفاده کنیم. ابزارهای متنوعی برای این منظور وجود دارند، اما بطور کلی همه با روش مشابهی کار می‌کنند. اطلاعات مربوط به فعالیت‌هایی مثل فراخوانی سرویس و مراجعه به دیتابیس که درون میکروسرویس رخ می‌دهد، در یک span  ذخیره می‌شوند. Span‌های جداگانه توسط شناسه‌ای یکتا به هم مرتبط می‌شوند و به عنوان یک trace نمایش داده می‌شوند. با استفاده از این trace‌ها، مجموعه‌ای از اطلاعات مثل تاریخ شروع و پایان هر درخواست و هر فعالیت را در اختیار داریم. 


جهت گرفتن دیتای مربوط به هر span، درون هر میکروسرویس می‌توانیم از پروژه‌های متن باز OpenTracing  و یا  OpenTelemetry استفاده کنیم. کتابخانه OpenTracing.Contrib.NetCore پیاده سازی OpenTracing در دات نت می‌باشد و می‌تواند  فعالیت‌های مربوط به ASP.NET Core، Entity Framework Core System.Net.Http (HttpClient)، System.Data.SqlClient و Microsoft.Data.SqlClient را دریافت و به tracer  ارسال کند. 

برای پیاده سازی distributed tracing، می‌توانیم از ابزار متن باز و محبوب Jaeger (با تلفظ یِگِر)  که ابتدا توسط شرکت Uber منتشر شد، استفاده کنیم. نحوه کارکرد Jaeger بصورت زیر می‌باشد:




ساده‌ترین روش  برای راه‌اندازی Jager، استفاده از داکر ایمیج All in one که شامل ماژول های agent ، collector،  query  و ui  است. پورت 6831 مربوط به agent  و پورت 16686 مربوط به ui می‌باشد. برای جزئیات مربوط به ماژول‌های مختلف از این لینک استفاده کنید.

docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 14268:14268 -p 14250:14250 -p 16686:16686 -p 5778:5778  
--name jaeger jaegertracing/all-in-one:latest

بعد از اجرای دستور بالا، اطلاعات مربوط به سرویس‌ها و trace ها  در ماژول Jager UI  با آدرس http://localhost:16686 قابل مشاهده است. 

جهت استفاده از Jaeger از پروژه تستی که شامل دو سرویس User و Gateway می‌باشد، استفاده می‌کنیم. در سرویس User، متد AddUser در صورت عدم وجود کاربر در دیتابیس، اطلاعات کاربر از گیت‌هاب را دریافت و در دیتابیس ذخیره می‌کند. سرویس Gateway از Ocelot برای مسیردهی درخواست‌ها استفاده می‌کند. برای آشنایی با ocelot‌ این پست را  مطالعه نمایید. 


    public async Task<ApiResult<Models.User>> AddUserAsync(string username)
        {

            var result = new ApiResult<Models.User>();
            
            var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Login == username);

            if (user is null)
            {
                try
                {
                    var url = string.Format(_appConfig.Github.ProfileUrl, username);
                    var apiResult = await _httpClient.GetStringAsync(url);
                    var userDto = JsonSerializer.Deserialize<UserDto>(apiResult);
                    user = _mapper.Map<Models.User>(userDto);
                    await _applicationDbContext.Users.AddAsync(user);
                    await _applicationDbContext.SaveChangesAsync();
                    result.Result = user;
                    result.Message = "User successfully Created";
                    return result;
                }
                catch (Exception e)
                {
                    result.Message = "User not found";
                    return result;
                }
            }

            result.Message = "User already exist";
            result.Result = user;

            return result;

        }


برای ثبت Trace مربوط به درخواست‌ها در Jaeger ، بعد از نصب  پکیج‌های Jaeger و OpenTracing.Contrib.NetCore در هر دو سرویس، در کانفیگ هریک از سرویس‌ها مورد زیر را اضافه می‌کنیم:

"JaegerConfig": {
    "Host": "localhost",
    "Port": 6831,
    "IsEnabled": true,
  "SamplingRate": 0.5
  }


و برای اضافه شدن tracer به برنامه از متد الحاقی زیر استفاده می‌کنیم:

 public static class Extensions
    {
        public static void AddJaeger(this IServiceCollection services, IConfiguration configuration)
        {
            var config = configuration.GetSection("JaegerConfig").Get<JaegerConfig>();
            
            if (!(config?.IsEnabled ?? false))
                return;

            if (string.IsNullOrEmpty(config?.Host))
                throw new Exception("invalid JaegerConfig");

            services.AddSingleton<ITracer>(serviceProvider =>
            {
                string serviceName = Assembly.GetEntryAssembly()?.GetName().Name;

                ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();

                var sampler = new ProbabilisticSampler(config.SamplingRate); 

                var reporter = new RemoteReporter.Builder()
                    .WithLoggerFactory(loggerFactory)
                    .WithSender(new UdpSender(config.Host, config.Port, 0))
                    .WithFlushInterval(TimeSpan.FromSeconds(15))
                    .WithMaxQueueSize(300)
                    .Build();
                
                ITracer tracer = new Tracer.Builder(serviceName)
                    .WithLoggerFactory(loggerFactory)
                    .WithSampler(sampler)
                    .WithReporter(reporter)
                    .Build();

                GlobalTracer.Register(tracer);

                return tracer;
            });

            services.AddOpenTracing();
        }
    }


برای ثبت trace‌ها استراتژی‌های متفاوتی وجود دارد. در اینجا از ProbabilisticSampler استفاده شده‌است که در سازنده‌ی آن می‌توان درصد ثبت Trace‌ها را مقدار دهی کرد. در نهایت این متد الحاقی را در Startup اضافه می‌کنیم:

builder.Services.AddJaeger(builder.Configuration);


بعد از اجرای پروژه و فراخوانی https://localhost:6000/gateway/Users/Add ، سرویس Gateway، درخواست را به سرویس User ارسال می‌کند و این سرویس‌ها در  Jaeger UI  قابل مشاهده هستند.




جهت مشاهده trace ‌ها ، سرویس مورد نظر را انتخاب و روی Find Traces کلیک کنید. با کلیک روی Trace مورد نظر، جزئیات فعالیت هایی مثل فراخوانی سرویس و مراجعه به دیتابیس قابل مشاهده است. 


برای اضافه کردن لاگ سفارشی به یک span، می‌توان از اینترفیس ITracer  استفاده کرد:

        private readonly IUserService _userService;
        private readonly ITracer _tracer;

        public UsersController(IUserService userService, ITracer tracer)
        {
            _userService = userService;
            _tracer = tracer;
        }
        [HttpPost]
        public async Task<ActionResult> AddUser(AddUserDto model)
        {
            var actionName = ControllerContext.ActionDescriptor.DisplayName;
            
            using var scope = _tracer.BuildSpan(actionName).StartActive(true);
            
            scope.Span.Log($"Add user log username: {model.Username}");
            
            return Ok(await _userService.AddUserAsync(model.Username));
        }  



کدهای مربوط به این مطلب در اینجا قابل دسترسی است. 

اشتراک‌ها
تفاوت MERGE و REBASE در git

Understand the differences between MERGE and REBASE and learn how you can use these commands in your projects! 

تفاوت MERGE و REBASE در git
اشتراک‌ها
ASP.NET 5 beta 6 منتشر شد

The new Microsoft.Framework.Caching.SqlServer package will allow you to use SQL Server as your cache store in ASP.NET 5 applications.

ASP.NET 5 beta 6 منتشر شد
مطالب
اعتبارسنجی سفارشی سمت کاربر و سمت سرور در jqGrid
همانطور که در مطلب «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» نیز ذکر شد، خاصیت editrules یک ستون، برای مباحث اعتبارسنجی اطلاعات ورودی توسط کاربر پیش بینی شده‌است. برای مثال اگر required: true در آن تنظیم شود، کاربر مجبور به تکمیل این سلول خاص خواهد بود. در اینجا خواصی مانند number و integer از نوع bool هستند، minValue و maxValue از نوع عددی، email, url, date, time از نوع bool و custom قابل تنظیم است. در ادامه نحوه‌ی اعمال اعتبارسنجی‌های سفارشی سمت سرور و همچنین سمت کلاینت را بررسی خواهیم کرد.


مدل برنامه و نیازمندی‌های اعتبارسنجی آن

namespace jqGrid08.Models
{
    public class User
    {
        public int Id { set; get; }

        public string Name { set; get; }

        public string Email { set; get; }

        public string Password { set; get; }

        public string SiteUrl { set; get; }
    }
}
مدل کاربر فوق را در نظر بگیرید. در حین ورود اطلاعات نیاز است:
- نام کاربر به صورت اجباری وارد شود و همچنین بین 3 تا 40 حرف باشد.
- همچنین نام کاربر نباید بر اساس اطلاعات موجود در بانک اطلاعاتی، تکراری وارد شود.
- ورود ایمیل شخص اجباری است؛ به علاوه فرمت آن نیز باید با یک ایمیل واقعی تطابق داشته باشد.
- ایمیل وارد شده‌ی یک کاربر جدید نیز نباید تکراری بوده و پیشتر توسط کاربر دیگری وارد شده باشد.
- ورود کلمه‌ی عبور در حالت ثبت اطلاعات اجباری است؛ اما در حالت ویرایش اطلاعات خیر (از کلمه‌ی عبور موجود در این حالت استفاده خواهد شد).
- ورود آدرس سایت کاربر اجباری بوده و همچنین فرمت آدرس وارد شده نیز باید معتبر باشد.


اعتبار سنجی سمت سرور و سمت کلاینت نام کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Name))',
                        align: 'right', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40
                        },
                        editrules: {
                            required: true,
                            custom: true,
                            custom_func: function (value, colname) {
                                if (!value)
                                    return [false, "لطفا نامی را وارد کنید"];

                                if (value.length < 3 || value.length > 40)
                                    return [false, colname + " باید بین 3 تا 40 حرف باشد"];
                                else
                                    return [true, ""];
                            }
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری نام کاربر به پایان می‌رسد. اما نیاز است این نام بین 3 تا 40 حرف باشد. بنابراین نیاز است سمت کاربر بتوان اطلاعات وارد شده توسط کاربر را دریافت کرده و سپس طول آن‌را بررسی نمود. این‌کار، توسط مقدار دهی خاصیت custom به true و سپس تعریف متدی برای custom_func قابل انجام است.
خروجی این متد یک آرایه دو عضوی است. اگر عضو اول آن true باشد، یعنی اعتبارسنجی موفقیت آمیز بوده‌است؛ اگر خیر، عضو دوم آرایه، پیامی است که به کاربر نمایش داده خواهد شد.
تا اینجا کار اعتبارسنجی سمت کاربر به پایان می‌رسد. اما نیاز است در سمت سرور نیز بررسی شود که آیا نام وارد شده تکراری است یا خیر. برای این منظور تنها کافی است رویداد afterSubmit حالت‌های Add و Edit را بررسی کنیم:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    afterSubmit: showServerSideErrors
                },
                //add options
                {
                    afterSubmit: showServerSideErrors
                },
                //delete options
                {
                });
        });

        function showServerSideErrors(response, postdata) {
            var result = $.parseJSON(response.responseText);
            if (result.success === false) {
                //نمایش خطای اعتبار سنجی سمت سرور پس از ویرایش یا افزودن
                //و همچنین جلوگیری از ثبت نهایی فرم
                return [false, result.message, result.id];
            }

            return [true, "", result.id];
        }
شیء response، حاوی اطلاعات بازگشت داده شده از طرف سرور است. برای مثال یک چنین خروجی JSON ایی را در حالت‌های شکست اعتبارسنجی بازگشت می‌دهیم:
        [HttpPost]
        public ActionResult AddUser(User postData)
        {
            //todo: Add user to repository
            if (postData == null)
                return Json(new { success = false, message = "اطلاعات دریافتی خالی است" }, JsonRequestBehavior.AllowGet);

            if (_usersInMemoryDataSource.Any(
                    user => user.Name.Equals(postData.Name, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "نام کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            if (_usersInMemoryDataSource.Any(
                    user => user.Email.Equals(postData.Email, StringComparison.InvariantCultureIgnoreCase)))
            {
                return Json(new { success = false, message = "آدرس ایمیل کاربر تکراری است" }, JsonRequestBehavior.AllowGet);
            }

            postData.Id = _usersInMemoryDataSource.LastOrDefault() == null ? 1 : _usersInMemoryDataSource.Last().Id + 1;
            _usersInMemoryDataSource.Add(postData);

            return Json(new { id = postData.Id, success = true }, JsonRequestBehavior.AllowGet);
        }
در سمت کلاینت در روال رویدادگردان afterSubmit می‌توان با آنالیز response و سپس استخراج فیلدهای JSON آن، وضعیت success و همچنین پیام‌های بازگشت داده شده را بررسی کرد.
خروجی روال رویدادگردان afterSubmit نیز بسیار شبیه است به حالت اعتبارسنجی سفارشی یک ستون. اگر عضو اول آرایه بازگشت داده شده توسط آن false باشد، یعنی اعتبارسنجی سمت سرور، با شکست مواجه شده و در این حالت از عضو دوم آرایه برای نمایش پیام خطای بازگشت داده شده از طرف سرور استفاده خواهد شد.



اعتبار سنجی ایمیل کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Email))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 250,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            email: true
                        },
                        formatter: 'email'
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری ایمیل کاربر به پایان می‌رسد. همچنین با تنظیم email: true، به صورت خودکار فرمت ایمیل وارد شده نیز بررسی می‌شود.
مطابق نیازمندی‌های اعتبارسنجی پروژه، ایمیل وارد شده نیز نباید تکراری باشد. این مورد نیز توسط خروجی روال رویدادگردان afterSubmit که پیشتر توضیح داده شده، مدیریت می‌شود.



اعتبار سنجی کلمه عبور کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.Password))',
                        align: 'center', width: 70,
                        editable: true, edittype: 'password',
                        editoptions: {
                            maxlength: 10,
                            dir: 'ltr'
                        },
                        editrules: {
                            //required: true ---> در این حالت خاص قابل استفاده نیست
                            //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                            //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        }
                    },
                ],
حالت بررسی اعتبارسنجی کلمه‌ی عبور در اینجا، حالت ویژه‌ای است. نیاز است در حالت ثبت اطلاعات اجباری باشد اما در حالت ویرایش خیر. بنابراین نمی‌توان از خاصیت required: true استفاده کرد؛ چون به هر دو حالت ویرایش و ثبت اطلاعات به صورت یکسان اعمال می‌شود.
برای این منظور تنها کافی است از روال رویدادگردان beforeSubmit استفاده کرد:
            $('#list').jqGrid({
                // ...
            }).navGrid(
                '#pager',
                //enabling buttons
                { add: true, del: true, edit: true, search: false },
                //edit option
                {
                    /*, beforeSubmit: function (posdata, obj) {
                        //در حالت ویرایش رکورد، ورود کلمه عبور اختیاری است
                        return [true, ""];
                    }*/
                },
                //add options
                {
                    beforeSubmit: function (postdata, obj) {
                        //در حالت افزودن رکورد، ورود کلمه عبور اجباری است
                        if (postdata.Password == null || postdata.Password == "" || postdata.Password == undefined)
                            return [false, "لطفا کلمه عبور را وارد کنید"];
                        return [true, ""];
                    }
                },
                //delete options
                {
                });
        });
چون می‌خواهیم تنها حالت Add را تحت کنترل قرار دهیم، رویدادگردان beforeSubmit آن‌را مقدار دهی کرده‌ایم. توسط postdata کلیه اطلاعات قابل ارسال به سرور به صورت یک شیء جاوا اسکریپتی یا JSON در اختیار ما است. سپس با بررسی برای مثال postdata.Password می‌توان در مورد مقدار کلمه‌ی عبور تصمیم گیری کرد. در اینجا نیز خروجی متد باید یک آرایه دو عضوی باشد تا در صورت false بودن اولین عضو آن، پیام سفارشی اعتبارسنجی خاصی را بتوان به کاربر نمایش داد.



اعتبار سنجی آدرس سایت کاربر

                colModel: [
                    {
                        name: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        index: '@(StronglyTyped.PropertyName<User>(x => x.SiteUrl))',
                        align: 'center', width: 150,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 1000,
                            dir: 'ltr'
                        },
                        editrules: {
                            required: true,
                            url: true
                        },
                        formatter: function (cellvalue, options, rowObject) {
                            return "<a href='" + cellvalue + "' >" + cellvalue + "</a>";
                        },
                        unformat: function (cellvalue, options, cell) {
                            return $('a', cell).attr('href');
                        }
                    },
                ],
با تنظیم required: true، کار تنظیم ورود اجباری آدرس سایت کاربر به پایان می‌رسد. همچنین با تنظیم url: true، به صورت خودکار فرمت URL وارد شده نیز بررسی می‌شود.



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid08.zip
اشتراک‌ها
اورلیا به صورت عملی

This books shows all the steps necessary for the development of SPA (Single Page Application) applications with the Aurelia. With this book you will learn:

  • How to create a project in Aurelia
  • How to create Aurelia plugin
  • Understand how the SystemJS imports works
  • Using HTTP to connect to a server
  • HttpClient
  • FetchClient
  • How to use Firebase
  • Offline the app data
  • Working with loopback
  • ... 
اورلیا به صورت عملی
نظرات مطالب
ایجاد کپچایی (captcha) سریع و ساده در ASP.NET MVC 5

ممنون از شما. فقط یک نکته‌ی کوچک در مورد memory stream هست که بهتره درنظر گرفته بشه. در این شیء متدهای ToArray و GetBuffer یکی نیستند. متد GetBuffer حجمی نزدیک به 2 برابر آرایه اصلی رو عموما داره و انتهاش یک سری بایت‌های اضافی هم شاید باشند. اما ToArray اصل دیتا رو بر می‌گردونه.

Note that the buffer contains allocated bytes which might be unused. For example, if the string "test" is written into the MemoryStream object, the length of the buffer returned from GetBuffer is 256, not 4, with 252 bytes unused. To obtain only the data in the buffer, use the ToArray method; however, ToArray creates a copy of the data in memory.

مطالب
بهبود کارآیی حلقه‌های foreach در دات نت 7
بالاخره تفاوت کارآیی بین حلقه‌های for و foreach در دات نت 7 برطرف شده‌است که این مورد نیز یکی دیگر از دلایل بهبود کارآیی LINQ در دات نت 7 است. در این مطلب به همراه آزمایشی، این مورد را بررسی خواهیم کرد.


تدارک یک آزمایش برای بررسی کارآیی حلقه‌های for و foreach در دات نت 7

یک برنامه‌ی کنسول جدید را ایجاد کرده و سپس کتابخانه‌ی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
  </ItemGroup>
</Project>
در ادامه به این پروژه، کلاس زیر را اضافه می‌کنیم:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace NET7Loops;

[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[MemoryDiagnoser(false)]
public class Benchmarks
{
    private int[] ItemsArray;
    private List<int> ItemsList;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random(420);
        var randomItems = Enumerable.Range(0, 1000).Select(_ => random.Next());
        ItemsArray = randomItems.ToArray();
        ItemsList = randomItems.ToList();
    }

    [Benchmark]
    public void For_Array()
    {
        for (var i = 0; i < ItemsArray.Length; i++)
        {
            var item = ItemsArray[i];
        }
    }

    [Benchmark]
    public void For_List()
    {
        for (var i = 0; i < ItemsList.Count; i++)
        {
            var item = ItemsList[i];
        }
    }

    [Benchmark]
    public void ForEach_Array()
    {
        foreach (var item in ItemsArray)
        {
        }
    }

    [Benchmark]
    public void ForEach_List()
    {
        foreach (var item in ItemsList)
        {
        }
    }
}
که توسط دستورات زیر در حالت release اجرا شده و نتایج نهایی را نمایش می‌دهد:
using BenchmarkDotNet.Running;
using NET7Loops;

BenchmarkRunner.Run<Benchmarks>();
توضیحات:

- می‌توان یک پروژه را یکبار بر اساس دات نت 7 و یکبار هم بر اساس دات نت 6 با تغییر target framework آن‌ها کامپایل و اجرا کرد تا بتوان نتایج این دو را با هم مقایسه کرد و یا می‌توان با ذکر [SimpleJob(RuntimeMoniker.Net60)] و همچنین [SimpleJob(RuntimeMoniker.Net70)]، این مورد را به صورت خودکار به BenchmarkDotNet دات نت واگذار کرد.
- در این آزمایش، ابتدا یک آرایه و یک لیست را تهیه می‌کنیم.
- سپس یکبار حلقه‌های for و foreach را بر روی آرایه و همین عملیات را بر روی لیست تهیه شده، تکرار می‌کنیم.

نتایج حاصل به صورت زیر هستند:


همانطور که در نتایج فوق هم مشاهده می‌کنید:
در دات نت 6
- تفاوتی بین کارآیی حلقه‌ها‌ی for و foreach، زمانیکه بر روی یک آرایه اجرا می‌شوند، وجود ندارد.
- اما کارآیی حلقه‌ی foreach نسبت به حلقه‌ی for، زمانیکه بر روی یک لیست اجرا می‌شوند، تقریبا 50 درصد کمتر است.

در دات نت 7
- تفاوتی بین کارآیی حلقه‌ها‌ی for و forach، زمانیکه بر روی یک آرایه اجرا می‌شوند، وجود ندارد. بنابراین از این لحاظ با دات نت 6 تفاوتی ندارد.
- اما کارآیی حلقه‌ی foreach نسبت به حلقه‌ی for، زمانیکه بر روی یک لیست اجرا می‌شود، تقریبا یکسان و قابل چشم‌پوشی است. یعنی در دات نت 7، کارآیی این دو حلقه یکی شده‌است. اما چرا؟


روشی در جهت یافتن یکی بودن سرعت حلقه‌های for و foreach بر اساس خروجی کامپایلر

با مشاهده‌ی نتایج حاصل از BenchmarkDotNet می‌توان به بهبود کارآیی حاصل پی‌برد؛ اما برای مثال چرا زمانیکه از آرایه استفاده می‌شود، حتی در دات نت 6، تفاوتی بین دو حلقه‌ی for و foreach وجود ندارد، اما زمانیکه از لیست‌ها استفاده می‌شود، این کارآیی 50 درصد افت می‌کند؟
برای پاسخ به این سؤال می‌توان از IL Viewer موجود در Rider استفاده کرد که آخرین نگارش آن به همراه نمایش #Low-level C هم هست:

این همان خروجی است که توسط کامپایلر، پیش از تولید کدهای باینری نهایی، تهیه می‌شود. یعنی اگر قصد داشته باشیم تا درک کامپایلر را نسبت به قطعه کدی مشاهده کنیم، می‌توان به این خروجی مراجعه کرد که به صورت زیر است:
// Decompiled with JetBrains decompiler
// Type: NET7Loops.Benchmarks
// Assembly: NET7Loops, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: E398BEE7-8123-4C55-AF9A-F7D83DDA73F1
// Assembly location: C:\Prog\1401\Net7Tests\NET7Loops\bin\Debug\net7.0\NET7Loops.dll
// Compiler-generated code is shown

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace NET7Loops
{
  [NullableContext(1)]
  [Nullable(0)]
  [SimpleJob(RuntimeMoniker.Net60, -1, -1, -1, -1, null, false)]
  [SimpleJob(RuntimeMoniker.Net70, -1, -1, -1, -1, null, false)]
  [MemoryDiagnoser(false)]
  public class Benchmarks
  {
    private int[] ItemsArray;
    private List<int> ItemsList;

    [GlobalSetup]
    public void Setup()
    {
      Benchmarks.<>c__DisplayClass2_0 cDisplayClass20 = new Benchmarks.<>c__DisplayClass2_0();
      cDisplayClass20.random = new Random(420);
      IEnumerable<int> source = Enumerable.Range(0, 1000).Select<int, int>(new Func<int, int>((object) cDisplayClass20, __methodptr(<Setup>b__0)));
      this.ItemsArray = source.ToArray<int>();
      this.ItemsList = source.ToList<int>();
    }

    [Benchmark(23, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void For_Array()
    {
      for (int index = 0; index < this.ItemsArray.Length; ++index)
      {
        int items = this.ItemsArray[index];
      }
    }

    [Benchmark(32, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void For_List()
    {
      for (int index = 0; index < this.ItemsList.Count; ++index)
      {
        int items = this.ItemsList[index];
      }
    }

    [Benchmark(41, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void ForEach_Array()
    {
      int[] itemsArray = this.ItemsArray;
      for (int index = 0; index < itemsArray.Length; ++index)
      {
        int num = itemsArray[index];
      }
    }

    [Benchmark(49, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void ForEach_List()
    {
      List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator();
      try
      {
        while (enumerator.MoveNext())
        {
          int current = enumerator.Current;
        }
      }
      finally
      {
        enumerator.Dispose();
      }
    }

    public Benchmarks()
    {
      base..ctor();
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass2_0
    {
      [Nullable(0)]
      public Random random;

      public <>c__DisplayClass2_0()
      {
        base..ctor();
      }

      internal int <Setup>b__0(int _)
      {
        return this.random.Next();
      }
    }
  }
}
در این خروجی بهتر می‌توان مشاهده کرد که چرا در حالت استفاده‌ی از آرایه‌ها، تفاوتی بین حلقه‌های for و foreach نیست؛ چون هر دو به صورت حلقه‌ی for تفسیر می‌شوند:
for (int index = 0; index < this.ItemsArray.Length; ++index)
{
   int items = this.ItemsArray[index];
}
اما زمانیکه به لیست‌ها می‌رسیم، حلقه‌ی foreach به صورت زیر تفسیر می‌شود که بدیهی است نسبت به حلقه‌ی for، کندتر اجرا خواهد شد:
      List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator();
      try
      {
        while (enumerator.MoveNext())
        {
          int current = enumerator.Current;
        }
      }
      finally
      {
        enumerator.Dispose();
      }
اگر این خروجی را برای دات نت 6 و دات نت 7 تهیه کنیم، به یک جواب خواهیم رسید. یعنی از دیدگاه #Low-level C، تفاوتی بین IL دات نت 6 و 7 از این لحاظ وجود ندارد. تفاوتی اصلی در بهبودهای JIT دات نت 7 است که سبب شده، خروجی نهایی حلقه‌‌های foreach با for یکی باشد.