مطالب
مروری بر تاریخچه محدودیت حافظه مصرفی برنامه‌های ASP.NET در IIS

زمانیکه اولین نگارش ASP.NET‌ حدود 10 سال قبل منتشر شد،‌ تنها سیستم عاملی که از آن پشتیبانی می‌کرد، ویندوز سرور 2000 بود، تنها پروسه‌ی اجرایی آن aspnet_wp نام داشت و تنها معماری پشتیبانی شده هم X86 بود. به پروسه‌ی aspnet_wp محدودیت مصرف حافظه‌ای اعمال شده بود که در حین آغاز آن بر اساس مقدار قابل تغییر processModel memoryLimit محاسبه و اعمال می‌شد (تعریف شده در فایل ماشین کانفیگ). این عدد به صورت درصدی از ظرفیت RAM فیزیکی سیستم، قابل تعریف و به صورت پیش فرض به 60 درصد تنظیم شده بود. به این ترتیب این پروسه مجاز نبود تا تمام حافظه‌ی فیزیکی مهیا را مصرف کند و در صورت وجود نشتی حافظه‌ای در برنامه‌ای خاص، این پروسه امکان بازیابی مجدد حافظه را پیدا می‌کرد (recycling). همچنین یک مورد دیگر را هم باید در نظر داشت و آن هم وجود قابلیتی است به نام ASP.NET Cache است که امکان ذخیره سازی مقادیر اشیاء را در حافظه‌ی مصرفی این پروسه مهیا می‌سازد. هر زمان که میزان این حافظه‌ی مصرفی به حد نزدیکی از محدودیت تعریف شده برسد، این پروسه به صورت خودکار شروع به حذف آن‌ها خواهد کرد.
محدودیت 60 درصدی تعریف شده، برای سیستم‌هایی با میزان RAM کم بسیار مفید بود اما در سیستم‌هایی با میزان RAM بیشتر، مثلا 4 گیگ به 2.4GB حافظه مهیا (60 درصد حافظه فیزیکی سیستم) محدود می‌شد و همچنین باید در نظر داشت که میزان user mode virtual address space مهیا نیز تنها 2 گیگابایت بود. بنابراین هیچگاه استفاده مؤثری از تمام ظرفیت RAM مهیا صورت نمی‌گرفت و گاها مشاهده می‌شد که یک برنامه تنها با مصرف 1.5GB RAM می‌توانست پیغام OutOfMemoryException را صادر کند. در این حالت مطابق بررسی‌های صورت گرفته مشخص شد که اگر مقدار processModel memoryLimit به حدود 800 مگابایت تنظیم شود، بهترین عملکرد را برای سیستم‌های مختلف می‌توان مشاهده کرد.

با ارائه‌ی ویندوز سرور 2003 و همچنین ارائه‌ی نسخه‌ی 1.1 دات نت فریم ورک و ASP.NET ، این وضعیت تغییر کرد. پروسه‌ی جدید در اینجا w3wp نام دارد و این پروسه تعاریف مرتبط با محدودیت حافظه‌ی خود را از تنظیمات IIS دریافت می‌کند (قسمت Maximum Used Memory در برگه‌ی Recycling مربوط به خواص Application Pool مرتبط). متاسفانه این عدد به صورت پیش فرض محدودیتی ندارد و به ظاهر برنامه مجاز است تا حد امکان از حافظه‌ی مهیا استفاده کند. به همین جهت یکی از مواردی را که باید در نظر داشت، مقدار دهی Maximum Used Memory ذکر شده است. خصوصا اینکه در نگارش 1.1 ، تنظیمات میزان مصرف RAM مرتبط با ASP.NET Cache نیز با برنامه یکی است.

در نگارش 2.0 دات نت فریم ورک، تنظیمات مرتبط با ASP.NET cache از تنظیمات میزان RAM مصرفی یک برنامه‌ی ASP.NET جدا شد و این مورد توسط قسمت cache privateBytesLimit قابل تنظیم و مدیریت است (در فایل IIS Metabase و همچنین فایل web.config برنامه).

نکته!
اگر process memory limit و همچنین cache memory limit را تنظیم نکنید، باز به همان عدد 60 درصد سابق بازخواهیم گشت و این مورد به صورت خودکار توسط IIS محاسبه و اعمال می‌شود. البته محدودیت ذکر شده برای پروسه‌های 64 بیتی در این حالت بسیار بهتر خواهد بود. اگر هر دوی این‌ها را تنظیم کنید، عدد حداقل بکارگرفته شده، مبنای کار خواهد بود و اگر تنها یکی را تنظیم کنید ، این عدد به هر دو حالت اعمال می‌گردد. برای بررسی بهتر می‌توان به مقدار Cache.EffectivePrivateBytesLimit و Cache.EffectivePercentagePhysicalMemoryLimit مراجعه کرد.

و ... اکنون بهتر می‌توانید به این سؤال پاسخ دهید که «سرور ما بیشتر از 4 گیگ رم دارد و برنامه‌ی ASP.NET من الان فقط 850 مگ رم مصرف کرده (که البته این هم نشانی از عدم dispose صحیح منابع است یا عدم تعیین تقدم و تاخر و زمان منقضی شدن، حین تعریف اشیاء کش)، اما پیغام out of memory exception را دریافت می‌کنم. چرا؟!»


