نظرات مطالب
اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
- قسمت «cfg.TokenValidationParameters» در سمت سرور، کار بررسی امضای دیجیتال و سایر مشخصات امنیتی را انجام می‌دهد. بنابراین امکان دستکاری آن در سمت کاربر نیست. اگر دستکاری شود، امضای دیجیتال آن در سمت سرور برگشت می‌خورد و در قسمت cfg.Events لاگ خواهد شد.
- در حافظه‌ی سرور چیزی را نباید ذخیره کنید. سمت سرور برنامه‌های وب، stateless و یا بدون حالت است (یعنی قرار نیست در آن، پس از پایان درخواست، اطلاعاتی از کاربر در حافظه باقی بماند؛ بدلیل محدودیت منابع و مسایل امنیتی). اما این توکن‌ها در سمت کاربر عموما توسط روش local storage ذخیره می‌شوند و هربار از سرور واکشی نمی‌شوند. این توکن‌ها فقط هربار به ازای هر درخواست به منابع محافظت شده‌ی سمت سرور، «باید» از کلاینت به سمت سرور ارسال شوند. در غیراینصورت درخواست بدون توکن، یک درخواست معمولی اعتبارسنجی نشده‌است و ذخیره سازی اطلاعات در حافظه‌ی سرور عملا بی‌معنا است. اطلاعات بیشتر در مورد ذخیره سازی سمت کلاینت: «معرفی Local Storage و چند کتابخانه مرتبط» و «ذخیره سازی اطلاعات در مرورگر توسط برنامه‌های Angular»  
اشتراک‌ها
رایگان شدن مخازن خصوصی github

سرویس github مخازن خصوصی خود رو رایگان کرد. از این پس می‌توان بصورت نامحدود repository‌های private ایجاد کرد. البته نسخه رایگان شامل محدودیت هایی خواهد بود:

- عدم اضافه کردن wiki

- محدودیت تعداد مشارکت کننده‌ها به 3 نفر

- عدم ساخت github page

رایگان شدن مخازن خصوصی github
نظرات مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 4 - طول حیات سرویس ها یا Service Lifetime
- هر درخواست ASP.NET Core، توسط یک ترد مدیریت می‌شود. بنابراین، هرچند کل برنامه‌ی وب، چندریسمانی است، اما به معنای به اشتراک گذاری اطلاعات آن درخواست خاص، با تردها و درخواست‌های دیگر نیست.
- Scoped بودن یک سرویس، فقط به معنای یکبار نمونه سازی آن در طول یک درخواست هست و هیچ معنای دیگری ندارد و هیچ ضمانت خاصی به همراه آن نیست.
- آیا می‌توان در طول یک درخواست، اطلاعات این سرویس Scoped را توسط سرویس‌های دیگر تغییر داد؟ بله. نمونه‌اش پیاده سازی الگوی واحد کار با Scoped تعریف کردن DbContext برنامه است. سرویس‌های مختلف، بر روی اطلاعات این یک نمونه از سرویس در دسترس، می‌توانند تاثیر گذار باشند و در آخر کار فقط یک تراکنش Commit شود.
- آیا اگر درون یک درخواست، آن یک نمونه از سرویس Scoped را به صورت چندریسمانی استفاده کنیم، thread-safe است؟ خیر. اطلاعات بیشتر.
نظرات مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت اول - نصب پیشنیازها
با سلام و خسته نباشید و تشکر از بابت مطالب مفیدتان
سوال بنده در مورد Angular اینه که آیا برای نوشتن سایت‌های تجاری مفیده یا نه ،
چند مورد که من بهش فکر کردم و جایی نتونستم ازش نتیجه بگیرم موارد زیره که اگه راهنمایی کنید برای ادامه آموزش بهتر
1- ایا آوردن اطلاعات و محتویات سایت همراه اکشن درخواست صفحه بهتر نیست و باعث سرعت بیشتر سایت نمیشه؟(معمولا اطلاعات در ساختار فریمورک انگولا بعد از لود شدن همه html‌ها و فایل‌های js  و با یک سرویس دریافت میشه "مثل لود کردن محتوی ایجکسی" که به نظرم به خاطر دو مرحله ای بودن کندی زیادی داره ) 
2- آیا در Seo تاثیر داره ؟ محتوا و روتینگ‌ها همه با ایجکس انجام میشه و نمیدونم گوگل این چیزا روی میفهمه یا نه 
در کل ممنون میشه که بدونم شما اگه بخواید یه سایت مثلا همین سایتتون را از اول بنویسید آیا از این فریمورک استفاده میکنید یانه
نظرات مطالب
هزینه استفاده از دات نت فریم ورک چقدر است؟
راستی،‌ ویندوز 8 دارای دات نت نگارش «4 و نیم» سرخود است. این هم رایگان است برای خریداران ویندوز و همچنین توسعه دهنده‌ها هم نگرانی از توزیع آن نخواهند داشت و یک قدم مثبت است در جهت ساده‌تر کردن کارها.
نظرات مطالب
هزینه استفاده از دات نت فریم ورک چقدر است؟
راستی،‌ ویندوز 8 دارای دات نت نگارش «4 و نیم» سرخود است. این هم رایگان است برای خریداران ویندوز و همچنین توسعه دهنده‌ها هم نگرانی از توزیع آن نخواهند داشت و یک قدم مثبت است در جهت ساده‌تر کردن کارها.
مطالب
بررسی معماری پیازی - قسمت اول

 معماری پیازی توسط جفری پالرمو در سال 2008 ابداع شد. این معماری راه بهتری را برای ساخت برنامه‌های کاربردی جهت تست پذیری، نگهداری و قابلیت اطمینان بهتر بر روی زیرساخت‌هایی مانند پایگاه‌های داده و خدمات ارائه می‌دهد. هدف اصلی این معماری، پرداختن به چالش‌های پیش روی معماری 3 لایه و ارائه راه حلی برای مشکلات رایج مانند اتصال و جداسازی وابستگی‌ها است. دو نوع اتصال وجود دارند؛ اتصال محکم و اتصال ضعیف که در ادامه آنها را بررسی میکنیم.


