Kynetic ORM: An ORM without configuration using C# 4.0 Dynamics, Generics and Reflection
Kynetic ORM: An ORM without configuration using C# 4.0 Dynamics, Generics and Reflection
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
Stored Procedure (که از این به بعد برای ایجاز، SP نوشته خواهد شد) هم از قاعده فوق مستثنی نیست و در صورت انتخاب صحیح میتواند به ارائهی محصول نهایی با کیفیتتری در زمان کوتاهتری کمک کند و در صورت انتخاب ناآگاهانه ممکن است باعث شکست یک پروژه (بخصوص در بلند مدت) شود.
تاریخچه
SQL توسط شرکت IBM در اوایل دهه 70 میلادی ایجاد شد. با اوج گرفتن زبانهای رویهای، SQL هم چندان از این قافله عقب نماند که منجر به پذیرش SP به عنوان یک استاندارد، در دهه 90 میلادی و پیاده سازی تدریجی آن توسط غولهای سازنده دیتابیس شد (رجوع فرمایید به ^ و ^). این فاصله 20 ساله باعث غنیتر شدن SQL شد و وجود SP - به معنی انتقال مدل برنامه نویسی رویهای به SQL - بخشی از مشکلات قبلی کار با کوئریهای پشت سر هم و خام را حل کرد. از سال 2000 میلادی به بعد، ORMهای قدرتمندی از جمله Hibernate و پیاده سازیهای مختلفی از Active Record و Entity Framework متولد شدند. بنابر این تقدم و تاخّرهای زمانی، بدیهی است اغلب مزایای SP نسبت به Raw SQL Query و اغلب معایب آن نسبت به ORMها باشد.
بنظر میرسد برای پاسخ به سوال اصلی این مطلب، ناگزیر به مقایسه SP با رقبای دیرینهاش هستیم. با برشمردن معایب و مزایای SP میتوان به نتیجهی منطقیتری رسید. البته باید در نظر داشت صرف استفاده از SP به معنای بهرهمند شدن از مزایای آن و صرف استفاده نکردن از آن هم بهرهمندی از رقبای آن نیست. چگونگی استفاده یک ابزار، مهمتر از خود ابزار است.
معایب SP
- دستورات Alter Table ، Add Column و Drop Column به این سادگیها هم نیستند؛ ممکن است به یکی از جداول دیتابیس دو ستون اضافه یا از آن حذف شوند. مجبوریم تمامی SPها را بخصوص Insert و Update متناظر با جدول را تغییر دهیم که این تغییرات ممکن است بصورت زنجیرهوار به سایر SPها هم سرایت کند. حال شرایطی را در نظر بگیرید که تعداد SPهای شما به چند ده و یا حتی به چند صد عدد و بیشتر، رسیده باشد که این به معنی زحمت بیشتر و تغییرات پر هزینهتر است.
- احتمال کند شدن ماشین سرویس دهنده در اثر اجرای تعداد
زیادی SP ؛ چناچه بخش زیادی از منطق برنامه از طریق SP اجرا شود، سرور دیتابیس موظف به اجرای آنهاست. اما در صورتیکه منطق،
در کد برنامه قرار داشته باشد، امکان توزیع آن بر روی سرورهای مجزا و یا حتی ماشین
کلاینت وجود خواهد داشت. امروزه اکثر کلاینتها به دیتابیسهای سبک و سریعی مجهز شدهاند. بنابراین در صورت امکان چرا بار پردازشی را به عهده آنها نگذاریم؟!
- یکپارچگی کمتر؛ تقریبا همه اپلیکیشنها نیازمند
ارتباط با سایر سیستمها هستند. اگر بخشهای زیادی از منطق برنامه درون SP مخفی شده باشند، این نقطه تلاقی بین سیستمی، احتمالا
درون خود دیتابیس قرار میگیرد و این به معنی ایجاد SP های بیشتر، افزودن
پارامترهای بیشتر، توسعه SPهای قبلی و بطور
خلاصه اعمال تغییرات بیشتر، که منتج به قابلیت نگهداری کمترخواهد شد.
- انعطاف پذیری کمتر؛ در یک شرایط ایده آل، عملکرد اپلیکیشن، مستقل از دیتابیس است. اگر نیاز به تغییر دیتابیس، مثلا از اوراکل به Microsoft SQL Server وجود داشته باشد، نیاز به بازنویسی و انتقال فانکشنها و SP ها محتمل است و از آنجائیکه که با وجود استانداردها، دیتابیسهای مختلف، معمولا در Syntax دستورات، تفاوتهای فاحشی دارند، هر چه کد بیشتری در SP ها باشد، نیاز به انتقال و تبدیل بیشتری وجود دارد.
- عدم وجود بازخورد مناسب؛ بسیاری از اوقات در صورت بروز اشکالی در حین اجرای یک SP، فقط با یک متن ساده بصورت Table has no rows و یا error مواجه میشویم. چنین خطاهایی هنگام دیباگ اصلا خوشایند نیستند. MS SQL در این بین بازخوردهای مناسبی را ارائه میکند. اگر تجربه کار با سایر دیتابیسها را داشته باشید، اهمیت بازخوردهای مناسب، ملموستر خواهد بود.
- کد نویسی سختتر؛ نوشتن کد SQL معمولا در همان IDE اپلیکیشن انجام نمیشود. جابجایی مداوم بین دو IDE ، دیباگ و کد نویسی از طریق دو اینترفیس مجزا، اصلا ایدهال نیست.
- SP منطق را بیش از حد پنهان میکند؛ حتی با دانستن نام صحیح یک SP، باز هم تصویری از پارامترهای ارسالی به آن و نتیجه برگشتی نخواهیم داشت. نمیدانیم نتیجه حاصل از اجرای SP ما مقداری را برمیگرداند یا خیر؟ در صورت وجود برگشتی، یک Cursor است یا یک مقدار؟ اگر Cursor است شامل چه ستونهایی است؟
- SP نمیتواند یک شیء را به عنوان آرگومان بپذیرد؛ بنابراین احتمال کثیف شدن کد به مرور افزایش پیدا میکند و بدتراز آن، در صورت ارسال اشتباه یک پارامتر، یا عدم تطابق تعداد پارامترها، مجبور به بررسی تمام آنها بصورت دستی هستیم. برای مثال دو قطعه کد زیر را با هم مقایسه کنید:
INSERT INTO User_Table(Id,Username,Password,FirstName,SureName,PhoneNumber,x,Email) VALUES (1,'VahidN','123456','Vahid','Nasiri','09120000000','vahid_xxx@example.com')
و معادل آن در یک ORM فرضی:
public void Insert(User user) { _users.Insert(user); db.Save(); }
بهوضوح قطعه کد sql، قبل از خوب یا بد بودن، زشت است. همچنین پارامتر x آن که فرضاً به تازگی اضافه شده، مقداری را دریافت نکرده و باعث بروز خطا خواهد شد.
- نبود Query Chaining؛ یکی از ویژگیهای جذاب ORMهای امروزی، امکان تشکیل یک کوئری با قابلیت خوانایی بالا و افزودن شرطهای بیشتر از طریق الگوی builder است. قطعه کد زیر یک SP برای جستجوی داینامیک نام و نام خانوادگی در یک جدول فرضی به اسم Users است:
public ICollection<User> GetUsers(string firstName,string lastName,Func<User, bool> orderBy) { var query = _users.where(u => u.LastName.StartsWith(lastName)); query = query.where(u => u.FirstName.StartsWith(firstName)); query = query.OrderBy(orderBy); return query.ToList(); }
در مقایسه با معادل SP آن:
CREATE PROCEDURE DynamicWhere @LastName varchar(50) = null, @FirstName varchar(50) = null, @Orderby varchar(50) = null AS BEGIN DECLARE @where nvarchar(max) SELECT @where = '1 = 1' IF @LastName IS NOT NULL SELECT @Where = @Where + " AND A.LastName LIKE @LastName + '%'" IF @FirstName IS NOT NULL SELECT @Where = @Where + " AND A.FirstName LIKE @FirstName + '%'" DECLARE @orderBySql nvarchar(max) SELECT @orderBySql = CASE WHEN @OrderBy = "LastName" THEN "A.LastName" ELSE @OrderBy = "FirstName" THEN "A.FirstName" END DECLARE @sql nvarchar(max) SELECT @sql = " SELECT A.Id , A.AccountNoId, A.LastName, A.FirstName, A.PostingDt, A.BillingAmount FROM Users WHERE " + @where + " ORDER BY " + @orderBySql exec sp_executesql @sql, N'@LastName varchar(50), @FirstName varchar(50) @LastName, @FirstName END
حاجت به گفتن نیست که قطعه کد اول چقدر خواناتر، انعطاف پذیرتر، خلاصهتر و قابل نگهداریتر است.
- نداشتن امکانات زبانهای مدرن؛ زبانها و IDEهای مدرن، امکانات قابل توجهی را برای نگهداری بهتر، انعطاف پذیری بیشتر، مقیاس پذیری بالاتر، تست پذیری دقیقتر و... ارائه میکنند. به عنوان مثال:
- زمان بر بودن نوشتن SP؛ گاهی نوشتن یک تابع در یک ORM یا بعضا نوشتن یک کوئری SQL کوتاه در یک رشته متنی، سادهتر از نوشتن کد SP است. آیا برای هر وظیفه کوچک در دیتابیس، نوشتن یک SP ضروری است؟
مزایای SP :
- کمتر کردن Round Trips در شبکه و متعاقبا کاهش ترافیک شبکه؛ اگر از یک فراخوانی استفاده کنیم، کاهش Round Tripها تاثیر چندانی نخواهد داشت. همچنین ارسال یک کوئری کامل، نسبت به ارسال فقط اسم SP و پارامترهای آن، پهنای باند بیشتری اِشغال میکند. البته در یک شبکه با سرعت قابل قبول، بعید است این دو مزیت محسوس باشند؛ اما به هر حال برای موارد خاص، دو مزیت محسوب میشوند. نکته دیگر آنکه بدلیل Pre-Compiled بودن SPها و همچنین کَش شدن Execution Plan آنها، اندکی با سرعت بالاتری اجرا میشوند.
- امکان چک کردن سینتکس قبل از اجرای آن؛ در مقایسه با Raw Query مزیت محسوب میشود.
- امکان به اشتراک گذاری کد؛ برای پروژههایی که چندین اپلیکیشن با چندین زبان برنامه نویسی مختلف در حال تهیه هستند و نیازمند دسترسی مستقیم به دادهها با سرعت به نسبت بالاتری هستند، SP میتواند یک راه حل ایده آل محسوب شود. بجای پیاده سازی منطق برنامه در هر اپلیکیشن بصورت جداگانه و زحمت کدنویسی هرکدام، میتوان از SP استفاده کرد. هرچند امروزه معمولا برای حل این مشکل، API های مشترک معماری Restful ارجحیت دارد.
- کمک به ایجاد یک پَک؛ در یک زیر سیستم با نیازمندی مشخص که اعمال تغییرات در آن محتمل نمیباشد نیز SP میتواند یک گزینه مناسب به حساب آید. مثلا یک سیستم Membership را در نظر بگیرید که در پروژههای مختلف شما مورد استفاده قرار خواهد گرفت. برای مثال میشود یک سیستم Membership سفارشی را با امکان Hash پسورد و رمز کردن دادههای حساس، به کمک SP و Function های مناسب فراهم کرد و در واقع بین Application Login و Data Logic تمایز قائل شد. شخصا معماری Restful را به این روش هم ترجیح میدهم.
- بهرمند شدن از امکانات بومی SQL ؛ به عنوان نمونه برای ترانهاده کردن خروجی یک کوئری میتوان از فانکشن Pivot استفاده کرد. یا فانکشنهای تحلیلی Lead و Lag (لینک مستندات اوراکل این دو فانکشن به ترتیب در ^ و ^ ) که بنظر نمیرسد هنوز معادل مستقیمی درORM ها داشته باشند.
- تسلط و کنترل بیشتر و دقیقتر بر کوئری نهایی؛ گفته میشود SP و عبارات SQL در دیتابیس، حکم assembly را در سایر زبانها دارند. بنابراین با SP میتوان عبارات SQL و نحوه اجرای آن را در دیتابیس، بطور کامل تحت فرمان داشت. این در حالی است که هر یک از ORMها دستورات زبان برنامه نویسی مبداء را به یک عبارت SQL ترجمه میکنند که این عبارت چندان تحت کنترل برنامه نویس نیست و بیشتر به مدل کاری ORM بستگی دارد.
- امکان join بین دو یا چند دیتابیس مجزا؛ حال آنکه امکان join بین دو Context در ORM ها وجود ندارد. بعلاوه اگر دو دیتابیس مدنظر ما روی دو سرور مجزا باشند، با SP و کانفیگ Linked Server کماکان میشود کوئری join دار نوشت.
- برای عملیاتهای Batch مناسبتر است؛ در مقام مقایسه با ORM ها که با تکنیکهای مختلفی سعی در افزایش سرعت عملیات Batch، بخصوص Insert و Update را دارند، SP با سرعت قابل قبولتری اجرا میشود.
- عدم نیاز به یادگیری سینتکس و ابزاری جدید؛ موارد
بسیاری وجود دارند که فرصت یادگیری تکنولوژی جدیدی مثل یک ORM و یا SQL Bulk و حتی کتابخانههای ثالث مبتنی بر این ابزارها وجود ندارند و ممکن است مجبور شوید برای باقی ماندن در بازار رقابتی، از
دانستههای قبلی خود استفاده کنید .
- تخصصیتر کردن وظایف؛ برنامه نویسهای دیتابیس به صورت تخصصی اقدام به تحلیل روابط و ایندکسها میکنند، دیتابیس را ایجاد و نرمال سازی مینمایند، SP های متناسب را میسازند و به بهترین شکل Optimize و در آخر تست میکنند.
- امنیت به نسبت بالاتر؛ میتوان مجوز اجرای SP را به یک کاربر اعطا کرد، بدون آنکه مجوز دسترسی به جداول مورد استفاده در آن SP را داد. همچنین نسبت به کوئریهای پارامتری نشده، SQL ارجیحت دارند چون احتمال آسیب پذیری در مقابل SQL Injection را کمتر میکنند.
نتیجهگیری
اگرچه SP ها برای پردازش دادهها آنقدر هم که در وبلاگها میخوانیم بد نیستند، اما سوء استفاده از آن، مشکلات عدیدهای را ایجاد خواهد کرد. با توجه به روند تغییرات تکنولوژیهای دسترسی به دادهها و معماریهای مدرن بنظر میرسد SP در بهترین حالت، ابزار مناسبی برای انجام عملیات CRUD است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
using NHibernate.Validator.Constraints;
namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
[NotNull]
public virtual int Balance { set; get; }
}
}
var list = new[] { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;
namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}
public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}
public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //یکبار در ابتدای اجرای برنامه باید ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */
نام پروژهی من AMD و فایل index.html بدان اضافه شده است. فرض کنید یک پوشهی جدید را به نام modules به آن اضافه میکنیم و در آن دو فایل typescript ی را به نامهای module1.ts و module2.ts، اضافه میکنیم.
محتویات module1 را اینگونه مینویسیم:
export module module1 { export abstract class firstCls { static f1(str: string) { console.log(str); } } }
و همچنین module2 به شکل زیر خواهد بود:
import * as Amd from 'module1'; module module2 { export class secondCls { f2(str: string) { Amd.module1.firstCls.f1(str); } } } new module2.secondCls().f2(`amd work's`);
(دقت کنید بعد از کامپایل شدن لفظ import تبدیل به define میشود)
از طریق add - new item فایل tsconfig.json را به مسیر اصلی پروژه اضافه کنید. در صورتی که آن را پیدا نکردید، به صورت دستی فایل آن را ساخته و محتویات زیر را در آن کپی نمایید:
{ "compilerOptions": { "noImplicitAny": false, "noEmitOnError": true, "removeComments": false, "sourceMap": false, "target": "es6", "module": "amd" }, "exclude": [ "node_modules" ] }
در ادامه به root پروژه رفته و دستور npm init را ارسال کرده تا فایل package.json تولید شود. همچنین برای requirejs نیز دستور زیر را ارسال مینماییم:
npm install requirejs --save-dev
حال requirejs به پروژهی شما اضافه شده است.
برای مدیریت کردن فراخوانی initial module در پوشهی modules که قبلا ساخته بودیم فایل main.js راساخته و کدهای زیر را بدان اضافه مینماییم.
(لازم به ذکر است این فایل را میتوانیم با استفاده از typescript نوشته و requirejs definitely typed را به پروژه اضافه کرده و از مزایای intellisense بودن بهره ببریم)
کدهای زیر را درون main.js مینویسیم:
require(['modules/module2.js'], modules_module2()); function modules_module2() { //additionals config goes here }
از آنجاییکه ممکن است تعداد وابستگی فایلها زیاد باشد و ترتیب load شدن آنها نیز اهمیت داشته باشد، در این قسمت میتوان configهای بیشتری را همچون sequence در load کردن فایلها، تعریف کرد که میتوانید در وبسایت رسمی requirejs آن را مطالعه بفرمایید.
حال فایل index.html را باز کرده و config برای فراخوانی requirejs, main.js را مینویسیم؛ به صورت زیر:
<h1>Hello requirejs and amd modules</h1> <!--src means require js address--> <!--data-main means initial require config--> <script src="node_modules/requirejs/require.js" data-main="modules/main.js"></script> <script></script>
پر واضح است که src آدرس فایل require.js و همچنین data-main آدرس initial require config پروژه را مشخص میکند.
اکنون پروژه را run کرده و میبینید که فایلهای مورد نیاز به صورت async برای ما load میشوند. اگر از مرورگر کروم استفاده مینمایید، بدین صورت میتوانید network و همچنین console را مشاهد نمایید:
مشاهد میکنید که فایلهای مورد نیاز load شدهاند و همچنین amd work's در console نمایش داده شده است.
requirejs بدین صورت عمل میکند: بعد از یافتن هر فایل، با استفاده از regex کل فایل را بررسی کرده و به دنبال وابستگیهای آن فایل میگردد (منظور همان importها میباشد و آن فایل به صورت async لود میشود) و در فایلهای بعدی نیز همین روال ادامه خواهد یافت. هر چند راهکارهایی برای بهبود کارآیی در آن اندیشیده شده است؛ بدین صورت که اساس کارش با استفاده از singleton میباشد و بعد از require کردن فایلی، هر دفعه که فراخوانی میشود، نیاز به پردازش مجدد ندارد. با این وجود ممکن است در بعضی مواقع و مخصوصا با اشتباهات سهوی برنامه نویسان از کارآیی نرم افزار مطبوع شما بکاهد.
کدهای این برنامه را میتوانید از اینجا دریافت نمایید (ضمن اینکه وابستگیهای اضافهتری مانند پوشهی node_modules حذف شدهاند؛ بنابراین npm install فراموش نشود)
دانلود AMD.zip
در قسمت بعد به امکانات توکار کامپایلر typescript برای معماری ماژولها میپردازیم
یک نکتهی تکمیلی: نحوهی نامگذاری ویژهی عناصر در فرمهای جدید Blazor SSR
اگر با نگارشهای دیگر Blazor کار کرده باشید، عموما یک EditForm را به صفحه اضافه کرده و چند المان را به آن اضافه میکنیم و ... کار میکند. حتی اگر کامپوننتهای سفارشی را هم بر این مبنا تهیه کنیم ... بازهم بدون نکتهی خاصی کار میکنند. اما ... در برنامههای Blazor SSR اینطور نیست! زمانیکه برای مثال مدل فرم را به این صورت تعریف میکنیم:
[SupplyParameterFromForm] public OrderPlace? MyModel { get; set; }
و آنرا به نحو متداولی در صفحه نمایش میدهیم:
<InputText @bind-Value="MyModel.City"/>
اگر به المان رندر شدهی در مرورگر مراجعه کنیم، ویژگی name حاصل، با MyModel.City مقدار دهی شدهاست و ... این موضوع درج نام خاصیت مدل (و یا اصطلاحا Html Field Prefix)، برای Blazor SSR بسیار مهم است! تاحدی که اگر از آن آگاه نباشید، ممکن است ساعتی را مشغول دیباگ برنامه شوید که چرا، مقدار نالی را دریافت کردهاید و یا عناصر تعریف شدهی در کامپوننتهای سفارشی، کار نمیکنند و مقدار نمیگیرند!
متاسفانه API بازگشت نام کامل عناصری که توسط Blazor SSR تولید میشود، عمومی نیست و internal است. اگر از کامپوننتهای استاندارد خود Blazor استفاده میکنید، نیازی نیست تا به این موضوع فکر کنید و مدیریت آن خودکار است؛ اما همینکه قصد تولید کامپوننتهای سفارشی مخصوص SSR را داشته باشید، اولین مشکلی را که با آن مواجه خواهید شد، دقیقا همین مسالهی تولید صحیح HtmlFieldPrefixها است.
برای رفع این مشکل و دسترسی به API پشت صحنهی تولید نام فیلدها در Blazor SSR، میتوان از کامپوننت پایهی InputBase خود Blazor ارثبری کرد و به این ترتیب به خاصیت جدید NameAttributeValue آن دسترسی یافت (این خاصیت به داتنت 8 و مخصوص Blazor SSR، اضافه شدهاست) که اینکار در کلاس BlazorHtmlField انجام شدهاست. روش استفادهی از آن هم به صورت زیر است:
private BlazorHtmlField<T?> ValueField => new(ValueExpression ?? throw new InvalidOperationException(message: "Please use @bind-Value here.")); [Parameter] public T? Value { set; get; } [Parameter] public EventCallback<T?> ValueChanged { get; set; } [Parameter] public Expression<Func<T?>> ValueExpression { get; set; } = default!;
زمانیکه میخواهیم در یک کامپوننت سفارشی، خاصیتی bind پذیر را طراحی کنیم، روش کار آن، مانند مثال فوق است که به همراه یک خاصیت، یک EventCallback و یک Expression است تا اعتبارسنجی و انقیاد دوطرفه را فعال کند. اما ... اگر همین Value را مستقیما در فیلدهای کامپوننت استفاده کنیم ... مقدار نمیگیرد؛ چون به همراه نام کامل خاصیت بایند شدهی به آن نیست. برای مثال بجای MyModel.City فقط City درج میشود (که به علت نداشتن .MyModel، سیستم binding از مقدار آن صرفنظر میکند). اکنون با استفاده از BlazorHtmlField فوق، میتوان به نام کامل تولیدی توسط Blazor SSR دسترسی یافت و از آن استفاده کرد:
<input type="text" dir="ltr" name="@ValueField.HtmlFieldName" id="@ValueField.HtmlFieldName" />
HtmlFieldName ای که در اینجا درج میشود، توسط خود Blazor محاسبه شده و با انتظارات موتور binding آن تطابق دارد و دیگر به خواص بایند شدهای که مقدار نمیگیرند، نخواهیم رسید.