بنابراین ایجاد یک Application pool جدید به ازای هر برنامه‌ی ASP.NET امری است بسیار مهم زیرا:
- به این ترتیب هر برنامه‌ی ASP.NET در پروسه‌ای ایزوله از پروسه‌ی دیگر اجرا خواهد شد (این مساله از لحاظ امنیتی هم بسیار مهم است). در اینجا هر برنامه، از پروسه‌ی w3wp.exe مجزای خاص خود استفاده خواهد کرد (شبیه به مرورگرهایی که هر tab را در یک پروسه جدید اجرا می‌کنند).
- اگر پروسه‌ای به حد بالای مصرف حافظه‌ی خود رسید با تنظیمات انجام شده در قسمت recycling مرتبط با Application pool اختصاصی آن، به صورت خودکار کار بازیابی حافظه صورت می‌گیرد و این امر بر روی سایر برنامه‌ها تاثیر نخواهد داشت (کاربران سایر برنامه‌ها مدام شکایت نمی‌کنند که سشن‌ها پرید. کش خالی شد. زیرا در حالت وجود application pool اختصاصی به ازای هر برنامه، مدیریت حافظه برنامه‌ها از هم ایزوله خواهند بود)
- کرش صورت گرفته در یک برنامه به دلیل عدم مدیریت خطاها، بر روی سایر برنامه‌ها تاثیر منفی نخواهد گذاشت. (زمانیکه ASP.NET worker process به دلیل استثنایی مدیریت نشده خاتمه یابد بلافاصله و به صورت خودکار مجددا «وهله‌ی دیگری» از آن شروع به کار خواهد کرد؛ یعنی تمام سشن‌های قبلی از بین خواهند رفت؛ که در صورت ایزوله سازی ذکر شده، سایر برنامه‌ها در امان خواهند ماند؛ چون در پروسه ایزوله‌ی خود مشغول به کار هستند)
- با وجود application pool اختصاصی به ازای هر برنامه، می‌توان برای سایت‌های کم ترافیک و پرترافیک، زمان‌های recycling متفاوتی را اعمال کرد. به این ترتیب مدیریت حافظه‌ی بهتری قابل پیاده سازی می‌باشد. همچنین در این حالت می‌توان مشخص کرد کدام سایت از تعداد worker process بیشتر یا کمتری استفاده کند.
- کاربری که پروسه‌ی ASP.NET تحت آن اجرا می‌شود نیز همینجا تعریف می‌گردد. بنابراین به این ترتیب می‌توان به برنامه‌ای دسترسی بیشتر و یا کمتر داد، بدون تاثیر گذاری بر روی سایر برنامه‌های موجود.

نتیجه گیری:
- از IIS استفاده می‌کنید؟ آیا می‌دانید Application pool چیست؟
- آیا می‌دانید در صورت عدم مقدار دهی پارامترهای حافظه‌ی یک Application pool ، به صورت پیش فرض چند درصد از حافظه‌ی فیزیکی مهیا در اختیار شما است؟


برای مطالعه بیشتر:

مطالب
سایت‌های مهمی که از ASP.NET MVC استفاده می‌کنند

عموما استفاده وسیع از نگارش‌های مختلف ASP.NET مربوط به اینترانت‌های شرکت‌های خصوصی و دولتی است. برنامه‌هایی که هیچ وقت رنگ آسمان را هم نخواهند دید و کسی از آمار یا وجود آن‌ها مطلع نخواهد شد. اما در این بین هستند سایت‌های عمومی که از این فناوری‌ها استفاده می‌کنند. مهم‌ترین و پرترافیک‌ترین سایت‌هایی که در حال حاضر از ASP.NET MVC کمک می‌گیرند شامل موارد زیر هستند:
جالب اینجا است که اخیرا سایت msnbc استفاده وسیعی از RavenDB را هم شروع کرده است.

سایر منابع:

سؤال: چگونه تشخیص دهیم یک سایت از ASP.NET MVC استفاده می‌کند؟

ابتدا افزونه Server Spy را نصب کنید. این افزونه می‌تواند وب سروری را که یک سایت هم اکنون مورد استفاده قرار داده، تشخیص دهد. اگر IIS بود، یعنی این سایت از یکی از مشتقات ASP یا ASP.NET استفاده می‌کند. اگر پسوند صفحات به asp ختم شده بود، ASP‌ کلاسیک دهه نود است. در غیراینصورت یا Web forms است یا MVC. در این حالت به سورس صفحه مراجعه کنید. اگر از ViewState خبری نبود یعنی ASP.NET MVC است.
البته این روش در 90 درصد موارد جواب می‌دهد. می‌شود هدر ارسالی وب سرور را کلا تغییر داد. یعنی ضرورتی ندارد که یک سایت استفاده کننده از IIS حتما اعلام کند که از این وب سرور خاص استفاده می‌کند. یا در ASP.NET Web forms می‌شود ViewState را با ترفندهایی حذف کرد. اما ... این مسایل همه گیر نیست و روش‌ ذکر شده شناسایی، در اکثر موارد جواب می‌دهد.

اشتراک‌ها
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
 public class HandleConcurrencyExceptionAttribute : FilterAttribute, IExceptionFilter
    {
        private PropertyMatchingMode _propertyMatchingMode;
        /// <summary>
        /// This defines when the concurrencyexception happens, 
        /// </summary>
        public enum PropertyMatchingMode
        {
            /// <summary>
            /// Uses only the field names in the model to check against the entity. This option is best when you are using 
            /// View Models with limited fields as opposed to an entity that has many fields. The ViewModel (or model) field names will
            /// be used to check current posted values vs. db values on the entity itself.
            /// </summary>
            UseViewModelNamesToCheckEntity = 0,
            /// <summary>
            /// Use any non-matching value fields on the entity (except timestamp fields) to add errors to the ModelState.
            /// </summary>
            UseEntityFieldsOnly = 1,
            /// <summary>
            /// Tells the filter to not attempt to add field differences to the model state.
            /// This means the end user will not see the specifics of which fields caused issues
            /// </summary>
            DontDisplayFieldClashes = 2
        }


        public HandleConcurrencyExceptionAttribute()
        {
            _propertyMatchingMode = PropertyMatchingMode.UseViewModelNamesToCheckEntity;
        }

        public HandleConcurrencyExceptionAttribute(PropertyMatchingMode propertyMatchingMode)
        {
            _propertyMatchingMode = propertyMatchingMode;
        }


        /// <summary>
        /// The main method, called by the mvc runtime when an exception has occured.
        /// This must be added as a global filter, or as an attribute on a class or action method.
        /// </summary>
        /// <param name="filterContext"></param>
        public void OnException(ExceptionContext filterContext)
        {
            if (!filterContext.ExceptionHandled && filterContext.Exception is DbUpdateConcurrencyException)
            {
                //Get original and current entity values
                DbUpdateConcurrencyException ex = (DbUpdateConcurrencyException)filterContext.Exception;
                var entry = ex.Entries.Single();
                //problems with ef4.1/4.2 here because of context/model in different projects.
                //var databaseValues = entry.CurrentValues.Clone().ToObject();
                //var clientValues = entry.Entity;
                //So - if using EF 4.1/4.2 you may use this workaround
                var clientValues = entry.CurrentValues.Clone().ToObject();
                entry.Reload();
                var databaseValues = entry.CurrentValues.ToObject();

                List<string> propertyNames;

                filterContext.Controller.ViewData.ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                        + "was modified by another user after you got the original value. The "
                        + "edit operation was canceled and the current values in the database "
                        + "have been displayed. If you still want to edit this record, click "
                        + "the Save button again to cause your changes to be the current saved values.");
                PropertyInfo[] entityFromDbProperties = databaseValues.GetType().GetProperties(BindingFlags.FlattenHierarchy | BindingFlags.Public | BindingFlags.Instance);

                if (_propertyMatchingMode == PropertyMatchingMode.UseViewModelNamesToCheckEntity)
                {
                    //We dont have access to the model here on an exception. Get the field names from modelstate:
                    propertyNames = filterContext.Controller.ViewData.ModelState.Keys.ToList();
                }
                else if (_propertyMatchingMode == PropertyMatchingMode.UseEntityFieldsOnly)
                {
                    propertyNames = databaseValues.GetType().GetProperties(BindingFlags.Public).Select(o => o.Name).ToList();
                }
                else
                {
                    filterContext.ExceptionHandled = true;
                    UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);
                    filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
                    return;
                }



                UpdateTimestampField(filterContext, entityFromDbProperties, databaseValues);

                //Get all public properties of the entity that have names matching those in our modelstate.
                foreach (var propertyInfo in entityFromDbProperties)
                {

                    //If this value is not in the ModelState values, don't compare it as we don't want
                    //to attempt to emit model errors for fields that don't exist.

                    //Compare db value to the current value from the entity we posted.

                    if (propertyNames.Contains(propertyInfo.Name))
                    {
                        if (propertyInfo.GetValue(databaseValues, null) != propertyInfo.GetValue(clientValues, null))
                        {
                            var currentValue = propertyInfo.GetValue(databaseValues, null);
                            if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString()))
                            {
                                currentValue = "Empty";
                            }

                            filterContext.Controller.ViewData.ModelState.AddModelError(propertyInfo.Name, "Current value: "
                                 + currentValue);
                        }
                    }

                    //TODO: hmm.... how can we only check values applicable to the model/modelstate rather than the entity we saved?
                    //The problem here is we may only have a few fields used in the viewmodel, but many in the entity
                    //so we could have a problem here with that.
                    //object o = propertyInfo.GetValue(myObject, null);
                }

                filterContext.ExceptionHandled = true;

                filterContext.Result = new ViewResult() { ViewData = filterContext.Controller.ViewData };
            }
        }