اتصال محکم

هنگامی که یک کلاس، به یک وابستگی مشخصی وابسته است، گفته می‌شود که به شدت با آن کلاس همراه است. یک اتصال محکم جفت شده، به یک شیء دیگر وابسته است. این بدان معناست که تغییر یک شیء در یک برنامه‌ی با اتصال محکم جفت شده، اغلب نیاز به تغییر در تعدادی از اشیاء دیگر دارد. هنگامیکه یک برنامه کوچک است، دشوار نیست، اما در یک برنامه‌ی بزرگ، ایجاد تغییرات بسیار دشوار است.


اتصال ضعیف

یعنی دو شیء مستقل هستند و یک شیء می‌تواند بدون اینکه به آن وابسته باشد، از شیء دیگری استفاده کند. این یک هدف طراحی است که به دنبال کاهش وابستگی‌های متقابل بین اجزای یک سیستم، با هدف کاهش خطر این است که تغییرات در یک جزء، مستلزم تغییر در هر جزء دیگر باشد.


مزایای معماری پیازی

چندین مزیت برای معماری پیازی وجود دارند که در زیر ذکر شده‌اند:

  • قابلیت نگهداری بهتری را فراهم می‌کند؛ زیرا همه کدها به لایه‌ها یا مرکز، بستگی دارند.
  • تست پذیری بهتری را فراهم می‌کند؛ زیرا آزمون واحد را می‌توان برای لایه‌های جداگانه، بدون تأثیر بر سایر ماژول‌های برنامه ایجاد کرد.
  • این برنامه یک برنامه‌ی کاربردی با اتصال آزاد را ایجاد می‌کند؛ زیرا لایه بیرونی برنامه، همیشه از طریق واسط‌ها با لایه داخلی، ارتباط برقرار می‌کند.
  • هرگونه پیاده سازی پیوسته، در زمان اجرا به برنامه ارائه می‌شود.
  • موجودیت‌های دامنه، هسته و بخش مرکزی هستند. می‌تواند به هر دو لایه پایگاه داده و UI دسترسی داشته باشد.
  • لایه‌های داخلی هرگز به لایه خارجی وابسته نیستند. کدی که ممکن است تغییر کرده باشد، باید بخشی از یک لایه خارجی باشد.


لایه‌های معماری پیاز

این معماری به شدت به اصل وارونگی وابستگی، متکی است. رابط کاربری از طریق واسط‌ها با منطق تجاری ارتباط برقرار می‌کند و دارای چهار لایه است. لایه‌ها به سمت مرکز هستند. بخش مرکزی، موجودیت‌های Domain است که نشان‌دهنده موضوعات تجاری و رفتاری است. این لایه‌ها می‌توانند متفاوت باشند اما لایه موجودیت‌های دامنه، همیشه بخشی از دامنه‌ی مرکزی است. لایه دیگر، رفتار بیشتر یک شیء را تعریف می‌کند. در ادامه به توضیح لایه‌های معماری پیاز توجه فرمایید:


Domain Entities Layer

این بخش مرکزی معماری است. تمام اشیاء دامنه‌ی برنامه را در خود نگه می‌دارد. اگر برنامه ای با چهارچوب موجودیت ORM توسعه داده شود، این لایه دارای کلاس‌های POCO (Code First) یا Edmx (Database First) با موجودیت‌ها است. این نهادهای دامنه هیچ وابستگی ندارند.


Repository Layer

این لایه برای ایجاد یک لایه Abstraction بین لایه نهادهای دامنه و لایه منطق تجاری یک برنامه، در نظر گرفته شده‌است. این یک الگوی دسترسی به داده‌است که باعث می‌شود یک رویکرد مرتبط‌تر برای دسترسی به داده‌ها وجود داشته باشد. ما یک مخزن عمومی را ایجاد می‌کنیم که منبع داده را برای داده‌ها جستجو می‌کند، داده‌ها را از منبع داده به یک نهاد تجاری نگاشت می‌کند و تغییرات موجودیت تجاری را به منبع داده ارائه می‌دهد.


Service Layer

این لایه دارای رابط‌هایی است که برای برقراری ارتباط بین لایه UI و لایه مخزن استفاده می‌شود و به همراه منطق تجاری برای یک موجودیت است. بنابراین به آن لایه منطق تجاری نیز می‌گویند.


UI Layer

خارجی‌ترین لایه است و می‌تواند برنامه‌ی وب، Web API یا پروژه واحد تست باشد. این لایه دارای یک پیاده سازی از جنس Dependency Inversion Principle است بطوری که برنامه، یک برنامه‌ی کاربردی جفت شده‌ی آزاد می‌سازد و از طریق واسط‌ها با لایه داخلی ارتباط برقرار می‌کند.

در مطالب بعدی با مبحث معماری پیازی، نکات تکمیلی و مهمتری از لایه پیاز را تشریح میکنیم و یک پروژه را با معماری پیاز، راه اندازی میکنیم.

مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت ششم
در پنج قسمت گذشته به نحوه نصب و راه اندازی Xamarin برای توسعه هر سه پلتفرم Android / iOS / Windows پرداختیم. حال با داشتن محیطی آماده برای کد زدن و تست، می‌توانیم به آموزش کد نویسی بپردازیم.
ابتدا بد نیست نگاهی به ساختار XamApp بیاندازیم. این پروژه‌ی مثال، خود از چهار قسمت تشکیل شده‌است:
XamApp | XamApp.Android | XamApp.UWP | XamApp.iOS
در خود XamApp درصد زیادی (گاهی تا 95%) از کد پروژه پیاده سازی می‌شود که بین سه پلتفرم مشترک است و شامل ظاهر فرم‌ها و منطق برنامه می‌شود. در پاره‌ای از مواقع، لازم می‌شود که از امکانات هر پلتفرم استفاده کنیم و در این صورت به سراغ پروژه‌های Android - iOS - Windows می‌رویم. نه تنها امکانات هر پلتفرم را در همان زبان CSharp و Visual Studio می‌توانید استفاده کنید، بلکه Documentation مربوطه به زبان CSharp برای تک تک امکانات سیستم عامل‌ها وجود دارد و اصلا لازم نیست برای فراگیری نحوه کار AR Kit در iOS به سراغ مستندات Apple بروید و یا برای کار با Intent در Android سری به سایت گوگل بزنید!
برای پیاده سازی UI از XAML استفاده می‌کنیم. XAML خود XML است، به همراه کلی امکانات هیجان انگیز که سعی می‌کنیم بجای ارائه یک توضیح کلی، به مرور و با معرفی امکاناتش، شما را با ماهیت خود XAML آشنا کنیم.
در XAML، همه چیز Tag هست (<>). برای مثال برای ایجاد یک دکمه داریم:
<Button Text="This is a test button" />
در XAML هر تگ، معادل یک کلاس CSharp ای است. در نتیجه ما کلاس Button هم داریم. همچنین Text در مثال بالا به این معنا است که کلاس Button دارای یک Property با نام Text است.

