اشتراکها
مطالب
Microbenchmark
What Is Micro Benchmark? Micro benchmark is a benchmark designed to measure the performance of a very small and specific piece of code. (^)
البته این موضوع امروزه بیشتر در Java مطرحه تا دات نت (^ و ^ و ^) اما مفاهیم اصلی مختص یک زبان یا پلتفرم نیست.
وقتی در مورد آزمایش بار برای مقایسه کارایی کلاس StrigBuilder تحقیق میکردم به مطلب جالبی برخورد کردم. خلاصش این میشه که برای تست بار قسمتهایی از کدتون میتونین زمان موردنیاز برای اجرای اون کد رو بررسی کنین و چون ممکنه انجام این کار چندین بار نیاز بشه بهتره از متد زیر برای اینکار استفاده کنین:
static void Profile(string description, int iterations, Action func) { // clean up GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); // warm up func(); var watch = Stopwatch.StartNew(); for (int i = 0; i < iterations; i++) { func(); } watch.Stop(); Console.Write(description); Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds); }
سه خط اول متد بالا برای آمادهسازی حافظه جهت اجرای تست موردنظر است. برای آشنایی بیشتر با نحوه عملکرد Garbage Collector (^ و ^ و ^) خوندن کتاب فوق العاده CLR via C# - 3rd ed رو پیشنهاد میکنم (فصلهای 21 و 22).
سپس یکبار اکشن موردنظر (که حاوی قطعه کد موردنظره) اجرا میشه تا مسائل مربوطه به بارگذاریهای اولیه در نتیجه تست تاثیر نزاره (warm up).
در نهایت هم آزمایش بار برای تعداد تکرار درخواست شده انجام میشه و زمان اجرای اون در خروجی چاپ میشه.
برای استفاده از متد فوق میشه از کد زیر استفاده کرد:
Profile("a descriptions", how_many_iterations_to_run, () => { // ... code being profiled });
و برای استفاده از این متد در آزمایش کارایی کلاس StringBuilder میشه از کدی شبیه به کد زیر استفاده کزد:
var iterations = 10000000; var testString = ".NET Tips is awesme!"; do { var sb1 = new StringBuilder(testString); var sb2 = new StringBuilder(testString) { Capacity = testString.Length * iterations }; try { Profiler.Profile("StringBuilder Profiler", iterations, () => sb1.Append(testString)); Profiler.Profile("StringBuilder Capacity Profiler", iterations, () => sb2.Append(testString)); } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("----------------------------------------------------------------"); sb1.Clear(); sb2.Clear(); } } while (Console.ReadKey(true).Key == ConsoleKey.C); // C = continue
البته برای اینکه عملیات مقدار دهی خاصیت Capacity در قسمت warm up متد profile نتایج رو تحت تاثیر قرار نده برای این تست من اون قسمت رو کامنت کردم (اگر این کار رو نکنین زمانهای بدست اومده برای هر دو مورد یکی خواهد بود). اجرای کد بالا نتایج زیر رو تو سیستم من ارائه داد:
میبینین که نتایج استفاده از متد موردبحث کمی فرق داره و افزایش کارایی در حالت استفاده از پراپرتی Capacity دیگه حدود 3 برابر نیست و حدود 2 دو برابره. البته زمان بدست اومده برای هر دو مورد نسبت به قبل کاهش داشته که بیشترش میتونه مربوطه به عدم درنظر گرفتن زمان موردنیاز برای ایجاد کلاس StringBuilder در این تست جدید باشه (چون بعید میدونم عملیات پاکسازی حافظه توسط GC تو این تست تاثیر چندانی داشته باشه). درهر حال نتایج این تست بیشتر به واقعیت نزدیکه!
مطالب
آشنایی اولیه با gRPC
در مقالهی قبلی بطور کلی با Protocol Buffers آشنا شدیم. در این قسمت با gRPC آشنا شده و همچنین به پیاده سازی یک سرور و کلاینت، با استفاده از gRPC پرداخته که توسط آن به تبادل اطلاعات با یکدیگر میپردازند.
مدل و سرویسها بصورت واضحی نوشته شدهاند؛ SayHello با ورودی HelloRequest و خروجی HelloReply تعیین شدهاست.
از طریق Grpc.Tools میتوانیم protobufهای خود را بصورت خودکار بعد از build تولید نماییم. در csproj آیتم زیر را اضافه کرده و آدرس protobuf را تعیین مینماییم.
حال کافی است کدهای زیر را جایگزین نماییم:
همانطور که مشاهده میکنید، مدلها و سریسها بصورت خودکار تولید شدهاند (ضمن اینکه میتوانستیم بصورت دستی نیز protobuf را برای زبان دلخواه خود تولید نماییم).
فرض کنید فایل generate شده در پوشهی proto قرار گرفته به نام "helloworld.pb.go"
gRPC یک فریم ورک مدرن و متن باز با کارآیی بالاست. توسط گوگل پیاده سازی شده و جزء انجمن CNCF میباشد (مثل Docker & Kubernetes) که بر روی سیستم عاملهای متعددی اجرا میشود. به صورت خیلی کارا میتواند سرویسهای متعددی را به یکدیگر متصل کرده و همچنین از امکاناتی همچون load balancing, monitoring, tracing, health checking, authentication به صورت خیلی ساده پشتیبانی میکند. بسایر سریع و همچنین Low Latency است. مستقل از یک زبان برنامه نویسی خاص هست و برای streaming بسیار مناسب است و همچنین برای سیستمهای توزیع شده پیشنهاد میشود و به راحتی قابل توسعه و نگهداری است.
راجع به مزایای gRPC بسیار صحبت کردیم. برای طراحی سرویسهای متعددی که با یکدیگر در ارتباط هستند، مناسب میباشد. از HTTP/2 به صورت پیشفرض استفاده میکند (راجع به تفاوت HTTP/2 و HTTP1 اینجا را مطالعه بفرمایید).
شاید بزرگترین مشکلی که در حال حاضر دارد این است که REST را پشتیبانی نمیکند. بدین معنا که شما از طریق browser نمیتوانید یک در خواست را به یک سرور پیاده سازی شده توسط gRPC بصورت مستقیم ارسال کنید. راه حل اول برای حل این مشکل، پیاده سازی یک restful gateway با ابزار دلخواه خود و بقیه سرویسها بعد از آن به هم از طریق gRPC ارتباط برقرا میکنند، یا راه حل بهتر اینکه از grpc-gateway استفاده شود. ابزاری است که به کمک آن میتوانید سیستم خود را با REST یکپارچه سازی نمایید (هر چند راههای دیگری برای وصل شدن از مرورگر به یک سرور gRPC با استفاده از کتابخانههای third party میسر شده، اما خارج از موضوع بحث است و مطالعهی بیشتر را به خواننده واگذار میکنم)
قدم اول در پیاده سازی یک سرور/کلاینت با استفاده از gRPC، آشنایی با protocol buffers هست. برای آشنایی، به مقالهی قبلی رجوع فرمایید. تمامی پیاده سازیهای ما از روی کدهای تولید شده از تعاریف protocol bufferهایی هست که نوشتهایم.
حال فرض کنید میخواهیم یک سرور gRPC را با استفاده از #C نوشته و پیاده سازی نماییم:
۱) قدم اول قطعا نوشتن protobuf میباشد. همانطور که در مقالهی قبلی ذکر شده است، به صورت زیر، مدل و همچنین متدهای لازم را معرفی مینماییم و نام آن را helloworld.proto قرار میدهیم.
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
۲) حالا کافی است یک پروژهی Console را ساخته و ابتدا پکیجهای زیر را نصب نماییم.
Google.Protobuf grpc Grpc.Tools
<ItemGroup> <Protobuf Include="helloworld.proto" /> </ItemGroup>
using System; using System.Collections.Generic; using System.Threading.Tasks; using Helloworld; using Grpc.Core; namespace ServerGrpc { class GreeterImpl : Greeter.GreeterBase { public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context) { System.Console.WriteLine("request made!"); return Task.FromResult(new HelloReply { Message = "Hello " + request.Name }); } } class Program { const int Port = 50051; public static void Main(string[] args) { Server server = new Server { Services = { Greeter.BindService(new GreeterImpl()) }, Ports = { new ServerPort("localhost", Port, ServerCredentials.Insecure) } }; server.Start(); Console.WriteLine("Greeter server listening on port " + Port); Console.WriteLine("Press any key to stop the server..."); Console.ReadKey(); server.ShutdownAsync().Wait(); } } }
سرور را بر روی پورت مشخصی ایجاد کرده و همچنین سرویس مورد نظرمان را پیاده سازی کردهایم؛ به صورت فوق همه چیز به سادهترین صورت در نظر گرفته شده است.
gRPC به صورت خودکار از پروتکل امن ssl استفاده میکند؛ اما برای راحتی کار ما از آن استفاده نکردهایم.
نکته: فایلهای generate شده را از طریق آدرس زیر میتوانید پیدا کنید:
obj/Debug/netcoreapp2.2(یا نسخهی دیگری که استفاده میکنید)
حالا بنا داریم یک کلاینت را با یک زبان برنامه نویسی کاملا مجزا نوشته و به سرور grpc متصل شویم. این کلاینت را با زبان Go خواهیم نوشت (بدیهی است میتوان جای زبانهای برنامه نویسی کلاینت و سرور را تغییر داد).
نکته: خیلی وارد جزیات زبان Go نمیشویم و فقط اشارهای به موارد کلی خواهیم کرد.
ابتدا باید از روی protobuf کد مربوط به Go را تولید نماییم؛ به صورت زیر:
protoc helloworld.proto --go_out=plugins=grpc:.
یک فایل به نام main.go ساخته و کدهای زیر را وارد مینماییم.
package main import ( "fmt" "golang.org/x/net/context" "google.golang.org/grpc" "gosample/proto" ) func main() { initial() } func initial(){ conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure()) defer conn.Close() client := helloworld.NewGreeterClient(conn) data, _ := client.SayHello(context.Background(), &helloworld.HelloRequest{Name : "Ali"}) fmt.Println(data) }
به سرور به صورت insecure متصل شده ایم؛ آخر برنامه connection را میبندیم و SayHello را فراخوانی کرده و جواب را بر روی خروجی نمایش میدهیم.
نکته: gosample اسم پروژهای است که من ساختهام و proto آدرس پوشهای است که فایل تولید شدهی grpc داخل آن قرار گرفتهاست؛ بقیه نیز کتابخانههای لازم برای کار با grpc میباشد.
نکته: gRPC برای streaming دیتا بسیار مناسب است (هم یکطرفه و همینطور دو طرفه).
نکته: به دلیل سادگی کار با ابزارهای مختلف، انتخاب خیلی خوبی برای سیستمهای توزیع شدهاست؛ همانطور که مشاهده کردید به راحتی قابلیت تعامل بین زبانهای برنامه نویسی متعددی برقرار است.
نکتهی آخر: از وارد شدن به موارد ریز اجتناب کردهام و صرفا این مقاله جهت آشنایی و دید کلی نسبت به این موضوع در نظر گرفته شدهاست.
تب Script در FireBug مخصوص دیباگ کردن کدهای JavaScript است. امکاناتی که در این قسمت گنجانده شده بسیار کاربردی بوده و همچنین در بیشتر قسمتها با ابزارهای خطایابی دیگر مشابه است. برای مثال اگر با Visual Studio کار کرده باشید، با نحوهی ایجاد Break Point ، قسمتهای Watch,Stack و ... آشنا خواهید بود.
این پنل هم مشابه پنلهای دیگر فایرباگ دارای یک بخش با عنوان Options Menu هست که با راست کلیک کردن بروی عناون یا کلیک بروی مثلث کنار عنوان پنل قابل دسترسی است. تنظیماتی که در اینجا قابل تعیین است عبارتند از:
Panel Toolbar
نواری است که ابزارهای پنل بروی آن قرار دارند و به ترتیب عبارنتد از:
Context Menu
تنها قابلیت جدیدی که در این منو وجود دارد، Run To This Line است. هنگامی که پنل در حالت دیباگ است، با راست کلیک کردن بروی خط مورد نظر و انتخاب گزینهی Run to This Line میتوانید خطوط میانی را رد کرده و به خط مورد نظر بروید. البته خطوطی که رد میشوند ، اجرا میشوند.
این کار معادل این است که درخط مورد نظر یک Break Point اضافه کنید و دکمهی F8 را بزنید.
Breakpoints
توسط Break Pointها میتوانید خطوطی از برنامه را برای خطایابی مشخص کنید. زمانی که دیباگر به نقطهی مورد نظر برسد متوقف شده و میتوانید به بررسی برنامه بپردازید. میتوانید با بردن موس بروی متغییرها مقدار آن هارا بررسی کنید یا با کمک قسمتهای Watch و Stack در پنل جانبی اطلاعات بیشتری در مورد اجرای برنامه در نقطهی جاری بدست آورید.(در مورد پنلهای جانبی در قسمت بعدی توضیح خواهم داد.) همچنین با کمک دکمه هایی که در جدول فوق توضیح داده شده اند روند اجرای برنامه را خط به خط ادامه دهید.
برای ایجاد یک Break Point، بروی شمارهی خط مورد نظر کلیک کنید. نقطه ای قرمز رنگ نمایش داده خواهد شد. البته دقت کنید که همهی خطوط برنامه اجرا نمیشوند و نمیتوانید در آن خطوط از این امکان بهره ببرید. شمارهی خطوط فعال با رنگ سبز مشخص شده اند.
امکان مفید دیگری که همراه با این قابلیت ارائه شده است (که در محیطهای دیگر هم وجود دارد)، عبارت شرطی است. به این صورت که ضمن قرار دادن Break Point در یک نقطه از برنامه، میتوانید یک شرط هم برای توقف برنامه تعیین کنید.
فرض کنید یک حلقه دارید که 300 بار تکرار میشود و مثلا در اجرای 250ام آن مشکلی وجود دارد. در این حالت میتوانید از این قابلیت استفاده کنید و شرط توقف را i == 250 قرار بدهید.
برای تعیین یک شرط برای یک Break Point، بروی خط مورد نظر راست کلیک کنید و شرط را در قسمت مشخص شده وارد کنید.
امکان مفید دیگری که وجود دارد، Break Point خودکار است. اگر از مقالات قبلی دکمهی Break On All Errors در پنل Console و Break On Mutate در پنل HTML را بخاطر داشته باشید میدانید که در هر یک هنگام اجرای یک رخداد مورد نظر، دیباگر در خطی که موجب آن رخداد شده است متوقف شده و میتوانید کنترل برنامه را بدست بگیرید. در این حالت نیازی به ایجاد Break Point نیست و FireBug بصورت خودکار این کار را انجام میدهد.
Search
این بخش در همه پنلها وجود دارد با این تفاوت که در پنل Script و CSS دو گزینهی Multiple Files و Use Regular Expression وجود دارد که به ترتیب امکان جستجو در فایلهای js و css اضافه شده به صفحه هستند. قابلیت دیگری هم که فقط در پنل Script وجود دارد، پرش به یک خط در برنامه است به این صورت که با وارد کردن # و یک عدد به عنوان شمارهی خط، همان خط نمایش داده میشود.
در قسمت بعدی پنل جانبی که شامل سه بخش Watch، Stack و Breakpoints است را بررسی خواهیم کرد.
این پنل هم مشابه پنلهای دیگر فایرباگ دارای یک بخش با عنوان Options Menu هست که با راست کلیک کردن بروی عناون یا کلیک بروی مثلث کنار عنوان پنل قابل دسترسی است. تنظیماتی که در اینجا قابل تعیین است عبارتند از:
- Enabled/Disabled : برای فعال/غیرفعال کردن پنل است. فعال بودن این قسمت ممکن است موجب کاهش سرعت بارگزاری صفحات شود.
- Track Throw/Catch : در صورت فعال بودن این گزینه، در صورت رویدادن خطا در بلاک try/catch ، دیباگر در آن نقطه از برنامه متوقف شده و کنترل برنامه به شما داده میشود. (البته بنده موفق بررسی کامل این قابلیت نشدم. ظاهرا ورژنهای آخری باگ دارند. مثل غیرفعال شدن کامل همین پنل، "Debugger not activated"! اگر کسی موفقیتی در کار با این مورد داشت، سپاسگذار میشوم اطلاع بدهد.)
- Show Break Notifications : در صورت فعال بودن این گزینه، هنگام توقف کدی در صفحه، اطلاعاتی در مورد علت توقف آن در بالای پنل ارائه خواهد شد.
Panel Toolbar
نواری است که ابزارهای پنل بروی آن قرار دارند و به ترتیب عبارنتد از:
- Break On Next : این دکمه که مشابه آن در اکثر پنلها وجود دارد، هنگام اجرای یک دستور JavaScript آن را متوقف کرده و شما میتوانید به بررسی آن بپردازید.
- Script Type Menu : با انتخاب یکی از چهار گزینه موجود میتوانید نتیجه اسکریپتهای اضافه شده به صفحه که در قسمت Script Location Menu نمایش داده شده اند را فیلتر کنید. (متاسفانه این گزینه هم مشابه گزینه Track Throw/Catch برای بنده، ورژن 1.11.4 نتیجه ای نداشته است.)
- Script Location Menu : در این قسمت اسکریپت هایی که در صفحه وجود دارند نمایش داده میشوند. مشابه پنل HTML میتوانید هنگام بازبودن این لیست، با تایپ کردن نتایج را فیلتر کنید.
همچنین با راست کلیک کردن بروی این قسمت میتوانید آیتم یا فایل انتخاب شده را در ادیتور مورد نظر باز کنید، در پنل DOM بررسی کنید، فایل را در یک تب جدید باز کنید، آدرس فایل را در حافظه موقت کپی کنید. - Execution Control Buttons : این دکمهها زمانی که دیباگر به یک Break Point برسد یا به دلیل فعال بودن دکمههای Break On ... متوقف شود، فعال میشوند و با کمک آنها میتوانید عملیات خطایابی را ادامه دهید.
در جدول زیر این دکمهها و توضیحات تکمیلی هریک را مشاهده میکنید:نوع دکمه Shortcut توضیحات اجرای مجدد Shift + F18 Stack فعلی را مجدد اجرا میکند.
(Stack: لیستی از توابع به ترتیبی که با فراخوانی آنها تابع جاری فراخونی شده است. در مقاله بعدی توضیحات بیشتری ارائه خواهد شد.)ادامه F8 اجرای برنامه را (تا Break Point بعدی) ادامه میدهد. وارد شدن F11 در صورت رسیدن به یک تابع، با زدن این دکمه دیباگر وارد بندهی آن تابع میشود. رد شدن F10 اجرای برنامه را در سطح (Scope) فعلی ادامه میدهد.
مشابه قبلی وارد تابع نمیشود.خارج شدن Shift + F11 کنترل به تابعی که تابع جاری را فراخوانی کرده است بازمیگردد.
روشی سودمند زمانی که هنگام خطایابی وارد کدهای jQuery یا مثل آن میشوید :)
Context Menu
تنها قابلیت جدیدی که در این منو وجود دارد، Run To This Line است. هنگامی که پنل در حالت دیباگ است، با راست کلیک کردن بروی خط مورد نظر و انتخاب گزینهی Run to This Line میتوانید خطوط میانی را رد کرده و به خط مورد نظر بروید. البته خطوطی که رد میشوند ، اجرا میشوند.
این کار معادل این است که درخط مورد نظر یک Break Point اضافه کنید و دکمهی F8 را بزنید.
Breakpoints
توسط Break Pointها میتوانید خطوطی از برنامه را برای خطایابی مشخص کنید. زمانی که دیباگر به نقطهی مورد نظر برسد متوقف شده و میتوانید به بررسی برنامه بپردازید. میتوانید با بردن موس بروی متغییرها مقدار آن هارا بررسی کنید یا با کمک قسمتهای Watch و Stack در پنل جانبی اطلاعات بیشتری در مورد اجرای برنامه در نقطهی جاری بدست آورید.(در مورد پنلهای جانبی در قسمت بعدی توضیح خواهم داد.) همچنین با کمک دکمه هایی که در جدول فوق توضیح داده شده اند روند اجرای برنامه را خط به خط ادامه دهید.
برای ایجاد یک Break Point، بروی شمارهی خط مورد نظر کلیک کنید. نقطه ای قرمز رنگ نمایش داده خواهد شد. البته دقت کنید که همهی خطوط برنامه اجرا نمیشوند و نمیتوانید در آن خطوط از این امکان بهره ببرید. شمارهی خطوط فعال با رنگ سبز مشخص شده اند.
امکان مفید دیگری که همراه با این قابلیت ارائه شده است (که در محیطهای دیگر هم وجود دارد)، عبارت شرطی است. به این صورت که ضمن قرار دادن Break Point در یک نقطه از برنامه، میتوانید یک شرط هم برای توقف برنامه تعیین کنید.
فرض کنید یک حلقه دارید که 300 بار تکرار میشود و مثلا در اجرای 250ام آن مشکلی وجود دارد. در این حالت میتوانید از این قابلیت استفاده کنید و شرط توقف را i == 250 قرار بدهید.
برای تعیین یک شرط برای یک Break Point، بروی خط مورد نظر راست کلیک کنید و شرط را در قسمت مشخص شده وارد کنید.
امکان مفید دیگری که وجود دارد، Break Point خودکار است. اگر از مقالات قبلی دکمهی Break On All Errors در پنل Console و Break On Mutate در پنل HTML را بخاطر داشته باشید میدانید که در هر یک هنگام اجرای یک رخداد مورد نظر، دیباگر در خطی که موجب آن رخداد شده است متوقف شده و میتوانید کنترل برنامه را بدست بگیرید. در این حالت نیازی به ایجاد Break Point نیست و FireBug بصورت خودکار این کار را انجام میدهد.
Search
این بخش در همه پنلها وجود دارد با این تفاوت که در پنل Script و CSS دو گزینهی Multiple Files و Use Regular Expression وجود دارد که به ترتیب امکان جستجو در فایلهای js و css اضافه شده به صفحه هستند. قابلیت دیگری هم که فقط در پنل Script وجود دارد، پرش به یک خط در برنامه است به این صورت که با وارد کردن # و یک عدد به عنوان شمارهی خط، همان خط نمایش داده میشود.
در قسمت بعدی پنل جانبی که شامل سه بخش Watch، Stack و Breakpoints است را بررسی خواهیم کرد.
در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کردهاید .
یک مثال دیگر آن میتواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمیکند؛ اما ... زمانیکه نوبت به نمایش آن فرا میرسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره میکند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه میکنید تشکیل خواهند شد:
یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.
اولین نکته اضافهتری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر میباشد:
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر میگردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب میشود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت میشود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشهها فیلتر میشوند (مواردی که reply آنها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:
سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آنها در یک برنامه ASP.NET MVC میتوان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
یک مثال دیگر آن میتواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمیکند؛ اما ... زمانیکه نوبت به نمایش آن فرا میرسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; namespace EFGeneral { public class BlogComment { public int Id { set; get; } [MaxLength] public string Body { set; get; } public virtual BlogComment Reply { set; get; } public int? ReplyId { get; set; } public ICollection<BlogComment> Children { get; set; } } public class MyContext : DbContext { public DbSet<BlogComment> BlogComments { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Self Referencing Entity modelBuilder.Entity<BlogComment>() .HasOptional(x => x.Reply) .WithMany(x => x.Children) .HasForeignKey(x => x.ReplyId) .WillCascadeOnDelete(false); base.OnModelCreating(modelBuilder); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var comment1 = new BlogComment { Body = "نظر من این است که" }; var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 }; var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 }; context.BlogComments.Add(comment121); base.Seed(context); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var list = ctx.BlogComments //.where ... .ToList() // fills the childs list too .Where(x => x.Reply == null) // for TreeViewHelper .ToList(); if (list.Any()) { } } } } }
در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره میکند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه میکنید تشکیل خواهند شد:
یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.
اولین نکته اضافهتری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر میباشد:
public class BlogComment { // ... public ICollection<BlogComment> Children { get; set; } }
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب میشود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت میشود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشهها فیلتر میشوند (مواردی که reply آنها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:
سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آنها در یک برنامه ASP.NET MVC میتوان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty) { var children = childrenProperty(parentItem); if (children == null || !children.Any()) { return; } //...
یک نکتهی تکمیلی: امکان داشتن متدهایی با خروجی تگدار در برنامههای Blazor (یا تعریف کامپوننتهای پویا)
اگر با React کار کرده باشید، یک چنین کدهایی اساس آنرا تشکیل میدهند:
در اینجا کامپوننتی به نام Rentals تعریف شدهاست که خروجی آن ... یک تگ HTML ای است و برای تشکیل آن نیازی به استفاده از "" و چسباندن رشتهها نبودهاست. دقیقا شبیه به یک چنین کاری را میتوان در برنامههای Blazor نیز انجام داد که به آن «Razor template syntax» و یا «templated components» هم گفته میشود:
در اینجا همانطور که مشاهده میکنید، امکان انتساب یک قالب HTML ای شروع شدهی با @ به یک RenderFragment وجود دارد که حتی میتواند جنریک هم باشد و وهلهای از این شیء جنریک را به صورت یک lambda expression دریافت کند. روش درج آنها را در صفحه نیز مشاهده میکنید که همانند فراخوانی متدها است و برای نمونه ItemTemplate جنریک، وهلهای از فیلد emp تعریف شدهی در قسمت code را دریافت کرده و در صفحه نمایش میدهد.
یا حتی میتوان از RenderFragment برای وهله سازی پویای یک کامپوننت مانند SurveyPrompt، مقدار دهی خاصیت Title آن و درج آن در صفحه به صورت زیر هم استفاده کرد:
اگر با React کار کرده باشید، یک چنین کدهایی اساس آنرا تشکیل میدهند:
import React from "react"; const Rentals = () => { return <h1>Rentals</h1>; }; export default Rentals;
@page "/razor" @template @ItemTemplate(emp) @code { RenderFragment template = @<p>The time is @DateTime.Now.</p>; RenderFragment<Employee> ItemTemplate = (item) => @<p>Employee name is @item.Name.</p>; Employee emp = new Employee { Name = "Test" }; public class Employee { public string Name; } }
یا حتی میتوان از RenderFragment برای وهله سازی پویای یک کامپوننت مانند SurveyPrompt، مقدار دهی خاصیت Title آن و درج آن در صفحه به صورت زیر هم استفاده کرد:
@page "/" @CreateDynamicComponent() @code { RenderFragment CreateDynamicComponent() => builder => { builder.OpenComponent(0, typeof(SurveyPrompt)); builder.AddAttribute(1, "Title", "Some title"); builder.CloseComponent(); }; }
اگر به Entity data model wizard در VS.Net 2010 دقت کرده باشید، گزینهی "Pluralize or singularize generated object names" نیز به آن اضافه شده است:
این مورد از این جهت حائز اهمیت است که عموما نام جداول در بانک اطلاعاتی، جمع است و نام کلاس متناظر ایجاد شده برای آن در کدهای برنامه بهتر است مفرد باشد. برای مثال نام جدول، Customers است و نام کلاس آن بهتر است Customer تعریف گردد. به این صورت کار کردن با آن توسط یک ORM با معناتر خواهد بود؛ زیرا زمانیکه یک وهله از شیء Customer ایجاد میشود، فقط یک رکورد از بانک اطلاعاتی مد نظر است؛ در حالیکه یک جدول مجموعهای است از رکوردها.
زبان انگلیسی هم پر است از اسامی جمع و مفرد باقاعده و بیقاعده و کل عملیات با اضافه و حذف کردن یک s و یا es پایان نمییابد؛ برای مثال phenomenon و phenomena را در نظر بگیرد تا Money و Moneys.
این امکان مهیا شده توسط Entity Framework 4.0 یا همان EF v2 با برنامه نویسی هم قابل دسترسی است و در اسمبلی System.Data.Entity.Design.dll و فضای نام System.Data.Entity.Design.PluralizationServices قرار گرفته است.
این اسمبلی جزیی از دات نت 4 است و اگر آنرا توسط گزینهی Add references در VS.NET مشاهده نمیکنید، علت آن است که در تنظیمات پروژه جاری، گزینهی Target framework بر روی Client profile قرار گرفته است که باید به دات نت 4 کامل تغییر یابد.
استفاده از آن هم به صورت زیر است:
using System;
using System.Data.Entity.Design.PluralizationServices;
using System.Globalization;
namespace PluralizationServicesTest
{
class Program
{
static void Main(string[] args)
{
var service = PluralizationService.CreateService(CultureInfo.GetCultureInfo("en"));
Console.WriteLine(service.Pluralize("mouse"));
Console.WriteLine(service.IsPlural("phenomena"));
}
}
}
ملاحظات:
این روش فعلا به زبان انگلیسی محدود است و اگر Culture را به مورد دیگری تنظیم کنید با خطای "We don't support locales other than English yet" متوقف خواهید شد.
روش دیگر:
کتابخانهی سورس باز Castle ActiveRecord نیز دارای کلاسی است به نام Inflector که برای همین منظور طراحی شده است:
کاربرد آن در Fluent NHibernate
در Fluent NHibernate کار نگاشت کلاسها به جداول به صورت خودکار صورت میگیرد و همچنین تولید ساختار بانک اطلاعاتی نیز به همین نحو میباشد. اما میتوان تولید نام جداول را سفارشی نیز نمود. برای مثال از کلاس Book به صورت خودکار ساختار جدولی به نام Books را تولید کند:
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;
using NHibernate.Helper.Toolkit;
namespace NHibernate.Helper.MappingConventions
{
public class TableNameConvention : IClassConvention
{
public void Apply(IClassInstance instance)
{
instance.Table(Inflector.Pluralize(instance.EntityType.Name));
}
}
}
... = new AutoPersistenceModel()
.Where(...)
.Conventions.Setup(c =>c.Add<TableNameConvention>())
.AddEntityAssembly(...)
...
این دو متد را در نظر بگیرید:
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشدهاست.
سؤال: در یک برنامهی بزرگ چطور میتوان لیست Contextهای Dispose نشده را یافت؟
در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی میتوان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
همانطور که ملاحظه میکنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection میتوان دسترسی یافت.
مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شدهاست؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» میتوانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی میتوان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه میکنیم.
StackTrace توسط نکتهی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کردهاند؟ » تهیه میشود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص میگردد.
در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آنها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشدهاست، مشخص میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
EFNonDisposedContext.zip
private static void disposedContext() { using (var context = new MyContext()) { Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); } } private static void nonDisposedContext() { var context = new MyContext(); Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); }
سؤال: در یک برنامهی بزرگ چطور میتوان لیست Contextهای Dispose نشده را یافت؟
در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی میتوان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data; using System.Data.Common; using System.Data.Entity.Infrastructure.Interception; namespace EFNonDisposedContext.Core { public class DatabaseInterceptor : IDbConnectionInterceptor { public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Closed); } public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Disposed); } public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Opened); } // the rest of the IDbConnectionInterceptor methods ... } }
مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شدهاست؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» میتوانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی میتوان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
public enum ConnectionStatus { None, Opened, Closed, Disposed } public class ConnectionInfo { public string ConnectionId { set; get; } public string StackTrace { set; get; } public ConnectionStatus Status { set; get; } public override string ToString() { return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace); } }
StackTrace توسط نکتهی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کردهاند؟ » تهیه میشود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص میگردد.
در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آنها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشدهاست، مشخص میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
EFNonDisposedContext.zip
ASP.NET Web API 2 بهمراه یک سری قابلیت جدید جالب منتشر شده است. در این پست 5 قابلیت برتر از این قابلیتهای جدید را بررسی میکنیم.
این رویکرد routing مزایای خود را دارد. از جلمه اینکه تمام مسیرها در یک مکان واحد تعریف میشوند، اما تنها برای الگوهایی مشخص. مثلا پشتیبانی از nested routing روی یک کنترلر مشکل میشود.
1. Attribute Routing
در کنار سیستم routing فعلی، ASP.NET Web API 2 حالا از Attribute Routing هم پشتیبانی میکند. در مورد سیستم routing فعلی، میتوانیم قالبهای متعددی برای routing بنویسیم. هنگامی که یک درخواست به سرور میرسد، کنترلر مناسب انتخاب شده و اکشن متد مناسب فراخوانی میشود.
در لیست زیر قالب پیش فرض routing در Web API را مشاهده میکنید.
Config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{Controller}/{id}", defaults: new { id = RouteParameter.Optional } );
در ASP.NET Web API 2 به سادگی میتوانیم الگوی URI ذکرد شده را پشتیبانی کنیم. لیست زیر نمونه ای از یک الگوی URI با AttributeRouting را نشان میدهد.
URI Pattern --> books/1/authors [Route("books/{bookId}/authors")] public IEnumerable<Author> GetAuthorByBook(int bookId) { ..... }
2. CORS - Cross Origin Resource Sharing
بصورت نرمال، مرورگرها اجازه درخواستهای cross-domain را نمیدهند، که بخاطر same-origin policy است. خوب، (CORS (Cross Origin Resource Sharing چیست؟
CORS یک مکانیزم است که به صفحات وب این را اجازه میدهد تا یک درخواست آژاکسی (Ajax Request) به دامنه ای دیگر ارسال کنند. دامنه ای به غیر از دامنه ای که صفحه وب را رندر کرده است. CORS با استانداردهای W3C سازگار است و حالا ASP.NET Web API در نسخه 2 خود از آن پشتیبانی میکند.
3. OWIN (Open Web Interface for .NET) self-hosting
ASP.NET Web API 2 بهمراه یک پکیج عرضه میشود، که Microsoft.AspNet.WebApi.OwinSelfHost نام دارد.
طبق گفته وب سایت http://owin.org :
OWIN یک اینترفیس استاندارد بین سرورهای دات نت و اپلیکیشنهای وب تعریف میکند. هدف این اینترفیس جداسازی (decoupling) سرور و اپلیکیشن است. تشویق به توسعه ماژولهای ساده برای توسعه اپلیکیشنهای وب دات نت. و بعنوان یک استاندارد باز (open standard) اکوسیستم نرم افزارهای متن باز را تحریک کند تا ابزار توسعه اپلیکیشنهای وب دات نت توسعه یابند.
بنابراین طبق گفتههای بالا، OWIN گزینه ای ایده آل برای میزبانی اپلیکیشنهای وب روی پروسس هایی به غیر از پروسس IIS است. پیاده سازیهای دیگری از OWIN نیز وجود دارند، مانند Giacomo، Kayak,Firefly و غیره. اما Katana گزینه توصیه شده برای سرورهای مایکروسافت و فریم ورکهای Web API است.
4. IHttpActionResult
در کنار دو روش موجود فعلی برای ساختن response اکشن متدها در کنترلر ها، ASP.NET Web API 2 حالا از مدل جدیدی هم پشتیبانی میکند.
IHttpResponseMessage یک اینترفیس است که بعنوان یک فاکتوری (factory) برای HttpResponseMessage کار میکند. این روش بسیار قدرتمند است بدلیل اینکه web api را گسترش میدهد. با استفاده از این رویکرد، میتوانیم response هایی با هر نوع دلخواه بسازیم.
برای اطلاعات بیشتر به how to serve HTML with IHTTPActionResult مراجعه کنید.
5. Web API OData
پروتکل (OData (Open Data Protocol در واقع یک پروتکل وب برای کوئری گرفتن و بروز رسانی دادهها است. ASP.NET Web API 2 پشتیبانی از expand, $select$ و value$ را اضافه کرده است. با استفاده از این امکانات، میتوانیم نحوه معرفی پاسخ سرور را کنترل کنیم، یعنی representation دریافتی از سرور را میتوانید سفارشی کنید.
- expand$: بصورت نرمال، هنگام کوئری گرفتن از یک کالکشن OData، پاسخ سرور موجودیتهای مرتبط (related entities) را شامل نمیشود. با استفاده از expand$ میتوانیم موجودیتهای مرتبط را بصورت inline در پاسخ سرور دریافت کنیم.
- select$: از این متد برای انتخاب چند خاصیت بخصوص از پاسخ سرور استفاده میشود، بجای آنکه تمام خاصیتها بارگذاری شوند.
- value$: با این متد مقدار خام (raw) فیلدها را بدست میآورید، بجای دریافت آنها در فرمت OData.
چند مقاله خوب دیگر
نظرات مطالب
#Defensive Code in C - قسمت سوم
ایمن سازی کدها قبول؛
متد نهایی دو وظیفه رو انجام میدهد. یکی اعتبار سنجی دادهها و دومی محاسبه هدف نهایی در صورت ممکن.
یعنی باید تمام متدها بصورت درونی دادهها را خودشون اعتبار سنجی کنند؟
آیا اصل SRP رو نقض نکردیم؟
آیا اینچنین کد نوشتنها باعث تکرار کد نمیشوند؟
نوشتن آزمون واحد برای متدهای چند مسولیتی به چه صورت خواهد بود؟
در پروژه واقعی با تعداد متدهای زیاد و لایههای متعدد به چه صورت باید رفتار کرد؟
نکته ای که قابل تأمل هست اینه که متد ما دو عدد از نوع رشته ای میگیرد و خروجی عددی تولید میکند. شاید با رفع این مشکل بشه کد تمیزتر و ایمنتری نوشت.
متد نهایی دو وظیفه رو انجام میدهد. یکی اعتبار سنجی دادهها و دومی محاسبه هدف نهایی در صورت ممکن.
یعنی باید تمام متدها بصورت درونی دادهها را خودشون اعتبار سنجی کنند؟
آیا اصل SRP رو نقض نکردیم؟
آیا اینچنین کد نوشتنها باعث تکرار کد نمیشوند؟
نوشتن آزمون واحد برای متدهای چند مسولیتی به چه صورت خواهد بود؟
در پروژه واقعی با تعداد متدهای زیاد و لایههای متعدد به چه صورت باید رفتار کرد؟
نکته ای که قابل تأمل هست اینه که متد ما دو عدد از نوع رشته ای میگیرد و خروجی عددی تولید میکند. شاید با رفع این مشکل بشه کد تمیزتر و ایمنتری نوشت.