پیاده سازی یک Filter سفارشی برای نگاشت استثنای همزمانی به خطاهای MadelState
مطالب
کاهش حجم قابل ملاحظه‌ی برنامه‌های Angular با استفاده از RxJS 5.5
Angular 5.x به همراه پشتیبانی از RxJS 5.5.x منتشر شده‌است. RxJS 5.5 نیز به همراه تغییر قابل ملاحظه‌ای در نحوه‌ی import اجزای آن توسط ویژگی جدید lettable operators است. در این مطلب نحوه‌ی ارتقاء برنامه‌های قبلی به این نگارش جدید و همچنین اثر آن‌را بر اندازه‌ی برنامه‌ی نهایی تولید شده، بررسی می‌کنیم.


روش جدید import اجزای RxJS در نگارش 5.5 آن

تغییرات تعاریف عملگرها:
تا پیش از Angular 5 و RxJS 5.5 (و یا Angular CLI versions <1.5.0)، اگر نیاز به عملگری (operator/function) مانند map وجود داشت، روش import آن به صورت زیر بود:
import 'rxjs/add/operator/map';
اما پس از RxJS 5.5 امکان import آن‌ها با روش مخصوص ES 6 میسر شده‌است (به نام جمع operators دقت داشته باشید؛ چون مسیر rxjs/observable نیز وجود دارد):
import { map } from 'rxjs/operators';
بنابراین در این حالت دیگر روش import یکجای این تعاریف در فایلی مانند «rxjs-operators.ts» وجود ندارد و این تعاریف باید به ازای هر فایلی که از آن‌ها استفاده می‌کنند، مانند سایر importهای ES 6 یکبار دیگر مجددا ذکر شوند؛ مانند:
import { map, catchError, tap } from 'rxjs/operators';
در حالت کلی مسیر node_modules/rxjs/operators را برای یافتن متدهای جدید بررسی کنید.

همچنین در این نگارش، Observable بجای rxjs/Rx :
import { Observable } from 'rxjs/Rx';
از rxjs/Observable دریافت می‌شود:
import { Observable } from 'rxjs/Observable';
تا بتوان از قابلیت‌های جدید آن استفاده کرد.

تغییرات تعاریف statics:
برای صدور خطاها بجای throw قبلی:
import 'rxjs/add/observable/throw';

Observable.throw('error');
خواهیم داشت:
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';

ErrorObservable.create('error');

و برای ایجاد تایمر، بجای timer پیشین:
import "rxjs/add/observable/timer";

const timerSource$ = Observable.timer(initialDelay);
خواهیم داشت:
import { timer } from 'rxjs/observable/timer';  

const timerSource$ = timer(initialDelay);
و به طور کلی مسیر node_modules\rxjs\observable را برای یافتن تعاریف static قبلی جستجو کنید.


معرفی lettable operators

Lettable Operators توابعی هستند که یک observable را دریافت و یک observable را بازگشت می‌دهند؛ به آن‌ها pipeable operators هم می‌گویند. از این جهت که در اینجا متد جدید pipe، برای ترکیب چندین تابع عملگر بر روی مقادیر observable توسط آن، ارائه شده‌است.
مزیت این روش این است که pipeable/lettable operators، یک سری تابع محض هستند و اگر مورد استفاده قرار نگیرند، به سادگی توسط سیستم و ابزار ساخت برنامه، از فایل نهایی حذف خواهند شد؛ یا اصطلاحا tree-shakable هست. اما روش پیشین تعریف این عملگرها، tree-shakable نبوده و حتی اگر توسط برنامه مورد استفاده قرار نگیرند، در بسته‌ی نهایی تولید شده، حضور خواهند داشت. Tree-shaking به معنای پروسه‌ی حذف کدهای مرده است. روش جدید استفاده‌ی از importهای ES 6، امکان تشخیص عملگرهای استفاده نشده را توسط ابزارهایی مانند TS-Lint و تنظیمات کامپایلر TypeScript به سادگی میسر می‌کنند و به این ترتیب با حذف متدها و ماژول‌های استفاده نشده، می‌توان به حجم نهایی بسیار کمتری رسید.