عناصر UI خود به سه دسته تقسیم می‌شوند. Page - Layout - Control
Page‌ها که شامل Content Page - MasterDetail Page - Tabbed Page و ... می‌شوند، صفحات برنامه را تشکیل می‌دهند. Content Page، ساده‌ترین نوع صفحه‌است که کل صفحه نمایش را پر می‌کند و همان طور که از اسمش بر می‌آید، Content ای برای نمایش دارد.

Tabbed Page نیز چندین Tab را نمایش می‌دهد که هر ‌Tab خود یک Content Page است. Carousel Page نیز همانند Tabbed Page است، ولی با Swipe کردن به چپ و راست می‌شود بین صفحات چرخید. هر دوی اینها Multi Page محسوب می‌شوند. MasterDetail نیز این امکان را می‌دهد که از بغل منویی برای Swipe کردن وجود داشته باشد. در نهایت Navigation Page محتوای یک Content Page را نمایش می‌دهد، ولی در بالای آن Navigation Bar دارد؛ شامل دکمه بازگشت به صفحه قبل و Title صفحه جاری و ...


علاوه بر Page ها، Layout‌ها نیز وجود دارند. برای مثال، Stack Layout برای چینش خطی (افقی یا عمودی) استفاده می‌شود. Grid برای ساختار شبکه‌ای استفاده می‌شود و Flex Layout عملکردی مشابه با Flex در وب دارد.

برای مثال، در صورتی که بخواهید چهار دکمه را هم اندازه با هم نمایش دهید، دارید:

    <Grid>
        <Button Text="1" Grid.Row="0" Grid.Column="0" />
        <Button Text="2" Grid.Row="0" Grid.Column="1" />
        <Button Text="3" Grid.Row="1" Grid.Column="0" />
        <Button Text="4" Grid.Row="1" Grid.Column="1" />
    </Grid>


در نهایت کنترل‌ها را داریم. برای مثال Label، Button و ... هر کدام از اینها نقشی را ایفا می‌کنند و امکاناتی دارند.

پس Page داریم، داخل Page از Layout استفاده می‌کنیم برای چینش کلی صفحه و در نهایت از کنترل‌های Image، ListView، Button و ... استفاده می‌کنیم تا ظاهر فرم تکمیل شود.


هر Page علاوه بر ظاهر خود، دارای یک منطق نیز هست. منطق، کاری است که آن فرم انجام می‌دهد. برای مثال فرم لاگین می‌تواند یک Stack Layout عمودی باشد، شامل یک Entry برای گرفتن نام کاربری، یک Entry برای گرفتن رمز عبور، که IsPassword آن True است و در نهایت یک دکمه که برای انجام عمل لاگین است.

در قسمت منطق که با CSharp نوشته می‌شود، ما یک Property از جنس string برای نگه داشتن نام کاربری داریم. یک Property از جنس string برای نگه داشتن رمز عبور و یک Command که عمل لاگین را انجام دهد. Property اول با نام UserName به Text آن Entry اول وصل می‌شود (به اصطلاح Bind می‌شود) و همین طور Property دوم با نام Password نیز به Text آن Entry دوم که IsPassword اش True بود وصل می‌شود و در نهایت Command لاگین به دکمه لاگین وصل می‌شود.


برای زدن ظاهر فرم لاگین، در پروژه XamApp روی فولدر Views راست کلیک نموده و از منوی Add به New Item رفته و Content Page را می‌زنیم. نام آن را LoginView.xaml می‌گذاریم که داخل تگ Content Page خواهیم داشت:

    <StackLayout Orientation="Vertical">
        <Entry Placeholder="User name" Text="{Binding UserName}" />
        <Entry IsPassword="True" Placeholder="Password" Text="{Binding Password}" />
        <Button Command="{Binding LoginCommand}" Text="Login" />
    </StackLayout>

برای زدن منطق، در پروژه XamApp روی فولدر ViewModels راست کلیک نموده و از منوی Add گزینه Class را انتخاب کرده و نام آن را LoginViewModel.cs می‌گذاریم که در داخل آن خواهیم داشت:

public class LoginViewModel : BitViewModelBase
{
        public string UserName { get; set; }

        public string Password { get; set; }

        public BitDelegateCommand LoginCommand { get; set; }

        public LoginViewModel()
        {
            LoginCommand = new BitDelegateCommand(Login);
        }

        public async Task Login()
        {
            // Login implementation ...
        }
}

