اشتراکها
اشتراکها
ساخت برنامه های تحت وب با Blazor
Today we released our first public preview of Blazor , a new experimental .NET web framework using C#/Razor and HTML that runs in the browser with WebAssembly . Blazor enables full stack web development with the stability, consistency, and productivity of .NET.
تا اینجا اگر به کدهای کامپوننت فرم لاگینی که ایجاد کردیم دقت کنید، تبدیل شدهاست به محلی برای انباشت حجم قابل توجهی از کد. به این ترتیب اگر قرار باشد فرمهای جدیدی را تعریف کنیم، نیاز خواهد بود قسمتهای عمدهای از این کدها را در هر جایی تکرار کنیم. بنابراین جهت کاهش مسئولیتهای آن، نیاز است بازسازی کد (refactoring) قابل ملاحظهای بر روی آن صورت گیرد.
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
با این تغییر، به فرم login بازگشته و خاصیت account موجود در state آنرا به data تغییر نام میدهیم. برای اینکار بهتر است دکمهی F2 را بر روی نام انتخاب شدهی account در VSCode فشار دهید تا تکست باکس تغییر نام آن ظاهر شود. مزیت کار با این ابزار refactoring توکار، اصلاح خودکار تمام ارجاعات به account قبلی، با این نام جدید است. همچنین نام تمام خواصی و متغیرهایی را هم که به account تنظیم کرده بودیم، به data تغییر میدهیم تا کار به روز رسانی state بر روی data صورت گیرد و نه account قبلی. در این حالت شاید استفاده از امکانات replace کلی ادیتور، بهتر از استفاده از ویژگی F2 باشد.
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
سپس در متد رندر کامپوننت فرم لاگین، تنها کافی است بجای المان button قبلی، از متد فوق استفاده کنیم:
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
همانطور که مشاهده میکنید، با استفاده از [] و دسترسی پویای به خواص اشیاء، میتوان رندر المان Input را تبدیل به متدی با قابلیت نگهداری بهتر کرد و از تکرار ویژگیهای name ، label ، value ، onChange و error به ازای هر فیلد مورد نیاز، پرهیز کرد. اکنون با این تغییر، متد رندر کامپوننت فرم لاگین به صورت زیر خلاصه میشود که بسیار بهتر است از تعریف تعداد قابل ملاحظهای div و کلاس بوت استرپی، تعریف المانها، اتصال تک تک آنها به خواص تعریف شده، اتصال آنها به رویداد گردانها و همچنین به اعتبارسنجها:
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
نیازی به ذکر type، در اولین renderInput ذکر شده، نیست؛ چون مقدار این پارامتر را ازمقدار پیشفرض text، دریافت میکند.
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
اکنون اگر تغییرات را ذخیره کرده و به مرورگر مراجعه کنیم، فیلد کلمهی عبور، دیگر حروف وارد شده را نمایش نمیدهد و بر اساس نوع استاندارد password، عمل میکند.
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
در کامپوننت جاری، منهای پارامترهایی که نام ویژگیهای تعریف شده، با نام آن پارامترها در تمام قسمتهای کامپوننت (نه فقط المان input)، یکی نیستند (name، label و error)، مابقی را میتوان توسط یک «rest operator»، به این متد ارسال کرد:
بنابراین منهای name، label و error که در قسمتهای دیگر کامپوننت استفاده میشوند، مابقی پارامترهای این کامپوننت تابعی را حذف کرده و با یک rest operator، دریافت میکنیم. سپس آنها را به کمک یک spread operator، در المان input، گسترده و درج میکنیم. شبیه به اینکار را در قسمت 15 و بخش «ارسال props سفارشی در حین مسیریابی به کامپوننتها» آن انجام داده بودیم. با کمک عملگرهای rest و spread، به سادگی میتوان هرنوع ویژگی جدیدی را که برای کار با المان input نیاز داریم، به کامپوننت جاری ارسال کرد؛ بدون اینکه نیازی باشد هربار تعریف پارامترهای آن را تغییر دهیم. پارامتر rest تعریف شده، یعنی هر خاصیت دیگری را بجز سه خاصیت name، label و error، به صورت خودکار به این کامپوننت تابعی ارسال کن.
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
تشخیص قسمتهایی که قابلیت استخراج از کامپوننت لاگین را دارند
قصد داریم قسمتهایی از کامپوننت لاگین فعلی را استخراج کرده و آنها را درون یک کامپوننت با قابلیت استفادهی مجدد قرار دهیم:
- خاصیت state: میخواهیم تمام فرمهایی را که تعریف میکنیم، دارای خاصیت errors باشند. بنابراین این خاصیت قابلیت استفادهی مجدد را دارد.
- خاصیت schema: قابلیت استفادهی مجدد را ندارد و مختص فرم لاگین تعریف شدهاست. این منطق از هر فرمی با فرم دیگر، متفاوت است.
- متد validate: در این متد، هیچ نوع وابستگی از آن به مفهوم لاگین وجود ندارد و کاملا قابلیت استفادهی مجدد را دارد. تنها this.state.account آن وابستهی به کامپوننت لاگین است و بدیهی است شیء account را در سایر فرمها نخواهیم داشت و ممکن است نام آن movie یا customer باشد. بنابراین قاعدهای را در اینجا تعریف میکنیم، بر این مبنا که از این پس، تمام فرمهای ما دارای خاصیتی به نام data خواهند بود که بیانگر اطلاعات آن فرم میباشد. با این تغییر، برای مثال در فرم لاگین، data به شیء account تنظیم میشود و در فرمی دیگر به شیء customer.
- متد validateProperty: همانند متد validate است و کاملا قابلیت استفادهی مجدد را دارد.
- متد handleSubmit: قسمت ابتدایی این متد که شامل غیرفعال کردن post back به سرور و اعتبارسنجی فرم است، قابلیت استفادهی مجدد را دارد. اما قسمت دوم آن مانند ارسال فرم به سرور و یا هر عملیات دیگری، از یک فرم به فرم دیگر میتواند متفاوت باشد.
- متد handleChange: این متد نیز قابلیت استفادهی مجدد را دارد؛ چون میخواهیم در تمام فرمها در حین تایپ اطلاعات، کار اعتبارسنجی ورودیها صورت گیرد. این متد نیز به this.state.account وابستهاست که قاعدهی تعریف خاصیت data در state، میتواند این مشکل را حل کند.
- متد رندر: طراحی آن کاملا وابستهاست به نوع فرمی که مدنظر میباشد؛ اما دکمهی submit آن خیر. بجز برچسب دکمهی submit، مابقی قسمتهای آن مانند کلاسهای CSS و منطق فعالسازی و غیرفعالسازی آن، قابلیت استفادهی مجدد را دارند.
بنابراین در ادامه کار، refactoring کامپوننت فرم لاگین را برای استخراج قسمتهای با قابلیت استفادهی مجدد آن، انجام خواهیم داد.
تبدیل قسمتهای با قابلیت استفادهی مجدد کامپوننت لاگین، به یک کامپوننت عمومی
ابتدا کامپوننت عمومی Form را که قابلیت استفادهی مجدد دارد، در فایل جدید src\components\common\form.jsx تعریف کرده و سپس کامپوننت فرم لاگین را طوری تغییر میدهیم که از آن، بجای کلاس پیشفرض Component، ارث بری کند. به این ترتیب تمام متدهای تعریف شدهی در این کامپوننت با قابلیت استفادهی مجدد، در کامپوننتهای مشتق شدهی از آن، در دسترس خواهند بود.
1- در ادامه همانطور که عنوان شد، خاصیت state فرمها باید دارای شیء data و شیء errors باشند تا توسط آنها بتوان اطلاعات کل فرم و اطلاعات خطاهای اعتبارسنجی را ذخیره کرد:
import React, { Component } from "react"; class Form extends Component { state = { data:{}, errors:{} }
2- در ادامه، کاری با خاصیت schema تعریف شدهی در کامپوننت لاگین نداریم؛ چون کاملا مختص به آن است. اما متدهای validate و validateProperty آنرا طور کامل cut کرده و به کامپوننت Form، منتقل میکنیم. با این انتقال، چون این متدها از کتابخانهی Joi استفاده میکنند، باید import آنرا نیز به ابتدای ماژول جدید فرم، اضافه کرد:
import Joi from "@hapi/joi";
3- سپس متد رندر کامپوننت Form را کاملا حذف میکنیم؛ چون این کامپوننت قرار نیست چیزی را رندر کند.
4- در قسمت دوم متد handleSubmit، برای مثال قرار است ارسال دادهها به سرور صورت گیرد. به همین جهت آنرا تبدیل به متدی مانند doSubmit کرده و سپس کل متد handleSubmit را نیز به کامپوننت Form منتقل میکنیم.
doSubmit = () => { // call the server console.log("Submitted!"); };
5- متد handleChange را نیز از کامپوننت فرم لاگین cut کرده و به کامپوننت Form منتقل میکنیم.
6- پس از این نقل و انتقالات، کار ارث بری از کامپوننت فرم را در کامپوننت فرم لاگین انجام میدهیم:
import Form from "./common/form"; // ... class LoginForm extends Form {
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، همانند قبل و آنچیزی که در انتهای قسمت قبلی به آن رسیدیم، بدون مشکل کار میکند؛ اما کدهای کامپوننت فرم لاگین به شدت کاهش یافته و ساده شدهاست. همچنین اگر دفعهی بعد، نیاز به ایجاد فرمی وجود داشت، دیگر نیازی به تکرار این حجم از کد نیست. تنها نیاز خواهیم داشت تا state را تعریف کرده و schema را اضافه کنیم و همچنین نیاز است متد doSumbit را پیاده سازی کنیم تا مشخص شود پس از تکمیل فرم و اعتبارسنجی آن، قرار است چه رخدادی واقع شود.
کدهای کامل کامپوننت فرم را از پیوست انتهای بحث میتوانید دریافت کنید؛ البته تمام متدهای آنرا در قسمت قبل تکمیل کرده بودیم و در اینجا صرفا یکسری cut/paste صورت گرفتند.
ساده کردن و بهبود پیاده سازی متد رندر
1- در متد رندر فعلی کامپوننت فرم لاگین، اگر به دکمهی submit آن دقت کنیم، بجز برچسب آن، مابقی قسمتهای آن در تمام فرمهای دیگری که تعریف خواهیم کرد، یکسان خواهند بود. به همین جهت این قسمت را میتوان تبدیل به یک متد کمکی در کلاس Form کرد:
renderButton(label) { return ( <button disabled={this.validate()} className="btn btn-primary"> {label} </button> ); }
{this.renderButton("Login")}
2- در قسمتهای قبل، برچسب، فیلدهای ورودی و تگها و کلاسهای بوت استرپی را به کامپوننت Input منتقل کردیم، تا به یک فرم سادهتر و با قابلیت نگهداری بالاتری برسیم. هرچند این هدف حاصل شده، اما باز هم تعاریف المانهای Input قرارگرفتهی در متد رندر کامپوننت لاگین، دارای الگوی تکراری ذکر یک خاصیت مشخص، تعریف رویدادگردانهای مشخص و اطلاعات اعتبارسنجی کاملا مشخصی هستند. به همین جهت تعریف المان Input را هم مانند متد renderButton فوق میتوان به کلاس پایه Form انتقال داد:
import Input from "./input"; //... renderInput(name, label) { const { data, errors } = this.state; return ( <Input name={name} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> );
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password")} {this.renderButton("Login")} </form> ); }
3- تا اینجا فرم لاگین تعریف شده، یک مشکل کوچک را دارد: فیلد پسورد آن، از نوع text تعریف شده و اطلاعات وارد شده را همانند یک textbox معمولی نمایش میدهد. برای رفع این مشکل، پارامتر type را با یک مقدار پیشفرض پر استفاده، تعریف کرده و به المان Input اعمال میکنیم:
renderInput(name, label, type = "text") { const { data, errors } = this.state; return ( <Input name={name} type={type} label={label} value={data[name]} onChange={this.handleChange} error={errors[name]} /> ); }
سپس این type را در قسمتی که المان مرتبط را رندر میکنیم، با password مقدار دهی خواهیم کرد:
render() { return ( <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderButton("Login")} </form> ); }
البته این تغییرات تا به اینجا کار نخواهند کرد؛ چون هنوز کلاس المان Input را جهت پذیرش ویژگی جدید type، ویرایش نکردهایم. بنابراین به فایل src\components\common\input.jsx مراجعه کرده و type را به آن اعمال میکنیم:
import React from "react"; const Input = ({ name, type, label, value, error, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type={type} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
4- مشکل! آیا باید به ازای هر ویژگی جدیدی که قرار است به این input اعمال کنیم، مانند type در اینجا، نیاز است یک پارامتر جدید را تعریف و سپس از آن استفاده کرد؟ در این حالت اینترفیس این کامپوننت از کنترل خارج میشود و همچنین هربار باید آنرا ویرایش کرد و تغییر داد. به علاوه اگر به تعریف این input دقت کنیم، نام 4 ویژگی آن، با مقادیری که دریافت میکنند، هم نام هستند (ویژگی value با مقدار value و ...):
<input value={value} name={name} type={type} onChange={onChange} id={name} className="form-control" />
import React from "react"; const Input = ({ name, label, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input {...rest} name={name} id={name} className="form-control" /> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Input;
با این تغییر در کامپوننت Input، سایر قسمتهای برنامه نیازی به تغییر ندارند. برای مثال در متد renderInput، سه ویژگی name، label و error تبدیل به سه پارامتر دریافتی از props میشوند (ترتیب ذکر آنها اهمیتی ندارد). مابقی ویژگیهای تعریف شدهی در آن، به صورت خودکار در قسمت input {...rest} درج خواهند شد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-20.zip
امکان تعریف پارامترهای اجباری در Blazor 6x
ذکر پارامترهای Blazor اختیاری هستند و در صورت عدم تعریف آنها، از مقدار پیشفرض این پارامترها استفاده میشود. اگر میخواهید تعریف پارامتری را اجباری کنید، اکنون در Blazor 6x میتوان به صورت زیر عمل کرد:
[Parameter, EditorRequired] public string Title { get; set; }
باید دقت داشت که این ویژگی در زمان اجرای برنامه بررسی نشده و همچنین تعریف آن به معنای بررسی null بودن مقادیر نیست.
نظرات مطالب
ASP.NET MVC #11
در زمانی که خصوصیت از نوع DateTime بصورت Nullable تعریف میشود در فرهنگهای مختلف ارزش ذخیره شده برای حالت null متفاوت است ، با بررسی مثال MvcAppPersianDatePicker معرفی شده توسط شما هم این مورد رو دیدم ، در چنین مواقعی برای کنترل null بودن محتوای خصوصیتها چه باید کرد ؟ مثلا برای فرهنگ فارسی مقدار null برابر است با (11/10/1278 ) ، من از مکانیزم معرفی شده در پروژه MvcAppPersianDatePicker استفاده میکنم .در زمان بررسی وضعیت null دچار مشکل هستم ، لطفا راهنمائی بفرمائید.
- «... تاریخ خالی وارد میشه ...»
اگر قرار هست تاریخی خالی وارد شود، باید آنرا nullable تعریف کنید (DateTime? ContractStartDate ) چون DateTime یک value types است و نه یک reference type.
- اگر قرار هست Required داشته باشد (مانند مثال شما)، که همان سمت کلاینت از این مساله جلوگیری میشود و کار به ارسال به سمت سرور نمیرسد؛ چون new RouteValueDictionary(validationAttributes) کار درج ویژگیهای اعتبارسنجی را انجام میدهد. البته به شرطی که UIHint را ذکر کرده باشید.
اگر قرار هست تاریخی خالی وارد شود، باید آنرا nullable تعریف کنید (DateTime? ContractStartDate ) چون DateTime یک value types است و نه یک reference type.
- اگر قرار هست Required داشته باشد (مانند مثال شما)، که همان سمت کلاینت از این مساله جلوگیری میشود و کار به ارسال به سمت سرور نمیرسد؛ چون new RouteValueDictionary(validationAttributes) کار درج ویژگیهای اعتبارسنجی را انجام میدهد. البته به شرطی که UIHint را ذکر کرده باشید.
با فرض فعال سازی و ثبت PersianDateModelBinder، این خطا زمانی حاصل میشود که یک فیلد datetime مقدار دهی نشده را بخواهید در بانک اطلاعاتی ذخیره کنید. نوع datetime در دات نت value type است و مقدار پیش فرض آن 0001-01-01 است (DateTime.MinValue) که قابل ذخیره سازی در بانک اطلاعاتی نیست. یا فیلد را nullable تعریف کنید (هم در سمت کدها و هم در سمت بانک اطلاعاتی) و یا حتما هنگام ذخیره سازی اطلاعات، آنرا مقدار دهی کنید تا مقدار پیش فرض خود را نداشته باشد.
نظرات مطالب
متدهای کمکی مفید در پروژه های asp.net mvc
چه خطایی میده؟ چون من الان استفاده کردم و خطایی نداد.
اگر یک چنین خطایی دریافت کردید:
مربوط است به نال بودن یکی از مقادیر تاریخ ثبت شده در بانک اطلاعاتی. خاصیت را در سمت کدهای خود nullable تعریف کنید.
اگر یک چنین خطایی دریافت کردید:
The conversion of a varchar data type to a datetime data type resulted in an out-of-range value.
خودکارسازی، در قسمتهای مختلف یک پروژه میتواند انجام شود. نمونههای مختلف این خودکارسازیها که اکثرا توسط رفلکشن انجام میشوند شامل نگاشت خودکار Dto به Entity و بالعکس (توسط AutoMapper)، ثبت خودکار تمام Entityها در DbContext بدون نیاز به ثبت تک تک آنها به صورت public DbSet<Person> People { get; set; } (که در این روش خودکار، اسم جداول میتواند به صورت جمع ثبت شود)، ثبت خودکار EntityTypeConfigurationها، ثبت خودکار کلیهی کلاسهای Profile برای کانفیگ AutoMapper و رجیستر خودکار DI سرویسها، تا نیازی به نوشتن کدهای تکراری و مشابه ;()<services.AddTransient<IUserService, UserService نداشته باشیم.
برای مشاهدهی عملی پیادهسازی این نمونهها میتوانید به پروژهی ASP.NET Core WebAPI مراجعه کنید. در این مقاله میخواهیم همین سناریو را برای ثبت سرویسهایمان در متد ConfigureServices انجام دهیم، تا نیازی به نوشتن هیچ کدی برای آنها نداشته باشیم.
و سپس در متد ConfigureServices میتوان آن را به صورت زیر استفاده کرد:
ولی اگه پروژهی ما متوسط به بالا باشد، کمکم تعداد سرویسهای ما زیاد میشود (برای مثال چند نمونه از سرویسهای رایج مورد استفاده، شامل سرویسهای لاگ خطاها مثل Elmah و سرویس HttpClientFactory و AutoRegisterDi (توضیح در ادامه مقاله) و AutoMapper و Cache و EFSecondLevelCache و Hangfire و ....) میبینیم که تعداد این سرویسها هم زیاد است و حتی به صورت اکستنشن هم به مرور زمان باعث شلوغ شدن استارتاپ میشوند. ضمن اینکه یک کار تکراری است که باید هر بار انجام شود.
کار تمام شد. حالا تمام سرویسهای ما با ایجاد کلاس مرتبط و implement شدن از اینترفیس IServiceInstaller، به طور خودکار در استارتاپ و متد ConfigureServies ثبت خواهند شد.
ثبت سرویسهای مختلف، به همراه تنظیمات آنها (مانند Authentication، Swagger، DbContext، ApiVersioning و ...) در استارتاپ میتواند به چندین صورت انجام شود.
روش اول اینکه به صورت دستی تمام کدهای مربوط به رجیستر کردن سرویسها و تنظیمات آنها، در متد ConfigureServices نوشته شود که خیلی جالب نیست و موجب شلوغ شدن سریع این متد میشود. نمونهی این شیوه را برای ثبت سرویس مربوط به DbContext میبینیم:
راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم میکند. برای مثال ثبت سرویس بالا را میتوانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public void ConfigureServices(IServiceCollection services) { // DbContext Service services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); // register other services .... }
راه دوم روش استفاده از متدهای الحاقی است؛ طوریکه برای هر سرویس، یک متد الحاقی را تعریف کنیم و از آن، در این متد استفاده کنیم که حجم کدها را تا حد زیادی کم میکند. برای مثال ثبت سرویس بالا را میتوانیم در کلاس دیگری با نام DbContextServiceCollectionExtensions.cs ثبت کنیم:
public static class DbContextServiceCollectionExtensions { public static void AddDbContext(this IServiceCollection services) { services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); } }
public void ConfigureServices(IServiceCollection services) { // Add DbContext services.AddDbContext(); //.... Register other services }
راه سوم ثبت سرویس، استفاده از یک اینترفیس به نام IServiceInstaller و استفاده از آن در کلاسهای مختلف مربوط به ثبت سرویس و بعد خواندن خودکار این تنظیمات با یک خط کد سادهی رفلکشن است که در ادامه میبینیم:
اینترفیس IServiceInstaller را تعریف میکنیم:
توضیح: پارامتر appSettings کلاسی شامل کلیهی مقادیر فایل appsettings.json است. شما میتوانید بجای آن از IConfiguration استفاده کنید و مقدار آن را در Startup پاس دهید. پارامتر آخر برای حالتی است که این فایلها را در لایهی دیگری به غیر از لایهی اصلی Api (مثل لایهی WebFamewrk) پیاده سازی میکنید.
public interface IServiceInstaller { void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly); }
سپس کلاسهای ثبت سرویسهایمان را با ارث بری از این اینترفیس میسازیم. برای نمونه رجیستر DbContext را با ایجاد کلاسی به نام DbContextInstaller انجام میدهیم:
public class DbContextInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { services.AddDbContext<AppDbContext>(options => { options .UseSqlServer(appSettings.ConnectionStrings.MyConnectionString, sqlServerOptionsBuilder => { sqlServerOptionsBuilder.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds); //Default is 30 seconds sqlServerOptionsBuilder.EnableRetryOnFailure(); sqlServerOptionsBuilder.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName); }) //Tips .ConfigureWarnings(warning => warning.Throw(RelationalEventId .QueryPossibleExceptionWithAggregateOperatorWarning)); // Activate EF Second Level Cache options.AddInterceptors(new SecondLevelCacheInterceptor()); }); } }
حالا برای ثبت این کلاس و کلاسهای مشابه Installer، میآییم یک متد الحاقی را برای متد ConfigureServices مینویسیم که در آن از رفلکشن استفاده میکنیم:
public static class ServiceInstallerExtensions { public static void InstallServicesInAssemblies(this IServiceCollection services, AppSettings appSettings) { var startupProjectAssembly = Assembly.GetCallingAssembly(); var assemblies = new[] { startupProjectAssembly, Assembly.GetExecutingAssembly() }; var installers = assemblies.SelectMany(a => a.GetExportedTypes()) .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(IServiceInstaller).IsAssignableFrom(c)) .Select(Activator.CreateInstance).Cast<IServiceInstaller>().ToList(); installers.ForEach(i => i.InstallServices(services, appSettings, startupProjectAssembly)); } }
در نهایت متد ConfigureServices ما به صورت زیر خواهد بود (بعد از اضافه کردن تمام سرویسها!):
public void ConfigureServices(IServiceCollection services) { //* HttpContextAccessor // services.AddHttpContextAccessor(); //* Controllers services.AddControllers(options => { options.Filters.Add(new AuthorizeFilter()); }) .AddNewtonsoftJson(); //* Installers services.InstallServicesInAssemblies(_appSettings); }
فقط یک نکته آخر اینکه برای رجیستر خودکار DI سرویسها (و ننوشتن کدهایی مانند ()<services.AddTransient<IUserService, UserService برای رجیستر هر سرویس) میتوانیم از Autofac استفاده کنیم (در پروژهی بالا آمده است) و یا از پکیج AutoRegisterDi استفاده کنیم (متعلق به Jon P Smith) که از خود Container داخلی Core استفاده میکند و از Autofac سبکتر است. کلاسی میسازیم به نام RegisterServicesUsingAutoRegisterDiInstaller:
public class RegisterServicesUsingAutoRegisterDiInstaller : IServiceInstaller { public void InstallServices(IServiceCollection services, AppSettings appSettings, Assembly startupProjectAssembly) { var dataAssembly = typeof(SomeRepository).Assembly; var serviceAssembly = typeof(SomeService).Assembly; var webFrameworkAssembly = Assembly.GetExecutingAssembly(); var startupAssembly = startupProjectAssembly; var assembliesToScan = new[] { dataAssembly, serviceAssembly, webFrameworkAssembly, startupAssembly }; #region Generic Type Dependencies services.AddScoped(typeof(IRepository<>), typeof(Repository<>)); #endregion #region Scoped Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(IScopedDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Scoped); #endregion #region Singleton Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Singleton); #endregion #region Transient Dependency Interface services.RegisterAssemblyPublicNonGenericClasses(assembliesToScan) .Where(c => c.GetInterfaces().Contains(typeof(ITransientDependency))) .AsPublicImplementedInterfaces(); // Default is Transient #endregion #region Register DIs By Name services.RegisterAssemblyPublicNonGenericClasses(dataAssembly) .Where(c => c.Name.EndsWith("Repository") && !c.GetInterfaces().Contains(typeof(ITransientDependency)) && !c.GetInterfaces().Contains(typeof(IScopedDependency)) && !c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(ServiceLifetime.Scoped); services.RegisterAssemblyPublicNonGenericClasses(serviceAssembly) .Where(c => c.Name.EndsWith("Service") && !c.GetInterfaces().Contains(typeof(ITransientDependency)) && !c.GetInterfaces().Contains(typeof(IScopedDependency)) && !c.GetInterfaces().Contains(typeof(ISingletonDependency))) .AsPublicImplementedInterfaces(); #endregion } }
(رجیستر در اینجا با اولویت اینترفیسهای ITransiantDependency، IScopedDependency، ISingletonDependency و سپس اتمام نام سرویس با کلمههای Repository و Service انجام میشود که شما میتوانید با منطق و نیاز خودتان آنها را تغییر دهید)