روش قبلی تعریف عملگرهای Observable، اصطلاحا Patching نامیده می‌شود. به این معنا که هر متد جدید import شده‌ی در برنامه، به Observable.prototype اصلی اضافه و وصله می‌شود. اما در این روش جدید، تنها متد وصله شده‌ی از پیش موجود، Observable.prototype.pipe است و تمام متدهای دیگر import شده، توابع محض هستند و نه وصله‌ای به Observable.prototype اصلی. زمانیکه وصله‌ای به Observable.prototype متصل می‌شود، دیگر امکان حذف آن توسط ابزارهای ساخت برنامه وجود ندارد (حتی اگر استفاده نشده باشند)؛ اما اگر این متدها به صورت خالص و مجزای از Observable.prototype ارائه شوند، امکان حذف کدهای مرده و استفاده نشده، به سادگی میسر خواهد شد؛ چون توابعی خالص و متکی به خود هستند.

یک نمونه مثال استفاده‌ی از pipeable/lettable operators را در کد زیر مشاهده می‌کنید:
import { from} from 'rxjs/observable/from';
import { map, scan, filter } from 'rxjs/operators';

const source$ = range(1,10);

const sumOfSquaredOddNumbers$ = source$.pipe(
   filter(n => n % 2 !== 0), 
   map(n => n * n),
   scan((acc,s) => acc + s, 0)
);
sumOfSquaredOddNumbers$.subscribe(v => console.log(v));

/*** Output ****/ 
1
10
35
84
165
این مثال، جمع به توان 2 اعداد را در یک بازه‌ی مشخص، محاسبه می‌کند. برای این منظور ابتدا یک منبع Observable توسط متد range ایجاد شده‌است.
در اینجا روش تعریف Observableها نیز تغییر کرده‌است و از متد of جهت کار با تعدادی ورودی مشخص و یا متد range برای کار با بازه‌ای از اعداد، استفاده می‌شود:
import { of } from 'rxjs/observable/of';
import { from } from 'rxjs/observable/from';
import { range } from 'rxjs/observable/range';

const source$ = of(1,2,3);
const rangeSource$ = range(0,5);
سپس توسط متد pipe، ترکیبی از متدهای RxJS را مشاهده می‌کند که بر روی منبع Observable اصلی کار می‌کنند.
متد filter، اعداد فرد بازه را انتخاب می‌کند. متد map این اعداد انتخابی را به توان 2 می‌رساند و سپس متد scan آن‌ها را با هم جمع می‌زند و نتیجه توسط متد pipe به صورت یک Observable دیگر بازگشت داده می‌شود که می‌توان مشترک آن شد و برای مثال خروجی فوق را در console درج کرد.


تغییر نام عملگرهای قبلی RxJS

تا اینجا دریافتیم که هدف اصلی pipeable/lettable operators، عدم معرفی آن‌ها به صورت یک وصله‌ی جدید جدانشدنی از Observable.prototype، به صورت توابع خالص است. اکنون که این عملگرها، تبدیل به متدهای خالص و متکی به خود شده‌اند، نباید با متدهای اصلی جاوا اسکریپت تداخل نام پیدا کنند؛ به همین جهت برای ارتقاء کدهای قدیمی خود، به این تغییر نام‌ها نیاز خواهید داشت: متد do به tap تغییر نام یافته‌است. متد switch شده‌است switchAll. بجای catch اینبار catchError داریم و finally شده‌است finalize.


مثالی از ارتقاء کدهای قدیمی به روش جدید RxJS 5.5

اگر مثال روش قدیمی مبتنی بر وصله کردن Observable.prototype، به صورت زیر باشد:
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/filter';

names = allUserData.
         map(user => user.name).
         filter(name => name);
معادل جدید آن به این صورت تغییر می‌کند:
import { Observable } from 'rxjs/Observable';
import { map, filter } from 'rxjs/operators';

names = allUserData.pipe(
   map(user => user.name),
   filter(name => name),
);
زمانیکه تعریف Observable از مسیر rxjs/Observable درخواست می‌شود، به همراه عملگر وصله شده‌ی pipe نیز هست. به همین جهت نیازی به تعریف مجدد آن نیست. پس از آن متدهای map و filter، به داخل pipe منتقل می‌شوند. در این بین نیاز است تغییر نام متدها را که پیشتر نیز ذکر شد، مدنظر داشته باشید.
به عنوان یک مثال تکمیلی، کدهای سری «احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular» جهت استفاده‌ی از pipeable/lettable operators به روز رسانی شده‌اند. لیست تغییرات آن‌ها را در اینجا می‌توانید مشاهده کنید.
اشتراک‌ها
دوره 7 ساعته ساخت برنامه‌های Blazor Server با کامپوننت‌های Syncfusion

Learn how to harness the power of the Syncfusion UI components from within a Blazor server application. We’ll also integrate the Microsoft Identity technology into our Blazor application to leverage login, registration, authorization and authentication functionality. Syncfusion provides a UI component suite for building powerful web, desktop, and mobile apps.

⭐️ Course Contents ⭐️
⌨️ (0:00:13) Introduction
⌨️ (0:00:49) Course Overview
⌨️ (0:10:25) Technologies used to Develop the Sales Management Application
⌨️ (0:13:20) Getting Started - Create the Blazor Project through Visual Studio 2022
⌨️ (0:15:02) Introduction to the Syncfusion DataGrid Component
⌨️ (0:43:39) Create the Database using Ef Core Code First Migrations
⌨️ (1:22:02) Integrate the Syncfusion DataGrid Component into the Application
⌨️ (3:02:44) Integrate the Syncfusion ListView component into the Sale Management Application
⌨️ (4:25:23) Integration of the Syncfusion Charts into the Sales Management Application to Display Sales Order Analytical Data
⌨️ (5:11:04) Create Dashboards for Employees
⌨️ (6:03:51) Integrate the Syncfusion Diagram into the Sales Management Application
⌨️ (6:22:25) Integrate the Syncfusion Scheduler into the Sales Management Application
⌨️ (6:52:53) Integrate Microsoft Identity into the Sales Management Application
⌨️ (7:40:34) Wrapping up 