BitDelegateCommand در این مثال، وظیفه اجرای متد Login را به عهده دارد و آن را اجرا می‌کند؛ زمانیکه کاربر روی دکمه لاگین Click یا Tap کند.


برای این که Content Page جدید، یعنی LoginView به همراه منطق آن، یعنی LoginViewModel در برنامه نشان داده شوند، لازم است با Navigation به آن صفحه برویم. برای این کار، ابتدا باید این زوج را رجیستر کنیم. برای این کار به متد RegisterTypes در کلاس App رفته (زیر فایل App.xaml یک فایل App.xaml.cs است) و خط زیر را به آن اضافه می‌کنیم:

containerRegistry.RegisterForNav<LoginView, LoginViewModel>("Login");

حال در متد OnInitializedAsync در چند خط بالاتر داریم:

await NavigationService.NavigateAsync("/Login", animated: false);

این سطر باعث می‌شود که Navigation Service که همان طور که از اسمش بر می‌آید، کارش Navigation بین صفحات است، صفحه لاگین را باز کند.

هم اکنون پروژه XamApp بروز شده و دارای این مثال است. در صورتی که آن را الآن Clone کنید و یا در صورتی که از قبل گرفته بودید، دستور git pull را برای گرفتن آخرین تغییرات بزنید، می‌توانید این کدها رو داخل پروژه داشته باشید.

برنامه را اجرا کنید و در متد Login، یک Break point بگذارید. سپس برنامه را اجرا کنید. User Name و Password را پر کنید و بر روی دکمه لاگین بزنید. خواهید دید که متد لاگین اجرا می‌شود و User Name و Password با مقادیری که نوشته بودید، پر شده‌اند.

هنوز موارد زیادی برای آموزش باقی مانده، اما با این توضیحات می‌توانید در محیط توسعه‌ای که آماده کرده‌اید، فرم‌هایی ساده را پیاده سازی کنید و برایشان منطق‌هایی ساده را بنویسید و به برنامه بگویید که در ابتدای اجرا آن، صفحه را برای شما باز کند. در قسمت بعدی، به صورت عمیق‌تر وارد UI می‌شویم.

نظرات مطالب
معماری میکروسرویس‌ها
من در مورد همه مشکلات میکرو سرویس زیاد با شما موافق نیستم 
  • از آنجایی که ارتباط بین سرویس‌ها در بستر شبکه انجام می‌شود، انتظار کندی عملکرد سرویس‌ها دور از ذهن نیست. (اتفاقا بخاطر توزیع برنامه بر روی چند سیستم در زمانی که بار زیادی بر روی سیستم هست پاسخ گویی به کاربر می‌تونه خیلی بسرعت انجام بپذیره و اتفاقا یکی از مزایای اون هست)
  • به دلیل ارتباطات شبکه‌ای، احتمال آسیب پذیری‌های امنیتی در این نوع برنامه‌ها بیشتر است. (البته بیشتر این توزیع در server farm  انجام می‌شه ،یعنی پشت فایروال و کسی جز سرورها در این شبکه خصوصی وجود ندارد، نمی‌گم نیست ولی خیلی نیست)
  • نوشتن سرویس‌هایی که در بستر شبکه با سایر سرویس‌ها در ارتباط هستند سختی و مشکلات خود را دارد. برنامه‌نویس در این شرایط، درگیر برقراری ارتباط، رمزگذاری داده‌ها در صورت نیاز و تبدیل آنها می‌شود.(همان موارد بالا)
  • به دلیل مجزا بودن بخش‌های مختلف برنامه، مانیتور کردن و ردیابی عملکرد سرویس‌ها، یکی از کارهای اصلی توسعه دهنده یا استفاده کننده از برنامه است. (اینم خودش یک فایده است و طبق اصل SRP  و تفاوت MicroServic با SOA  بیشتر بر همین نکته تاکید داره که یک میکرو سرویس کاملا مستقل می‌باشد و راحت‌تر قابل مانیتور کردن و ردیابی عملکرد سرویس می‌باشد
  • در مجموع سرعت برنامه‌های نوشته شده با معماری Microservices کندتر از برنامه‌های نوشته شده با معماری Monolithic است. دلیل آن محیط اجرایی برنامه‌ها است. برنامه‌هایی با معماری Monolithic بر روی حافظه سرور پردازش می‌شوند. (باز تاکید که اصل استفاده از میکرو سرویس برای سیستم هایی با تراکنش بالا می‌باشد ،هدف توسعه راحتر و بدون تاثیر بر بقیه سرویس‌ها و حتی بدون توقف آنها می‌باشد، همچنین امکان  horizontal Scalability   نرم افزار و بالا بردن تعداد سرور‌های ارائه دهنده سرویس براحتی بوجود خواهد امد ، پس می‌تونه سرعت رو خیلی بالا ببره و مشکل توقف سرویس که در خیلی از سامانه‌های ایرانی می‌بینیم رو از بین میبره )
مطالب
طراحی و پیاده سازی زیرساختی برای تولید خودکار کد منحصر به فرد در زمان ثبت رکورد جدید

هدف از این مطلب، ارائه راه حلی برای تولید خودکار کد یا شماره یکتا و ترتیبی در زمان ثبت رکورد جدید به صورت یکپارچه با EF Core، می‌باشد. به عنوان مثال فرض کنید در زمان ثبت سفارش، نیاز است بر اساس یکسری تنظیمات، یک شماره منحصر به فرد برای آن سفارش، تولید شده و در فیلدی تحت عنوان Number قرار گیرد؛ یا به صورت کلی برای موجودیت‌هایی که نیاز به یک نوع شماره گذاری منحصر به فرد دارند، مانند: سفارش، طرف حساب و ... 


یک مثال واقعی

در زمان ثبت یک Task، کاربر می‌تواند به صورت دستی یک شماره منحصر به فرد را نیز وارد کند؛ در غیر این صورت سیستم به طور خودکار شماره‌ای را به رکورد در حال ثبت اختصاص خواهد داد. بررسی یکتایی این کد در صورت وارد کردن به صورت دستی، توسط اعتبارسنج مرتبط باید انجام گیرد؛ ولی در غیر این صورت، زیرساخت مورد نظر تضمین می‌کند که شماره یکتایی را ایجاد کند.

ایجاد یک قرارداد برای موجودیت‌های دارای شماره منحصر به فرد
public interface INumberedEntity
{
    string Number { get; set; }
}
با استفاده از این واسط می‌توان از تکرار یکسری از تنظیمات مانند تنظیم طول فیلد Number و همچنین ایجاد ایندکس منحصر به فرد برروی آن، به شکل زیر جلوگیری کرد.
foreach (var entityType in builder.Model.GetEntityTypes()
    .Where(e => typeof(INumberedEntity).IsAssignableFrom(e.ClrType)))
{
    builder.Entity(entityType.ClrType)
        .Property(nameof(INumberedEntity.Number)).IsRequired().HasMaxLength(50);

    if (typeof(IMultiTenantEntity).IsAssignableFrom(entityType.ClrType))
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number), nameof(IMultiTenantEntity.TenantId))
            .HasName(
                $"UIX_{entityType.ClrType.Name}_{nameof(IMultiTenantEntity.TenantId)}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
    else
    {
        builder.Entity(entityType.ClrType)
            .HasIndex(nameof(INumberedEntity.Number))
            .HasName($"UIX_{entityType.ClrType.Name}_{nameof(INumberedEntity.Number)}")
            .IsUnique();
    }
}

ایجاد یک Entity برای نگهداری شماره قابل استفاده بعدی مرتبط با موجودیت‌ها
public class NumberedEntity : Entity, IMultiTenantEntity
{
    public string EntityName { get; set; }
    public long NextNumber { get; set; }
    
    public long TenantId { get; set; }
}

با تنظیمات زیر:
public class NumberedEntityConfiguration : IEntityTypeConfiguration<NumberedEntity>
{
    public void Configure(EntityTypeBuilder<NumberedEntity> builder)
    {
        builder.Property(a => a.EntityName).HasMaxLength(256).IsRequired().IsUnicode(false);
        builder.HasIndex(a => a.EntityName).HasName("UIX_NumberedEntity_EntityName").IsUnique();
        builder.ToTable(nameof(NumberedEntity));
    }
}