دوره 7 ساعته ساخت برنامه‌های Blazor Server با کامپوننت‌های Syncfusion
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت ششم
در این قسمت مدل‌های باقی مانده‌ی از بخش‌هایی را که در مقاله اول مطرح شدند، به اتمام می‌رسانیم. همچنین با بازخوردهایی که در مقالات قبل گرفتیم، در این قسمت تغییرات ایجاد شده‌ی در مدل‌های قسمت‌های قبل را نیز مطرح خواهیم کرد.

مدل‌های AuditLog (اصلاحیه)و ActivityLog

باید توجه داشت که اگر سیستم AuditLog، جزئیات بیشتری را در بر بگیرد، می‌توان از آن به عنوان History هم یاد کرد. در قسمت چهارم برای پست‌های انجمن یک جدول جدا هم به منظور ذخیره سازی تاریخچه‌ی تغییرات، در نظر گرفتیم. فرض کنید که یک سری از جداول دیگر هم نیازمند این امکان باشند! راه حل چیست؟
  1. استفاده از جداول جدا برای هر کدام از جداول به صورتیکه یک ارتباط یک به چند مابین آنها برقرار است. از این جداول تحت عنوان HistoryTable یاد می‌شود.
  2. استفاده از یک جدول برای نگهداری تاریخچه‌ی تغییرات جداولی که نیازمند این امکان هستند. 