شاید به نظر، استفاده از این موجودیت ضروریتی نداشته باشد و خیلی راحت می‌توان آخرین شماره ثبت شده‌ی در جدول مورد نظر را واکشی، مقداری را به آن اضافه و به عنوان شماره منحصر به فرد رکورد جدید استفاده کرد؛ با این رویکرد حداقل دو مشکل زیر را خواهیم داشت:

  • ایجاد Gap مابین شماره‌های تولید شده، که مدنظر ما نمی‌باشد. (با توجه به اینکه امکان ثبت دستی را هم داریم، ممکن است کاربر شماره‌ای را وارد کرده باشد که با آخرین شماره ثبت شده تعداد زیادی فاصله دارد که به خودی خود مشکل ساز نیست؛ ولی در زمان ثبت رکورد بعدی اگر به صورت خودکار ثبت شماره داشته باشد، قطعا آخرین شماره (بزرگترین) را که به صورت دستی وارد شده بود، از جدول دریافت خواهد کرد)


پیاده سازی یک PreInsertHook برای مقداردهی پراپرتی Number

internal class NumberingPreInsertHook : PreInsertHook<INumberedEntity>
{
    private readonly IUnitOfWork _uow;
    private readonly IOptions<NumberingConfiguration> _configuration;

    public NumberingPreInsertHook(IUnitOfWork uow, IOptions<NumberingConfiguration> configuration)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));
        _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
    }

    protected override void Hook(INumberedEntity entity, HookEntityMetadata metadata)
    {
        if (!entity.Number.IsNullOrEmpty()) return;

        bool retry;
        string nextNumber;
        
        do
        {
            nextNumber = GenerateNumber(entity);
            var exists = CheckDuplicateNumber(entity, nextNumber);
            retry = exists;
            
        } while (retry);
        
        entity.Number = nextNumber;
    }

    private bool CheckDuplicateNumber(INumberedEntity entity, string nextNumber)
    {
       //...
    }

    private string GenerateNumber(INumberedEntity entity)
    {
       //...
    }
}

ابتدا بررسی می‌شود اگر پراپرتی Number مقداردهی شده‌است، عملیات مقداردهی خودکار برروی آن انجام نگیرد. سپس با توجه به اینکه ممکن است به صورت دستی قبلا شماره‌ای مانند Task_1000 وارد شده باشد و NextNumber مرتبط هم مقدار 1000 را داشته باشد؛ در این صورت به هنگام ثبت رکورد بعدی، با توجه به Prefix تنظیم شده، دوباره به شماره Task_1000 خواهیم رسید که در این مورد خاص با استفاده از متد CheckDuplicateNumber این قضیه تشخیص داده شده و سعی مجددی برای تولید شماره جدید صورت می‌گیرد.


بررسی متد GenerateNumber

private string GenerateNumber(INumberedEntity entity)
{
    var option = _configuration.Value.NumberedEntityOptions[entity.GetType()];

    var entityName = $"{entity.GetType().FullName}";

    var lockKey = $"Tenant_{_uow.TenantId}_" + entityName;

    _uow.ObtainApplicationLevelDatabaseLock(lockKey);

    var nextNumber = option.Start.ToString();

    var numberedEntity = _uow.Set<NumberedEntity>().AsNoTracking().FirstOrDefault(a => a.EntityName == entityName);
    if (numberedEntity == null)
    {
        _uow.ExecuteSqlCommand(
            "INSERT INTO [dbo].[NumberedEntity]([EntityName], [NextNumber], [TenantId]) VALUES(@p0,@p1,@p2)", entityName,
            option.Start + option.IncrementBy, _uow.TenantId);
    }
    else
    {
        nextNumber = numberedEntity.NextNumber.ToString();
        _uow.ExecuteSqlCommand("UPDATE [dbo].[NumberedEntity] SET [NextNumber] = @p0 WHERE [Id] = @p1 ",
            numberedEntity.NextNumber + option.IncrementBy, numberedEntity.Id);
    }

    if (!string.IsNullOrEmpty(option.Prefix))
        nextNumber = option.Prefix + nextNumber;
    
    return nextNumber;
}

ابتدا با استفاده از متد الحاقی ObtainApplicationLevelDatabaseLock یک قفل منطقی را برروی یک منبع مجازی (lockKey) در سطح نرم افزار از طریق sp_getapplock ایجاد می‌کنیم. به این ترتیب بدون نیاز به درگیر شدن با مباحث isolation level بین تراکنش‌های همزمان یا سایر مباحث locking در سطح row یا table، به نتیجه مطلوب رسیده و تراکنش دوم که خواهان ثبت Task جدید می‌باشد، با توجه به اینکه INumberedEntity می‌باشد، لازم است پشت این global lock صبر کند و بعد از commit یا rollback شدن تراکنش جاری، به صورت خودکار قفل منبع مورد نظر باز خواهد شد.

پیاده سازی متد مذکور به شکل زیر می‌باشد:

public static void ObtainApplicationLevelDatabaseLock(this IUnitOfWork uow, string resource)
{
    uow.ExecuteSqlCommand(@"EXEC sp_getapplock @Resource={0}, @LockOwner={1}, 
                @LockMode={2} , @LockTimeout={3};", resource, "Transaction", "Exclusive", 15000);
}

با توجه به اینکه ممکن است درون تراکنش جاری چندین نمونه از موجودیت‌های INumberedEntity در حال ذخیره سازی باشند و از طرفی Hook ایجاد شده به ازای تک تک نمونه‌ها قرار است اجرا شود، ممکن است تصور این باشد که اجرای مجدد sp مذکور مشکل ساز شود و در واقع به Lock خود برخواهد خورد؛ ولی از آنجایی که پارامتر LockOwner با "Transaction" مقداردهی می‌شود، لذا فراخوانی مجدد این sp درون تراکنش جاری مشکل ساز نخواهد بود. 

گام بعدی، واکشی NextNumber مرتبط با موجودیت جاری می‌باشد؛ اگر در حال ثبت اولین رکورد هستیم، لذا numberedEntity مورد نظر مقدار null را خواهد داشت و لازم است شماره بعدی را برای موجودیت جاری ثبت کنیم. در غیر این صورت عملیات ویرایش با اضافه کردن IncrementBy به مقدار فعلی انجام می‌گیرد. در نهایت اگر Prefix ای تنظیم شده باشد نیز به ابتدای شماره تولیدی اضافه شده و بازگشت داده خواهد شد.

ساختار NumberingConfiguration

public class NumberingConfiguration
{
    public bool Enabled { get; set; }

    public IDictionary<Type, NumberedEntityOption> NumberedEntityOptions { get; } =
        new Dictionary<Type, NumberedEntityOption>();
}
public class NumberedEntityOption
{
    public string Prefix { get; set; }
    public int Start { get; set; } = 1;
    public int IncrementBy { get; set; } = 1;
}

با استفاده از دوکلاس بالا، امکان تنظیم الگوی تولید برای موجودیت‌ها را خواهیم داشت.

گام آخر: ثبت PreInsertHook توسعه داده شده و همچنین تنظیمات مرتبط با الگوی تولید شماره موجودیت‌ها

public static void AddNumbering(this IServiceCollection services,
    IDictionary<Type, NumberedEntityOption> options)
{
    services.Configure<NumberingConfiguration>(configuration =>
    {
        configuration.Enabled = true;
        configuration.NumberedEntityOptions.AddRange(options);
    });
    
    services.AddTransient<IPreActionHook, NumberingPreInsertHook>();
}

و استفاده از این متد الحاقی در Startup پروژه

services.AddNumbering(new Dictionary<Type, NumberedEntityOption>
{
    [typeof(Task)] = new NumberedEntityOption
    {
        Prefix = "T_",
        Start = 1000,
        IncrementBy = 5
    }
});

و موجودیت Task

public class Task : TrackableEntity, IAggregateRoot, INumberedEntity
{
    public const int MaxTitleLength = 256;
    public const int MaxDescriptionLength = 1024; 

    public string Title { get; set; }
    public string NormalizedTitle { get; set; }
    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo; 
    public byte[] RowVersion { get; set; }
    public string Number { get; set; }
}

با خروجی‌های زیر

پ.ن ۱: در برخی از Domain‌ها نیاز به ریست کردن این شماره‌ها براساس یکسری فیلد موجود در موجودیت مورد نظر نیز مطرح می‌باشد. به عنوان مثال در یک سیستم انبارداری شاید براساس FiscalYear و در یک سیستم فروش با توجه به نحوه فروش (SaleType)، لازم باشد این ریست برای شماره‌های موجودیت «سفارش»، انجام پذیرد. در کل با کمی تغییرات می‌توان از این روش مطرح شده در چنین حالاتی نیز به عنوان یک ابزار شماره گذاری خودکار کمک گرفت.
پ.ن ۲: استفاده از امکانات  Sequence در Sql Server هم شاید اولین راه حلی باشد که به ذهن می‌رسد؛ ولی از آنجایی که از تراکنش‌ها پشتیبانی ندارد، مسئله Gap بین شماره‌ها پابرجاست و همچنین آزادی عملی را به این شکل که در مطلب مطرح شد، نداریم.