در زیر پیاده سازی از روش دوم رو مشاهده میکنید.
  /// <summary>
    /// Represent The Operation's log
    /// </summary>
    public class AuditLog
    {
        #region Ctor
        /// <summary>
        /// Create One Instance Of <see cref="AuditLog"/>
        /// </summary>
        public AuditLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier of AuditLog
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets Type of  Modification(create,softDelet,Delete,update)
        /// </summary>
        public virtual AuditAction Action { get; set; }
        /// <summary>
        /// sets or gets description of Log
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// sets or gets when log is operated
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// sets or gets Type Of Entity 
        /// </summary>
        public virtual string Entity { get; set; }
        /// <summary>
        /// gets or sets  Old value of  Properties before modification
        /// </summary>
        public virtual string XmlOldValue { get; set; }
        /// <summary>
        /// gets or sets XML Base OldValue of Properties (NotMapped)
        /// </summary>
        public virtual XElement XmlOldValueWrapper
        {
            get { return XElement.Parse(XmlOldValue); }
            set { XmlOldValue = value.ToString(); }
        }
        /// <summary>
        /// gets or sets new value of  Properties after modification
        /// </summary>
        public virtual string XmlNewValue { get; set; }
        /// <summary>
        /// gets or sets XML Base NewValue of Properties (NotMapped)
        /// </summary>
        public virtual XElement XmlNewValueWrapper
        {
            get { return XElement.Parse(XmlNewValue); }
            set { XmlNewValue = value.ToString(); }
        }
        /// <summary>
        /// gets or sets Identifier Of Entity
        /// </summary>
        public virtual string EntityId { get; set; }
        /// <summary>
        /// gets or sets user agent information
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets user's ip address
        /// </summary>
        public virtual string OperantIp { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets log's creator
        /// </summary>
        public virtual User Operant { get; set; }
        /// <summary>
        /// sets or gets identifier of log's creator
        /// </summary>
        public virtual long OperantId { get; set; }
        #endregion
    }
  public enum AuditAction
    {
        Create,
        Update,
        Delete,
        SoftDelete,
    }

خصوصیاتی که نیاز به توضیح خواهند داشت:
  • Action : از نوع AdutiAction است و برای مشخص کردن نوع عملیاتی که انجام شده است، می‌باشد.
  • Description : اگر نیاز باشد توضیحاتی اضافی ثبت شوند، از این خصوصیت استفاده می‌شود.
  • Entity : مشخص کننده‌ی نام مدل خواهد بود. شاید بهتر بود از یک Enum استفاده می‌شد. ولی این سیستم به احتمال زیاد قرار است افزونه پذیر باشد و استفاده از Enum، یعنی محدودیت و این امکان وجود نخواهد داشت که سایر افزونه‌ها بتوانند از مدل بالا استفاده کنند. برا ی مثال BlogPost , NewsItem , ForumPost , ...
  • EntitytId : آی دی رکوردی است که تاریخچه‌ی آن ثبت شده است. از آنجائیکه بعضی از موجودیت‌ها دارای آی دی از نوع long و برخی دیگر Guid ، لذا ذخیره‌ی رشته‌ای آن مفید خواهد بود.
  • XmlOldValue : در برگیرنده‌ی مقدار (قبل از اعمال تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
  • XmlNewValue : در برگیرنده‌ی مقدار (بعد از تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشته‌ای ذخیره شوند.
  • Operant, OperantId: برای برقراری ارتباط یک به چند مابین مدل کاربر و مدل بالا در نظر گرفته شده‌اند که به عنوان انجام دهنده‌ی این تغییرات بوده است.
با استفاده از مدل بالا می‌توان متوجه شد که کاربر x چه خصوصیاتی از  موجودیت y را تغییر داده است و این خصوصیات قبل از تغییر چه مقدارهایی داشته‌اند.
  /// <summary>
    /// Represents Activity Log record
    /// </summary>
    public class ActivityLog
    {
        #region Ctor
        /// <summary>
        /// Create one instance of <see cref="ActivityLog"/>
        /// </summary>
        public ActivityLog()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            OperatedOn=DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets the comment of this activity
        /// </summary>
        public virtual string Comment { get; set; }
        /// <summary>
        /// gets or sets the date that this activity was done
        /// </summary>
        public virtual DateTime OperatedOn { get; set; }
        /// <summary>
        /// gets or sets the page url . 
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or sets the title of page if Url is Not null
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets user agent information
        /// </summary>
        public virtual string Agent { get; set; }
        /// <summary>
        /// gets or sets user's ip address
        /// </summary>
        public virtual string OperantIp { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets the type of this activity
        /// </summary>
        public virtual ActivityLogType Type{ get; set; }
        /// <summary>
        /// gets or sets the  type's id of this activity
        /// </summary>
        public virtual Guid TypeId { get; set; }
        /// <summary>
        /// gets or sets User that done this activity
        /// </summary>
        public virtual User Operant { get; set; }
        /// <summary>
        /// gets or sets Id of User that done this activity
        /// </summary>
        public virtual long OperantId { get; set; }
        #endregion
    }

   /// <summary>
    /// Represents Activity Log Type Record
    /// </summary>
    public class ActivityLogType
    {
        #region Ctor
        /// <summary>
        /// Create one Instance of <see cref="ActivityLogType"/>
        /// </summary>
        public ActivityLogType()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier 
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets the system name
        /// </summary>
        public virtual string Name{ get; set; }
        /// <summary>
        /// gets or sets the display name
        /// </summary>
        public virtual string DisplayName { get; set; }
        /// <summary>
        /// gets or sets the description 
        /// </summary>
        public virtual string Description { get; set; }
        /// <summary>
        /// indicate this log type is enable for logging
        /// </summary>
        public virtual bool IsEnabled { get; set; }
        #endregion
    }
مدل‌های بالا هم برای ثبت لاگ فعالیت‌های کاربران در سیستم در نظر گرفته شده است . برای مثال اگر بخش آخرین تغییرات سایت جاری را هم مشاهده کنید، یک همچین سیستمی را هم دارد. این لاگ‌ها برای ردیابی عملکرد کاربران در سیستم مفید خواهد بود.
  • Comment : توضیحات کوتاهی از اکشنی که کاربر انجام داده است.
  • Url : آدرس صفحه‌ای که این عملیات در آنجا انجام شده است. این خصوصیت نال‌پذیر می‌باشد.
  • Title : عنوان صفحه‌ای که این عملیات در آنجا انجام شده است؛ اگر Url نال نباشد.
  • Operant , OperantId : برای برقراری ارتباط یک به چند بین کاربر و مدل فعالیت‌ها در نظر گرفته شده‌اند.
  • Type : از نوع ActivityLogType پیاده سازی شده در بالا می‌باشد. با استفاده از مدل ActivityLogType می‌توان مثلا لاگ فعالیت مربوط به بخش اخبار را غیر فعال کند یا بالعکس و از این موارد.
خصوصیات مدل ActivityLogType :
  • Name : نام سیستمی آن است. برای مثال : Login ، NewsComment ، NewsItem و ...
  • IsEnabled : نشان دهنده‌ی این است که این نوع لاگ فعال است یا خیر.

کلاس پایه تمام مدل‌ها (اصلاحیه)

/// <summary>
    /// Represents the  entity
    /// </summary>
    /// <typeparam name="TForeignKey">type of user's Id that can be long or long? </typeparam>
    public abstract class Entity<TForeignKey>
    {
        #region Properties
        /// <summary>
        /// gets or sets date that this entity was created
        /// </summary>
        public virtual DateTime CreatedOn { get; set; }
        /// <summary>
        /// gets or sets Date that this entity was updated
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets IP Address of Creator
        /// </summary>
        public virtual string CreatorIp { get; set; }
        /// <summary>
        /// gets or set IP Address of Modifier
        /// </summary>
        public virtual string ModifierIp { get; set; }
        /// <summary>
        /// indicate this entity is Locked for Modify
        /// </summary>
        public virtual bool ModifyLocked { get; set; }
        /// <summary>
        /// indicate this entity is deleted softly
        /// </summary>
        public virtual bool IsDeleted { get; set; }
        /// <summary>
        /// gets or sets information of user agent of modifier
        /// </summary>
        public virtual string ModifierAgent { get; set; }
        /// <summary>
        /// gets or sets information of user agent of Creator
        /// </summary>
        public virtual string CreatorAgent { get; set; }
        /// <summary>
        /// gets or sets date that this entity repoted last time
        /// </summary>
        public virtual DateTime? ReportedOn { get; set; }
        /// <summary>
        /// gets or sets counter for Content's report
        /// </summary>
        public virtual int ReportsCount { get; set; }
        /// <summary>
        /// gets or sets count of Modification Default is 1
        /// </summary>
        public virtual int Version { get; set; }
        /// <summary>
        /// gets or sets action (create,update,softDelete) 
        /// </summary>
        public virtual AuditAction Action { get; set; }
        /// <summary>
        /// gets or sets TimeStamp for prevent concurrency Problems
        /// </summary>
        public virtual byte[] RowVersion { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets ro sets User that Modify this entity
        /// </summary>
        public virtual User ModifiedBy { get; set; }
        /// <summary>
        /// gets ro sets Id of  User that modify this entity
        /// </summary>
        public virtual TForeignKey ModifiedById { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual User CreatedBy { get; set; }
        /// <summary>
        /// gets ro sets User that Create this entity
        /// </summary>
        public virtual TForeignKey CreatedById { get; set; }
        #endregion
    }

  /// <summary>
    /// Represents the base Entity
    /// </summary>
    /// <typeparam name="TKey">type of Id</typeparam>
    /// <typeparam name="TForeignKey">type of User's Id that can be long or long?</typeparam>
    public abstract class BaseEntity<TKey,TForeignKey> : Entity<TForeignKey>
    {
        #region Properties
        /// <summary>
        /// gets or sets Identifier of this Entity
        /// </summary>
        public virtual TKey Id { get; set; }
        #endregion
    }
از دو کلاس معرفی شده‌ی در بالا برای کپسوله کردن یکسری خصوصیات تکراری استفاده شده است. البته با بهبودهایی نسبت به مقاله‌ی قبل که با مشاهده‌ی خصوصیات آنها قابل فهم خواهد بود. در برخی از مدل‌ها، برای مثال نظرات وبلاگ امکان ارسال نظر برای افراد Anonymous هم وجود داشت؛ لذا CreatedById امکان نال بودن را هم داشت. به همین دلیل برای کاهش کدها، کلاس‌های بالا را به صورت جنریک تعریف کردیم. فایل EDMX نهایی که در انتهای مقاله ضمیمه شده، برای درک تغییرات اعمال شده مفید خواهد.

مدل سیستم آگاه سازی

/// <summary>
    /// Represents the Notification Record
    /// </summary>
    public class Notification
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Notification"/>
        /// </summary>
        public Notification()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            ReceivedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// indicate that this notification is read by owner
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets notification's text body
        /// </summary>
        public virtual string Message { get; set; }
        /// <summary>
        /// gets or sets page url that this notification is related with it
        /// </summary>
        public virtual string Url { get; set; }
        /// <summary>
        /// gets or sets date that this Notification Received
        /// </summary>
        public virtual DateTime ReceivedOn { get; set; }
        /// <summary>
        /// gets or sets the type of notification
        /// </summary>
        public virtual NotificationType Type { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets the id of user that is owner of this notification
        /// </summary>
        public virtual long OwnerId { get; set; }
        /// <summary>
        /// gets or sets the user that is owner of this notification
        /// </summary>
        public virtual User Owner { get; set; }
        #endregion
    }

    public enum  NotificationType
    {
        NewConversation,
        NewConversationReply,
        ...
    }
در این سیستم برای اطلاع رسانی کاربر، علاوه بر ارسال ایمیل، بحث اطلاع رسانی RealTime را هم خواهیم داشت. اطلاع رسانی‌هایی که توسط کاربر خوانده نشده باشند، در جدول حاصل از مدل Notification ذخیره خواهند شد. خصوصیاتی که نیاز به توضیح دارند:
  • IsRead : مشخص کننده‌ی این است که یک اطلاع رسانی خوانده شده است یا خیر. در آخر هر روز اطلاع رسانی‌هایی که دارای خصوصیت IsRead با مقدار true هستند، حذف خواهند شد.
  • Url : برای مواردی که لازم است کاربر با کلیک بر روی آن به صفحه‌ی خاصی هدایت شود.
  • OwnerId, Owner : برای برقراری ارتباط یک به چند بین کاربر و مدل Notification در نظر گرفته شده‌اند.
  • Type : از نوع NotificationType و مشخص کننده‌ی نوع اطلاع رسانی می‌باشد .

مدل Observation

 public class Observation
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Observation"/>
        /// </summary>
        public Observation()
        {
            LastObservedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets datetime of last visit 
        /// </summary>
        public virtual DateTime LastObservedOn { get; set; }
        /// <summary>
        /// gets or sets Id Of section That user is  observing the entity
        /// </summary>
        public virtual string SectionId { get; set; }
        /// <summary>
        /// gets or sets  section That user is  observing in it
        /// </summary>
        public virtual string Section { get; set; }
        #endregion

        #region NavigationProperites
        /// <summary>
        /// gets or sets user that observed the entity
        /// </summary>
        public virtual User Observer { get; set; }
        /// <summary>
        /// gets or sets identifier of user that observed the entity
        /// </summary>
        public virtual long ObserverId { get; set; }
        #endregion
    }
این مدل برای ایجاد امکانی به منظور واکشی لیست افردای که در حال مشاهده‌ی یک بخش خاص هستند، مفید است. فرض کنید در یک انجمن قصد دارید لیست افردای را که در حال مشاهده‌ی آن هستند، در پایین صفحه نمایش دهید. برای تاپیک‌ها هم همین امکان لازم است. لذا مدل بالا مختص مدل خاصی نیست و برای هر بخشی می‌توان از آن استفاده کرد. 
فرض کنیم کاربری قصد هدایت به یک تاپیک را دارد. لذا هنگام هدایت شدن لازم است رکوردی در جدول حاصل از مدل بالا ثبت شود که کاربر x در بخش Topic، در تاریخ d، تاپیک به شماره‌ی y را مشاهده کرد. در صفحه‌ی مشاهده‌ی تاپیک می‌توان لیست افرادی را که قبل از مدت زمان مشخصی تاپیک را مشاهده کرده اند، نمایش داد. این رکورد‌ها را هم با تعریف یک Task می‌توان در بازه‌های زمانی مشخصی حذف کرد.

مدل صفحات داینامیک

/// <summary>
    /// represents one custom page
    /// </summary>
    public class Page : BaseEntity<long, long>
    {
        #region Properties
        /// <summary>
        /// gets or sets the blog pot body
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets the content title
        /// </summary>
        public virtual string Title { get; set; }
        /// <summary>
        /// gets or sets value  indicating Custom Slug
        /// </summary>
        public virtual string SlugUrl { get; set; }
        /// <summary>
        /// gets or sets meta title for seo
        /// </summary>
        public virtual string MetaTitle { get; set; }
        /// <summary>
        /// gets or sets meta keywords for seo
        /// </summary>
        public virtual string MetaKeywords { get; set; }
        /// <summary>
        /// gets or sets meta description of the content
        /// </summary>
        public virtual string MetaDescription { get; set; }
        /// <summary>
        /// gets or sets 
        /// </summary>
        public virtual string FocusKeyword { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content use CanonicalUrl
        /// </summary>
        public virtual bool UseCanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets CanonicalUrl That the Post Point to it
        /// </summary>
        public virtual string CanonicalUrl { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Follow for Seo
        /// </summary>
        public virtual bool UseNoFollow { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content user no Index for Seo
        /// </summary>
        public virtual bool UseNoIndex { get; set; }
        /// <summary>
        /// gets or sets value indicating whether the content in sitemap
        /// </summary>
        public virtual bool IsInSitemap { get; set; }
        /// <summary>
        /// gets or sets title for snippet
        /// </summary>
        public string SocialSnippetTitle { get; set; }
        /// <summary>
        /// gets or sets description for snippet
        /// </summary>
        public string SocialSnippetDescription { get; set; }
        /// <summary>
        /// gets or sets section's type that this page show on
        /// </summary>
        public virtual ShowPageSection Section { get; set; }
        /// <summary>
        /// indicate this page has not any body
        /// </summary>
        public virtual bool IsCategory { get; set; }
        /// <summary>
        /// gets or sets order for display forum
        /// </summary>
        public virtual int DisplayOrder { get; set; }

        #endregion

        #region NavigationProeprties
        /// <summary>
        /// gets or sets Parent of this page
        /// </summary>
        public virtual Page Parent { get; set; }
        /// <summary>
        /// gets or sets parent'id of this page
        /// </summary>
        public virtual long? ParentId { get; set; }
        /// <summary>
        /// get or set collection of page that they are children of this page
        /// </summary>
        public virtual ICollection<Page> Children { get; set; }
        #endregion
    }

  public enum ShowPageSection
    {
        Menu,
        Footer,
        SideBar
    }
مدل بالا مشخص کننده‌ی صفحاتی است که مدیر می‌تواند در پنل مدیریتی آنها را برای استفاده‌های خاصی تعریف کند. حالت درختی آن مشخص است. یکسری از خصوصیات مربوط به محتوای صفحه و همچنین تنظیمات سئو برای آن در نظر گرفته شده است که بیشتر آنها در مقالات قبل توضیح داده شده‌اند. خصوصیت Section از نوع ShowPageSection و برای مشخص کردن امکان نمایش صفحه‌ی مورد نظر در نظر گرفته شده‌است. همچنین این مدل بالا از کلاس پایه‌ی مطرح شده‌ی در اول مقاله، ارث بری کرده است که امکان ردیابی تغییرات آن را مهیا می‌کند.
  خوب! حجم مقاله زیاد شده است و تا اینجا کافی خواهد بود ؛ بر خلاف تصور بنده، یک مقاله‌ی دیگر نیز برای اتمام بحث لازم میباشد.

نتیجه‌ی تا این قسمت

مطالب
C# 7.1 - Tuple Name Inference
در مطلب «C# 7 - Tuple return types and deconstruction» با نوع‌های جدید بازگشتی Tuple در C# 7.0 آشنا شدیم. در C# 7.1 تشخیص نام اعضای Tuple تعریف شده بهبود یافته و از این لحاظ شبیه به anonymous types شده‌است. مفهوم «Name Inference» یا «حدس زدن نام‌ها» را با یک مثال بهتر می‌توان توضیح داد.
string name = "User 1";
int age = 20;
var personTuple = (name, age);
Console.WriteLine(personTuple.Item1); // User 1
Console.WriteLine(personTuple.Item2); // 20
در C# 7.0 مفهوم «Name Inference» پیاده سازی نشده‌است. به همین جهت کامپایلر قادر نیست نام اعضای Tuple تعریف شده‌ی فوق را حدس بزند و برای دسترسی به آن‌ها باید تنها از Item1 و Item2 مانند قبل استفاده کرد. البته اگر برای اعضای Tuple نام تعریف کنیم (قسمت «مفهوم Tuple Literals»)، آنگاه می‌توان Item1 و Item2 را با نام‌های این اعضاء جایگزین کرد:
string name = "User 1";
int age = 20;
var personTuple = (name: name, age:age);
Console.WriteLine(personTuple.name); // User 1
Console.WriteLine(personTuple.age); // 20
بنابراین ذکر نام صریح اعضای Tuple در سی‌شارپ 7 الزامی است؛ در غیراینصورت باید با همان نام‌های عمومی Item1 و Item2 جهت دسترسی به این اعضاء، کار کرد.
این وضعیت در C# 7.1 بهبود یافته‌است و اکنون کامپایلر در صورت عدم ذکر صریح نام اعضای Tuple، قادر است این نام‌ها را دقیقا بر اساس نام متغیرها، همانند قابلیتی که در مورد anonymous types وجود دارد، تعیین کند و حدس بزند:
string name = "User 1";
int age = 20;
var personTuple = (name, age);
Console.WriteLine(personTuple.name); // User 1
Console.WriteLine(personTuple.age); // 20
این مثال، شبیه به اولین مثالی است که در مورد C# 7.0 ذکر شد. اما در C# 7.1 نیازی به ذکر Item1 و Item2 جهت دسترسی به اعضای Tuple تعریف شده نیست (هرچند هنوز هم مجاز است) و کامپایلر نام این اعضاء را از نام متغیرهای متناظر با آن‌ها حدس می‌زند.


مثال‌هایی از حدس زدن نام‌های اعضای Tuple در C# 7.1

مثال اول همان حدس زدن نام‌های اعضای Tuple بر اساس نام متغیرهای محلی متناظر با آن‌ها است.

مثال دوم بر اساس نام خواص یک شیء است که توسط یک نوع Tuple بازگشت داده می‌شود:
var p = new Person
{
   Name = "User 1",
   Age = 20
};
var personTuple = (p.Name, p.Age);
Console.WriteLine(personTuple.Name);
Console.WriteLine(personTuple.Age);

در اینجا عملگر .? نیز پشتیبانی می‌شود:
Person p = null;
var personTuple = (p?.Name, p?.Age);
Console.WriteLine(personTuple.Name); // null
Console.WriteLine(personTuple.Age); // null

مثال سوم همان مثال دوم است که در یک عبارت LINQ بکار رفته‌است:
var people = new List<Person>
{
   new Person {Name = "User 1", Age = 42},
   new Person {Name = "User 2", Age = 18},
   new Person {Name = "User 3", Age = 21}
};

var tuples = people
   .Select(person =>
           (
              person.Name,
              person.Age,
              NameAndAge: $"{person.Name} is {person.Age}"
           )
);
var name = tuples.First().Name;
var age = tuples.First().Age; 
var nameAndAge = tuples.First().NameAndAge;
در اینجا نوع خروجی عبارت LINQ نوشته شده، لیستی از Tupleها است. در Tuple خروجی آن، نام دو عضو اول، از نام خواص متناظر با آن‌ها حدس زده می‌شود. نام عنصر سوم به صورت صریح مشخص شده‌است.


نکته 1: حدس زدن نام‌ها در مورد مقادیر خروجی متدها رخ نمی‌دهد.

در مثال ذیل نمی‌توان به personTuple.FirstName بر اساس نام متد ذکر شده دسترسی یافت و تنها می‌توان از Item1 در مورد آن استفاده کرد؛ اما در مورد متغیر محلی age می‌توان:
int age = 42;
var personTuple = (FirstName(), age);
Console.WriteLine(personTuple.Item1);
Console.WriteLine(personTuple.age);


نکته 2: اگر نام اعضای Tuple یکی باشند، عملیات حدس زدن نام‌ها رخ نمی‌دهد.

var p1 = new Person
{
   Name = "User 1",
   Age = 20
};

var p2 = new Person
{
   Name = "User 2",
   Age = 22
};

var personTuple = (p1.Name, p2.Name);
Console.WriteLine(personTuple.Item1); // User 1
Console.WriteLine(personTuple.Item2); // User 2
در این مثال چون Tuple تشکیل شده دارای نام‌های یکسان Name است، امکان حدس زدن نام‌ها میسر نیست و در اینجا نیز باید از طریق Item1 و ... به اعضای Tuple دسترسی یافت (و یا می‌توان به هر عضو Tuple یک نام منحصربفرد را انتساب داد).
مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


یک پروژه Web Forms بسازید

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

کنترلر ایجاد شده شامل یک سری متد است که بصورت خودکار برای شما اضافه شده اند، آنها را حذف کنید و کد زیر را به کنترلر خود اضافه کنید.

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

        public IEnumerable<Product> GetAllProducts()
        {
            return products;
        }

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

همانطور که می‌بینید در مثال جاری از فایل محلی استفاده شده است اما در اپلیکیشن‌های واقعی بهتر است از CDN‌‌ها استفاده کنید.

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید: