نظرات مطالب
متدهای کمکی مفید در پروژه های asp.net mvc
خطای زیر را میدهد:
LINQ to Entities does not recognize the method 'Persia.SolarDate ConvertToPersian(System.DateTime)' method, and this method cannot be translated into a store expression.
برای ساخت مدل
من اول به روش دیتابیس فرست EDMX را ساختم و سپس از ADO.NET DbContext Generator استفاده کردم.
سرچشمه Kafka از LinkedIn آغاز و سپس در سال 2011 توسط Apache بصورت open source ارائه شد. هدف آن ارائه یک بستر جریان دادهای توزیع شدهاست که اساس آن، Publish-Subscribe میباشد . سادگی اضافه کردن قابلیتهای مقیاس پذیری افقی، تحمل خطا و افزایش کارآیی توسط این بستر باعث شدهاست که هزاران شرکت از آن بعنوان بستر ارتباطی قسمتهای مختلف سیستمها و زیرسیستمهای خود استفاده کنند.
همانطور که گفته شد وظیفه و هدف اصلی Apache Kafka، ارائه یک بستر برای مدیریت و کنترل جریانهای اطلاعاتی با کارآیی بسیار بالا، در سیستمها و زیرسیستمهای مختلف است. یعنی شما میتوانید با ایجاد کردن یک Pipeline برای جریان اطلاعات خود، وابستگی مستقیم سیستمها و زیرسیستمها را از بین ببرید؛ آن هم بصورتی که بروز مشکلی در هر قسمت، کمترین میزان تاثیر را در سایر قسمتها داشته باشد.
فرض کنید شما تعداد زیادی سیستم و زیرسیستم مختلف را داشته باشید که هر کدام از آنها نیازمند ارتباط با برخی از قسمتهای دیگر است. در این صورت شما دو راه دارید: اول اینکه در هر قسمت سرویسهایی را برای ارتباط با سایر قسمتها پیاده سازی کنید و هر قسمت بصورت مستقیم با سایر قسمتها در ارتباط باشد.
مشخصا کنترل و مدیریت جریان اطلاعاتی در این پیاده سازی کار بسیار دشواری است. تغییر هر قسمت، تاثیر مستقیمی بر روی سایر قسمتها دارد و در صورتی که هریک از قسمتها با مشکلی روبرو شوند، سایر قسمتهای مرتبط نیز با مشکل روبرو میشوند. این مشکل زمانی بسیار نمایان میشود که در معماریهایی مانند میکروسرویس، بدلیل بالا رفتن تعداد زیرسیستمها و ارتباطات آنها، مدیریت این ارتباطات کار بسیار دشوار، پرهزینه و پیچیدهای میشود.
روش Apache Kafka برای رفع مشکل فوق به این صورت است که Kafka با بر عهده گرفتن مدیریت ارتباطات و جریان دادهای قسمتهای مختلف، به شما کمک میکند تا تیم پیاده سازی، تنها تمرکزشان را بر روی Businessی که میخواهند پیاده سازی کنند، قرار دهند. با این روش میتوانیم به راحتی سیستمهایی را پیاده سازی کنیم که از نظر ارتباطی در حالت معمول، پیچیده یا بسیار پیچیدهاند.
همانطور که میبینید دیگر نیازی نیست تا قسمتهای مختلف بصورت مستقیم با یکدیگر در ارتباط باشند؛ تمامی ارتباطات از طریق Kafka انجام میشود. تغییر یک قسمت، تاثیر زیادی بر روی سایر قسمتها ندارد. از دسترس خارج شدن یا بروز هر گونه مشکلی در یک قسمت، بر روی کل سیستم تاثیر زیادی ندارد. پیامهای مربوط به یک قسمت تا زمانی که پردازش نشدهاند از بین نمیروند. پس سیستمها میتوانند در حالت Offline نیز به کار خود ادامه دهند. شما میتوانید در این روش تمامی قسمتهای سیستم را بصورت یک Cluster پیاده سازی کنید. بنابراین احتمال از دسترس خارج شدن هر قسمت به کمترین میزان میرسد. اما حتی درصورتی که یک قسمت بصورت موقت از دسترس خارج شود، پیامهای مرتبط با آن قسمت تا زمانی که دوباره به جریان پردازش بازگردد، از بین نمیروند. پس از اضافه شدن قسمت از دسترس خارج شده، بلافاصله تمامی پیامهای مرتبط با آن قسمت برایش ارسال میشوند. برای بالا رفتن میزان کارآیی و تحمل خطا، به راحتی میتوانید خود Kafka را نیز بصورت یک Cluster پیاده سازی کنید و با بالا رفتن تعداد درخواست، در صورت نیاز میتوانید عملیات مقیاس پذیری افقی را به راحتترین روش ممکن انجام دهید.
نمایی از معماری کلی Apache Kafka:
برای شروع به آموزش Apache Kafka بهتر است ابتدا با مفاهیم و اصطلاحات آن آشنا شویم:
Producer:
ارسال کننده پیام. Application، سیستم یا زیرسیستمی که عملیات Publish پیام را برای Topic خاص از Kafka Server انجام میدهد.
Consumer:
دریافت کننده پیام. Application، سیستم یا زیرسیستمی که بر روی یک یا چند Topic خاص، Subscribe کردهاست (همچنین هر Consumer میتواند روی یک یا چند Partition از یک Topic خاص نیز Subscribe کند).
Consumer Group:
گروهی از Consumerها میباشند که با یک group.id، مشخص شدهاند. عموما این گروه شامل یک Replicate از یک Application است؛ مانند گروه ارسال کننده ایمیل (یک زیر سیستم ارسال کننده ایمیل که چندین بار در سرورهای مختلف اجرا شده است). Kafka این ضمانت را به ما میدهد که هر پیام ذخیره شده در یک Topic، برای تمامی Consumer Groupهای مرتبط ارسال شود؛ اما در هر Consumer Group، تنها یک دریافت کننده داشته باشد. یعنی هر پیام در هر Consumer Group، تنها توسط یک Consumer دریافت میشود.
Broker :
قسمتی که تمامی پیامها را از Producer دریافت میکند، سپس آنها را در Log مربوط به Topic مشخص شده ذخیره میکند و پس از آن، پیام ذخیره شده را برای تمامی Consumerهای مرتبط ارسال میکند.
Topic:
یک دسته بندی برای ذخیره کردن پیامهای Publish شده میباشد. Topicها همانند مفهوم Tableها در SQL Server میباشند. همانطور که میدانید هر Table از قبل تعریف شدهاست. یک کاربر با ارسال یک درخواست ثبت، دادهها را در آن ذخیره میکند و سپس گروهی از کاربران از دادههای ثبت شده استفاده میکنند. در مفهموم Topic نیز ابتدا ما Topic مورد نظر را با خصوصیاتی که باید داشته باشد تعریف میکنیم (البته میتوان بصورت Dynamic نیز آن را تعریف کرد؛ اما این روش توصیه نمیشود). سپس Producer پیام مربوطه را به همراه نام Topic برای Broker ارسال میکند. Broker پیام را در Partition مربوطه از Topic ذخیره میکند و سپس پیام برای تمامی Consumerهای مربوطه ارسال میشود.
Partition:
یکی از تفاوتهای بسیار مهم Kafka با سایر Message brokerها مانند RabitMQ که باعث بالارفتن کارآیی آن نیز شدهاست، قابلیت Partition در Topicها میباشد. در واقع هر Topic از یک یا چندین Partition برای ذخیره دادهها استفاده میکند. تعریف درست تعداد Partitionها در یک Topic، تاثیر مستقیمی بر درجه همزمانی و کارآیی در آن Topic و کل سیستم دارد. در Kafka تمامی پیامها به همان ترتیبی که وارد شدهاند، در Partitionهای یک Topic ذخیره میشوند و به همان ترتیب نیز برای Consumerها ارسال میشوند.
بطور مثال فرض کنید تعداد Partitionهای یک Topic با نام DepartmentMessage یک میباشد (از این Topic برای ذخیره پیامهای واحدهای مختلف یک سازمان استفاده میشود). در این صورت تمامی پیامهای دریافتی تنها در یک Partition ذخیره میشوند.
هر خانه از یک Partition، توسط یک شناسه از نوع int و با نام offset در دسترس است. تمامی پیامهای جدید ارسالی توسط Producer با offset ی بزرگتر از offset موجود در این Partition ذخیره میشوند؛ یعنی در انتهای آن قرار میگیرند. در مثال فوق در صورت دریافت پیام جدید، offset آن با عدد 10 مقداردهی میشود. همچنین عملیات خواندن نیز از کوچکترین offsetی که هنوز مقدار آن توسط Consumerها خوانده نشدهاست، انجام میشود. همانطور که مشخص است، بدلیل اینکه تعداد Partitionهای این مثال عدد یک میباشد، تمامی درخواستهای Producerها در یک Partition قرار میگیرند و تمامی Consumerها نیز از طریق یک Partition به پیامها دسترسی دارند؛ یعنی در صورت بالا بردن تعداد Producerها یا Consumerها، کارآیی بالا نمیرود. البته با اینکه کنترل مقدار اولیه offset برای شروع یک Consumer به دست خود Consumer و Zookeeper است، اما در اکثر موارد تمامی Consumerهای یک Topic باید از یک نقطه، شروع به خواندن دادهها کنند. در این حالت تا زمانیکه پیام با offset 1، توسط Consumerی خوانده نشود، هیچ Consumerی نمیتواند پیام شماره 2 را بخواند. استفاده کردن از یک Partition بیشتر زمانی کاربرد دارد که بخواهید تمامی پیامهایتان، واقعا در یک صف قرار بگیرند.
حال فرض کنید در سازمان شما سه واحد اداری، مالی و آموزش وجود دارد. در این صورت بدلیل اینکه تمامی پیامها در یک Partition ذخیره میشوند، تا زمانی که یک واحد تمامی پیامهای مرتبط با خود را از ابتدای Partition نخواندهاست، دیگر واحدها نمیتوانند به پیامهای مرتبط با خود دسترسی داشته باشند. پس در این صورت ما میتوانیم تعداد Partitionهای این Topic را عدد 3 درنظر بگیریم؛ بصورتی که پیامهای مرتبط با هر واحد در یک Partition جدا قرار بگیرد.
در این روش هر Producer زمانیکه پیامی را برای این Topic ارسال میکند، یک Key نیز برای آن مشخص میکند و این Key نشان دهنده این است که پیام جدید باید در کدام Partition ذخیره شود. یعنی بصورت همزمان میتوانید در هر سه Partition، پیامهایتان را ذخیره کنید؛ بصورتی که بطور مثال تمامی پیامهای مربوط به واحد اداری، در Partition 0 و تمامی پیامهای مربوط به واحد مالی، در Partition 1 و واحد آموزش، در Partition 2 ذخیره شوند و همچنین عملیات خواندن از این Topic نیز میتواند بصورت همزمان در واحدهای مختلف انجام شود.
باید در تعریف تعداد Partitionهای یک Topic این نکته را در نظر بگیرید که این تعداد کاملا به نیازمندی شما و کارآیی که شما مد نظر دارید، بستگی دارد. تعداد این Partitionها حتی میتواند به تعداد Userهای یک سیستم نیز تعریف شود. علاوه بر آن باید بدانید که هر Partition در هر زمان تنها توسط یک Primary Broker میتواند در دسترس سایر قسمتها قرار بگیرد و تمامی عملیات خواندن و نوشتن در Partition توسط این Kafka Server انجام میشود و در صورتیکه به هر دلیلی این سرور از دسترس خارج شود، مدیریت این Partition به سرورهای دیگر داده میشود.
Cluster:
مجموعه ای از Brokerها میباشد که بصورت یک Cluster اجرا شدهاند. این کار باعث بالا رفتن کارآیی و تحمل خطا میشود.
Primary Broker:
یک Kafka Server که مسئول خواندن و نوشتن در یک Partition است. در یک Cluster هر Partition در یک زمان تنها یک Primary Broker دارد. این Primary Broker همزمان میتواند برای Partitionهای دیگر نقش Replicas Broker را بازی کند. انتخاب یک Primary Broker برای یک Partition توسط ZooKeeper انجام میشود.
Replicas Brokers :
Kafka Serverهایی که شامل یک کپی از Partition میباشند. عملیات خواندن و نوشتن در Partition توسط Primary انجام میشود. در صورتیکه Primary از دسترس خارج شود، ZooKeeper یکی از Replicas Brokerها را بعنوان Primary در نظر میگیرد. همچنین این نکته را باید در نظر بگیرید که هر Replicate همزمان میتواند Primary پارتیشنهای دیگر باشد.
Replication Factor :
این خصوصیت احتمال از دست دادن دادههای یک Topic را به حداقل میرساند؛ به این صورت که هر پیام از یک Topic، در چندین سرور مختلف که تعداد آنها توسط این خصوصیت مشخص میشود، نگهداری میشود.
Apache ZooKeeper :
Kafka هیچ Stateی را نگه نمیدارد (اصطلاحا stateless میباشد). برای ذخیره کردن و مدیریت تمامی Stateها از جمله اینکه در حال حاضر Primary Broker برای یک Partition چه سروری است، یا اینکه پیامهای یک Partition تا کدام offset توسط Consumerها خوانده شدهاند یا اینکه کدام Consumer در حال حاضر در یک Consumer Group مسئول یک Partition میباشد، توسط Apache Zookeeper انجام میشود.
ضمانتهایی که Kafka میدهد:
- تمامی پیامهای دریافتی در یک Partition از یک Topic، به همان ترتیبی که دریافت میشوند ذخیره میشوند.
- Consumerها تمامی پیامها را در یک Partition به همان ترتیبی که ذخیره شدهاند، دریافت میکنند.
- در یک Topic با Replication Factorی با مقدار N، درجه تحمل خطا N - 1 میباشد.
تا اینجا با اهداف، مفاهیم و اصطلاحات Apache Kafka آشنا شدیم. در بخش بعد به راه اندازی قسمتهای مختلف آن در Ubuntu میپردازیم و میبینیم که به چه صورت میتوان به راحتی یک Cluster از سرورهای Kafka را ایجاد کرد.
Tag Helper Components یکی از ویژگیهای جدید ASP.NET Core 2.0 است و هدف آن میسر ساختن ایجاد و یا ویرایش المانهای HTML ایی در حال رندر در صفحه هستند. برای مثال یکی از کاربردهای آنها میتواند افزودن اسکریپتی به صورت پویا به تمام صفحات سایت باشد؛ مانند روش مایکروسافت برای افزودن Application Insights به برنامههای ASP.NET Core. در این حالت متد UserApplicationInsights یک tag helper component را به سیستم تزریق وابستگیها اضافه میکند که کار آن افزودن اسکریپتهای Application Insights به برنامه است؛ بدون اینکه نیازی باشد تا صفحات برنامه را جهت درج این اسکریپتها ویرایش کرد یا تغییر داد.
یک مثال: تهیهی یک TagHelperComponent جهت ویرایش تگهای article
در اینجا کار با ارث بری از کلاس پایه TagHelperComponent شروع میشود. عملکرد آن این است که اگر موتور Razor به پردازش تگ article رسید:
آنگاه اسکریپتی را که ملاحظه میکنید در این بین درج کند.
در ادامه برای اینکه سیستم را از وجود این TagHelperComponent مطلع کنیم، باید آنرا به صورت یک سرویس جدید، به فایل آغازین برنامه معرفی کنیم:
این نوع کامپوننتها تمام تگهای مشخص article موجود در صفحه را هدف قرار میدهند. اما ... اگر آنرا اجرا کنید اتفاقی خاصی رخ نخواهد داد!
نکتهی مهم TagHelperComponentها این است که در قسمت بررسی تگ در حال پردازش:
اگر این تگ ویژه که در اینجا برای مثال article نام دارد، پیشتر تحت عنوان یک TagHelperComponentTagHelper ثبت شده باشد، آنگاه قابلیت اجرا و تحت تاثیر قراردادن این تگ را خواهد یافت. به همین جهت باید این تگ را به عنوان HtmlTargetElement به صورت ذیل تعریف کرد:
سپس آنرا به فایل Views\_ViewImports.cshtml به نحو زیر اضافه نمود:
در اینجا TestTagHelperComponent2 نام اسمبلی جاری است که حاوی ArticleTagHelperComponentTagHelper میباشد.
پس از این تنظیمات است که اگر برنامه را اجرا کنید، این تغییر را (درج اسکریپت در بین تگ article) ملاحظه خواهید کرد:
Tag Helper Components توکار ASP.NET Core 2.0
در حال حاضر دو TagHelperComponent به نامهای HeadTagHelper و BodyTagHelper به صورت پیش فرض به سیستم اضافه شدهاند. یعنی تگهای head و body در ASP.NET Core 2.0 را میتوان توسط TagHelperComponent تحت تاثیر قرار داد و نیازی به تنظیمات TagHelperComponentTagHelper اضافهی فوق برای آنها وجود ندارد.
یک مثال:
در اینجا چون تگ ویژهی head پیشتر در سیستم ثبت شدهاست، مقایسهی انجام شده معتبر بوده و برای فعالسازی آن تنها کاری را که باید انجام داد، ثبت سرویس آن است (البته به شرطی که Microsoft.AspNetCore.Mvc.TagHelpers در فایل Views\_ViewImports.cshtml پیشتر تعریف شده باشد):
اینکار سبب درج اسکریپتی پیش از بسته شدن تگ head صفحه میشود:
یک مثال: تهیهی یک TagHelperComponent جهت ویرایش تگهای article
using System; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; namespace TestTagHelperComponent2.Utils { public class ArticleTagHelperComponent : TagHelperComponent { public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase)) { output.PostContent.AppendHtml("<script>console.log('Running ArticleTagHelperComponent');</script>"); } return Task.CompletedTask; } } }
<article> For Testing the TagHelperComponent. </article>
در ادامه برای اینکه سیستم را از وجود این TagHelperComponent مطلع کنیم، باید آنرا به صورت یک سرویس جدید، به فایل آغازین برنامه معرفی کنیم:
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>(); services.AddMvc(); }
نکتهی مهم TagHelperComponentها این است که در قسمت بررسی تگ در حال پردازش:
if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
using System; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; namespace TestTagHelperComponent2.Utils { [HtmlTargetElement("article")] [EditorBrowsable(EditorBrowsableState.Never)] public class ArticleTagHelperComponentTagHelper : TagHelperComponentTagHelper { public ArticleTagHelperComponentTagHelper( ITagHelperComponentManager componentManager, ILoggerFactory loggerFactory) : base(componentManager, loggerFactory) { } } }
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, TestTagHelperComponent2
پس از این تنظیمات است که اگر برنامه را اجرا کنید، این تغییر را (درج اسکریپت در بین تگ article) ملاحظه خواهید کرد:
Tag Helper Components توکار ASP.NET Core 2.0
در حال حاضر دو TagHelperComponent به نامهای HeadTagHelper و BodyTagHelper به صورت پیش فرض به سیستم اضافه شدهاند. یعنی تگهای head و body در ASP.NET Core 2.0 را میتوان توسط TagHelperComponent تحت تاثیر قرار داد و نیازی به تنظیمات TagHelperComponentTagHelper اضافهی فوق برای آنها وجود ندارد.
یک مثال:
public class MyHeadTagHelperComponent : TagHelperComponent { public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.Equals(context.TagName, "head", StringComparison.OrdinalIgnoreCase)) { output.PostContent.AppendHtml("<script>console.log('head tag');</script>"); } return Task.CompletedTask; } }
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITagHelperComponent, MyHeadTagHelperComponent>(); services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>(); services.AddMvc(); }
اشتراکها
معرفی استاندارد سورس باز #C
Moving the standards work into the open, under the .NET Foundation, makes it easier for standardization work. Everything from language innovation and feature design through implementation and on to standardization now takes place in the open. It will be easier to ask questions among the language design team, the compiler implementers, and the standards committee. Even better, those conversations will be public.
-
Separated the existing "Restore Packages" option in the Command Palette into two distinct functions:
- "Restore Project" - Displays a drop-down that shows all the available projects in the solution or in the workspace. Selecting one of them would trigger a dotnet restore for the particular project.
- "Restore All Projects" - Triggers a dotnet restore for all projects in the current solution or workspace.
هر برنامهی وب، دارای یک frontend و یک backend است. تا اینجا، تمام تمرکز این سری، بر روی پیاده سازی frontend بود و هیچکدام از برنامههایی را که تکمیل کردیم، تبادل اطلاعاتی را با وب سرویسهای backend نداشتند؛ اما به عنوان یک توسعه دهندهی React، نیاز است با نحوهی ارتباط با سرور آشنایی داشت که در طی چند قسمت به آن میپردازیم.
ایجاد برنامهی backend ارائه دهندهی REST API
در اینجا یک برنامهی سادهی ASP.NET Core Web API را جهت تدارک سرویسهای backend، مورد استفاده قرار میدهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی میتوانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائهی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. میتوان به این endpintها درخواستهای HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته میشود که جهت آزمایش برنامهها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، میتوان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائهی آرایهای از اشیاء جاوا اسکریپتی قابل استفادهی در برنامههای frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال میکنیم، آرایهای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کردهاست.
اگر علاقمندید بودید میتوانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائهی مطالب آنرا با ASP.NET Core Web API نیز پیاده سازی میکنیم (برای مطالعهی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز میتوان استفاده کرد):
مدل مطالب
ساختار این مدل، با ساختار مدل مطالب JSONPlaceholder یکی درنظر گرفته شدهاست، تا مطلب قابلیت پیگیری بیشتری را پیدا کند.
منبع دادهی فرضی مطالب
برای ارائهی سادهتر برنامه، یک منبع دادهی درون حافظهای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار میدهیم:
در این سرویس، نیازمندیهای کنترلر مطالب مانند ارائه لیست تمام مطالب، نمایش اطلاعات یک مطلب، به روز رسانی، ایجاد و حذف یک مطلب، تدارک دیده شدهاند. سپس از این سرویس در کنترلر زیر استفاده میکنیم:
کنترلر Web API برنامهی backend
این کنترلر که در مسیر شروع شدهی با https://localhost:5001/api قرار میگیرد، جهت پشتیبانی از افعال مختلف HTTP مانند Get/Post/Delete/Update طراحی شدهاست که در ادامه، در برنامهی React خود از آنها استفاده خواهیم کرد. پس از ایجاد این پروژهی web api، یک نمونه خروجی آن در مسیر https://localhost:5001/api/posts، به صورت زیر خواهد بود:
البته نمایش فرمت شدهی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.
ایجاد ساختار ابتدایی برنامهی ارتباط با سرور
در اینجا برای بررسی کار با سرور، یک پروژهی جدید React را ایجاد میکنیم:
در ادامه توئیتر بوت استرپ 4 را نیز نصب میکنیم. برای این منظور پس از باز کردن پوشهی اصلی برنامه توسط VSCode، دکمههای ctrl+` را فشرده (ctrl+back-tick) و دستور زیر را در ترمینال ظاهر شده وارد کنید:
سپس برای افزودن فایل bootstrap.css به پروژهی React خود، ابتدای فایل index.js را به نحو زیر ویرایش خواهیم کرد:
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام میدهد، مورد استفاده قرار میگیرد.
سپس فایل app.js را به شکل زیر تکمیل میکنیم:
که حاصل آن، یک دکمه، برای افزودن مطلبی جدید، به همراه جدولی است از مطالب که قصد داریم در ادامه، اطلاعات آنرا از سرور دریافت کرده و حذف و یا به روز رسانی کنیم:
نگاهی به انواع و اقسام HTTP Clientهای مهیا
در ادامه نیاز خواهیم داشت تا از طریق برنامههای React خود، درخواستهای HTTP را به سمت سرور (یا همان برنامهی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راهحل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت میتوانید از کتابخانههایی که خودتان ترجیح میدهید، نسبت به کتابخانههایی که به شما ارائه/تحمیل (!) میشوند (مانند برنامههای Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شدهاست! ابتدا با یک کتابخانهی HTTP مقدماتی شروع کردند. بعدی آنرا منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابستهاست به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار میکنید، باید کارها را آنگونه که Angular میپسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح میشود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمیتر آنها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید میتوانید از محصور کنندههای آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی میتوانید از آن در برنامههای React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانهی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژهی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!
نصب Axios در برنامهی React این قسمت
برای نصب کتابخانهی Axios، در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
پس از برپایی این مقدمات، ادامهی مطلب «ارتباط با سرور» را در قسمت بعدی پیگیری میکنیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
ایجاد برنامهی backend ارائه دهندهی REST API
در اینجا یک برنامهی سادهی ASP.NET Core Web API را جهت تدارک سرویسهای backend، مورد استفاده قرار میدهیم. هرچند این مورد الزامی نبوده و اگر علاقمند بودید که مستقل از آن کار کنید، حتی میتوانید از سرویس آنلاین JSONPlaceholder نیز برای این منظور استفاده کنید که یک Fake Online REST API است. کار آن ارائهی یک سری endpoint است که به صورت عمومی از طریق وب قابل دسترسی هستند. میتوان به این endpintها درخواستهای HTTP خود را مانند GET/POST/DELETE/UPDATE ارسال کرد و از آن اطلاعاتی را دریافت نمود و یا تغییر داد. به هر کدام از این endpointها یک API گفته میشود که جهت آزمایش برنامهها بسیار مناسب هستند. برای نمونه در قسمت resources آن اگر به آدرس https://jsonplaceholder.typicode.com/posts مراجعه کنید، میتوان لیستی از مطالب را با فرمت JSON مشاهده کرد. کار آن ارائهی آرایهای از اشیاء جاوا اسکریپتی قابل استفادهی در برنامههای frontend است. بنابراین زمانیکه یک HTTP GET را به این endpoint ارسال میکنیم، آرایهای از اشیاء مطالب را دریافت خواهیم کرد. همین endpoint، امکان تغییر این اطلاعات را توسط برای مثال HTTP Delete نیز میسر کردهاست.
اگر علاقمندید بودید میتوانید از JSONPlaceholder استفاده کنید و یا در ادامه دقیقا ساختار همین endpoint ارائهی مطالب آنرا با ASP.NET Core Web API نیز پیاده سازی میکنیم (برای مطالعهی قسمت «ارتباط با سرور» اختیاری است و از هر REST API مشابهی که توسط nodejs یا PHP و غیره تولید شده باشد نیز میتوان استفاده کرد):
مدل مطالب
namespace sample_22_backend.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } public int UserId { set; get; } } }
منبع دادهی فرضی مطالب
برای ارائهی سادهتر برنامه، یک منبع دادهی درون حافظهای را به همراه یک سرویس، در اختیار کنترلر مطالب، قرار میدهیم:
using System; using System.Collections.Generic; using System.Linq; using sample_22_backend.Models; namespace sample_22_backend.Services { public interface IPostsDataSource { List<Post> GetAllPosts(); bool DeletePost(int id); Post AddPost(Post post); bool UpdatePost(int id, Post post); Post GetPost(int id); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> public class PostsDataSource : IPostsDataSource { private readonly List<Post> _allPosts; public PostsDataSource() { _allPosts = createDataSource(); } public List<Post> GetAllPosts() { return _allPosts; } public Post GetPost(int id) { return _allPosts.Find(x => x.Id == id); } public bool DeletePost(int id) { var item = _allPosts.Find(x => x.Id == id); if (item == null) { return false; } _allPosts.Remove(item); return true; } public Post AddPost(Post post) { var id = 1; var lastItem = _allPosts.LastOrDefault(); if (lastItem != null) { id = lastItem.Id + 1; } post.Id = id; _allPosts.Add(post); return post; } public bool UpdatePost(int id, Post post) { var item = _allPosts .Select((pst, index) => new { Item = pst, Index = index }) .FirstOrDefault(x => x.Item.Id == id); if (item == null || id != post.Id) { return false; } _allPosts[item.Index] = post; return true; } private static List<Post> createDataSource() { var list = new List<Post>(); var rnd = new Random(); for (var i = 1; i < 10; i++) { list.Add(new Post { Id = i, UserId = rnd.Next(1, 1000), Title = $"Title {i} ...", Body = $"Body {i} ..." }); } return list; } } }
کنترلر Web API برنامهی backend
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc; using sample_22_backend.Models; using sample_22_backend.Services; namespace sample_22_backend.Controllers { [ApiController] [Route("api/[controller]")] public class PostsController : ControllerBase { private readonly IPostsDataSource _postsDataSource; public PostsController(IPostsDataSource postsDataSource) { _postsDataSource = postsDataSource; } [HttpGet] public ActionResult<List<Post>> GetPosts() { return _postsDataSource.GetAllPosts(); } [HttpGet("{id}")] public ActionResult<Post> GetPost(int id) { var post = _postsDataSource.GetPost(id); if (post == null) { return NotFound(); } return Ok(post); } [HttpDelete("{id}")] public ActionResult DeletePost(int id) { var deleted = _postsDataSource.DeletePost(id); if (deleted) { return Ok(); } return NotFound(); } [HttpPost] public ActionResult<Post> CreatePost([FromBody]Post post) { post = _postsDataSource.AddPost(post); return CreatedAtRoute(nameof(GetPost), new { post.Id }, post); } [HttpPut("{id}")] public ActionResult<Post> UpdatePost(int id, [FromBody]Post post) { var updated = _postsDataSource.UpdatePost(id, post); if (updated) { return Ok(post); } return NotFound(); } } }
البته نمایش فرمت شدهی JSON در مرورگر کروم، نیاز به نصب این افزونه را دارد.
ایجاد ساختار ابتدایی برنامهی ارتباط با سرور
در اینجا برای بررسی کار با سرور، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app sample-22-frontend > cd sample-22-frontend > npm start
> npm install --save bootstrap
import "bootstrap/dist/css/bootstrap.css";
سپس فایل app.js را به شکل زیر تکمیل میکنیم:
import "./App.css"; import React, { Component } from "react"; class App extends Component { state = { posts: [] }; handleAdd = () => { console.log("Add"); }; handleUpdate = post => { console.log("Update", post); }; handleDelete = post => { console.log("Delete", post); }; render() { return ( <React.Fragment> <button className="btn btn-primary mt-1 mb-1" onClick={this.handleAdd}> Add </button> <table className="table"> <thead> <tr> <th>Title</th> <th>Update</th> <th>Delete</th> </tr> </thead> <tbody> {this.state.posts.map(post => ( <tr key={post.id}> <td>{post.title}</td> <td> <button className="btn btn-info btn-sm" onClick={() => this.handleUpdate(post)} > Update </button> </td> <td> <button className="btn btn-danger btn-sm" onClick={() => this.handleDelete(post)} > Delete </button> </td> </tr> ))} </tbody> </table> </React.Fragment> ); } } export default App;
نگاهی به انواع و اقسام HTTP Clientهای مهیا
در ادامه نیاز خواهیم داشت تا از طریق برنامههای React خود، درخواستهای HTTP را به سمت سرور (یا همان برنامهی backend) ارسال کنیم، تا بتوان اطلاعاتی را از آن دریافت کرد و یا تغییری را در اطلاعات موجود، ایجاد نمود. همانطور که پیشتر نیز در این سری عنوان شد، React برای این مورد نیز راهحل توکاری را به همراه ندارد و تنها کار آن، رندر کردن View و مدیریت DOM است. البته شاید این مورد یکی از مزایای کار با React نیز باشد! چون در این حالت میتوانید از کتابخانههایی که خودتان ترجیح میدهید، نسبت به کتابخانههایی که به شما ارائه/تحمیل (!) میشوند (مانند برنامههای Angular) آزادی انتخاب کاملی را داشته باشید. برای مثال هرچند Angular به همراه یک HTTP Module توکار است، اما تاکنون چندین بار بازنویسی از ابتدا شدهاست! ابتدا با یک کتابخانهی HTTP مقدماتی شروع کردند. بعدی آنرا منسوخ شده اعلام و با یک ماژول جدید جایگزین کردند. بعد در نگارشی دیگر، چون این کتابخانه وابستهاست به RxJS و خود RxJS نیز بازنویسی کامل شد، روش کار کردن با این HTTP Module نیز مجددا تغییر پیدا کرد! بنابراین اگر با Angular کار میکنید، باید کارها را آنگونه که Angular میپسندد، انجام دهید؛ اما در اینجا خیر و آزادی انتخاب کاملی برقرار است.
بنابراین اکنون این سؤال مطرح میشود که در React، برای برقراری ارتباط با سرور، چه باید کرد؟ در اینجا آزاد هستید برای مثال از Fetch API جدید مرورگرها و یا روش Ajax ای مبتنی بر XML قدیمیتر آنها، استفاده کنید (اطلاعات بیشتر) و یا حتی اگر علاقمند باشید میتوانید از محصور کنندههای آن مانند jQuery Ajax استفاده کنید. بنابراین اگر با jQuery Ajax راحت هستید، به سادگی میتوانید از آن در برنامههای React نیز استفاده کنید. اما ... ما در اینجا از یک کتابخانهی بسیار محبوب و قدرتمند HTTP Client، به نام Axios (اکسیوس/ یک واژهی یونانی به معنای «سودمند») استفاده خواهیم کرد که فقط تعداد بار دانلود هفتگی آن، 6 میلیون بار است!
نصب Axios در برنامهی React این قسمت
برای نصب کتابخانهی Axios، در ریشهی پروژهی React این قسمت، دستور زیر را در خط فرمان صادر کنید:
> npm install --save axios
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-frontend-part-01.zip و sample-22-backend-part-01.zip
مطالب
Build Events
در ویژوال استودیو یک ویژگی جالب با عنوان Pre/Post-Build Event وجود دارد. این ویژگی به رویدادهای «قبل از بیلد» و «بعد از بیلد» اشاره دارد. از این ویژگی برای اجرای یکسری دستورات، قبل (Pre-build) یا بعد (Post-build) از عملیات بیلد استفاده میشود. دستوراتی که در این قسمت قابل اجرا هستند دقیقا همانند دستورات موجود در یک batch فایل میباشند. حتی میتوان یک فایل bat. را در این قسمت فراخوانی کرد. بطور خلاصه هرگونه دستوری که درون Command Prompt ویندوز یا در یک bat. فایل قابل اجرا باشد در این قسمت نیز قابل استفاده است. درنهایت تمام این دستورات توسط برنامه Cmd.exe اجرا میشوند.
همانطور که میبینید در ویژوال استودیو تنها ذخیره این تنظیمات در فایل پروژه انجام میشود و کلیه عملیات توسط ابزار MSBuild مدیریت میگردد. امکان بهر برداری از این رویدادها با استفاده مستقیم از ابزار MSBuild نیز وجود دارد اما به دلیل مفصل بودن بحث، جستجوی بیشتر به خوانندگان واگذار میشود.
نکته: قبل از ادامه بهتر است به این نکته اشاره کنم که مجموعه این دستورات چیزی فراتر از فراخوانی ساده یکسری فایل exe. هستند. درواقع کدی که در این قسمت به آن اشاره میشود، دارای ساختاری به صورت یک زبان برنامه نویسی ساده است. یعنی متن نهاییای که برای اجرا به cmd.exe ارسال میشود میتواند شامل دستورات ساده و اولیه برنامه نویسی چون if .. then .. else و حلقه for و از این قبیل نیز باشد. برای آشنایی بیشتر با زبان این نوع دستورات به منابع زیر مراجعه کنید:
تنظیم رویدادهای بیلد (Build Events)
برای تنظیم این رویدادها باید به تب Build Events در صفحه پراپرتیهای پروژه موردنظر مراجعه کنید. همانند تصویر زیر در یک پروژه کنسول #C:
البته در پروژههای VB.NET مسیر منتهی به این قسمت کمی فرق میکند که در تصویر زیر نشان داده شده است:
در پروژههای مربوط به زبانهای دیگر هم مسیر رسیدن به این رویدادها کمی متفاوت است. برای کسب اطلاعات بیشتر به اینجا مراجعه کنید.
در این قسمت میتوان همانند یک فایل batch دستورات موردنظر را در خطوط مجزا برای اجرا اضافه کرد. از این دستورات معمولا برای مدیریت عملیات بیلد، کپی فایلهای موردنیاز قبل یا بعد از بیلد، پاک کردن فولدرها، تغییر برخی تنظیمات با توجه به نوع کانفیگ بیلد (Debug یا Release)، ثبت یک اسمبلی در GAC و یا حتی اجرای برخی آزمونهای واحد و ... استفاده میشود.
نکته: درصورتیکه پروژه به روز باشد (یعنی ویژوال استودیو نیازی به تولید فایل اسمبلی نهایی پروژه به دلیل عدم وجود تغییری در کد برنامه نبیند) بدلیل عدم اجرای عملیات بید، دستورات قسمت Pre-build اجرا نمیشوند. اجرای دستورات قسمت Post-build نیز بستگی به تنظیمات قسمت :Run the post-build events همانند تصویر زیر دارد:
برای استفاده راحتتر از این ویژگی فرمی مخصوص وارد کردن این دستورات در ویژوال استودیو وجود دارد. برای دیدن این فرم بر روی دکمه ...Edit Pre-build یا ...Edit Post-build کلیک کنید. پنجره زیر نمایش داده میشود:
در این پنجره میتوان دستورات مورد نظر را وارد کرد. با اینکه هیچ امکان خاصی برای کمک به اضافه و ویرایش دستورات در این پنجره وجود ندارد! اما تنها ویژگی موجود در این فرم کمک بسیاری برای تکمیل دستورات موردنظر میکند. قبل از توضیح این ویژگی بهتر است با مفهوم Macro در این قسمت آشنا شویم.
Macro
در Build Events ویژوال استودیو یکسری متغیرهای ازقبل تعریف شده وجود دارد که به آنها Macro گفته میشود. برای مشاهده لیست این ماکروها روی دکمه << Macro کلیک کنید. پنجره مربوطه به صورت زیر گسترش مییابد تا جدولی به نام Macro Table را نمایش دهد:
همانطور که مشاهده میکنید تعداد 19 ماکرو به همراه مقادیرشان در این قسمت به نمایش گذاشته شده است. برای استفاده از این ماکروها کافی است تا روی یکی از آنها دابل کلیک کنید یا پس از انتخاب ماکروی موردنظر روی دکمه Insert کلیک کنید. دقت کنید که نحوه نمایش این ماکروها در متن دستورات به صورت زیر است:
$(<Macro_Name>)
که به جای عبارت <Macro_Name> عنوان ماکرو قرار میگیرد. مثلا:
$(OutDir) یا $(ProjectName)
نکته: نام این ماکروها case-sensitive نیست.
نحوه اجرای دستورات توسط ویژوال استودیو
ویژوال استودیو برای اجرای دستورات کار خاصی به صورت مستقیم انجام نمیدهد! وظیفه اصلی برعهده MSBuild (^) است. این ابزار پس از جایگزین کردن مقادیر ماکروها، محتوای کل دستورات موجود در هر یک از رویدادها را در یک فایل batch ذخیره میکند و فایل مربوط به هر رویداد را در زمان خودش به اجرا میگذارد. مثلا دستور زیر را درنظر بگیرید:
Copy $(OutDir)*.* %WinDir%
پس از ذخیره در فایل batch نهایی به صورت زیر در خواهد آمد:
Copy bin\Debug\*.* %WinDir%
نکته: در این زبان برنامه نویسی، عبارتی چون %WinDir% معرف یک متغیر است. در این مورد خاص این عبارت یک متغیر محیطی (Environment Variable) است. اطلاعات بیشتر در اینجا.
MSBuild عملیات اجرای این batch فایلهای تولیدی را زیر نظر دارد و هرگونه خطای موجود در این دستورات را به عنوان خطای زمان بیلد گزارش میدهد. اما از آنجاکه کل دستورات مربوط به هر رویداد درون یک فایل batch اجرا میشود، امکان گزارش محل دقیق خطای رخداده وجود ندارد. یعنی درصورتیکه مثلا تنها یکی از صدها خط دستور نوشته شده در این قسمت خطا بدهد تنها یک خطا و برای تمام دستورات نمایش داده میشود. البته همانطور که حدس میتوان حدس زد اجرای این دستورات ترنزکشنال نیست و اجرای تمامی دستورات تا قبل از وقوع خطا برگشت ناپذیر خواهند بود. برای نمونه به تصویر زیر و خطای نمایش داده شده دقت کنید:
نمونه اصلاح شده دستور فوق به صورت زیر است:
Copy "$(ProjectDir)$(OutDir)*.*" c:\test
نکته: به دلیل استفاده از کاراکتر فاصله به عنوان جداکننده آرگومانها در دستورات DOS، وجود فاصله در مسیرهای مورد استفاده در این دستورات عملیات را دچار خطا خواهد کرد. راهحل استفاده از کاراکتر " در ابتدا و انتهای رشتههای مربوط به مسیرها همانند دستور بالاست.
نکته: درصورت استفاده از یک فایل bat. برای ذخیره دستورات، امکان استفاده مستقیم از ماکروهای ویژوال استودیو درون آن وجود نخواهد داشت! یکی از راهحلها پاس کردن این متغیرها به صورت پارامتر در زمان فراخوانی فایل bat. است. مثلا:
"$(ProjectDir)postBuild.bat" "$(SolutionPath)"
برای دریافت این پارامترهای پاس شده درون batch فایل باید از عبارات 1% برای پارامتر اول و 2% برای پارامتر دوم و ... تا 9% برای پارامتر نهم است. برای کسب اطلاعات بیشتر به منابع معرفی شده در ابتدای مطلب مخصوصا قسمت Using batch parameters مراجعه کنید.
حال مجموعه دستورات زیر و خطای رخ داده را درنظر بگیرید:
با بررسی مطلب متوجه میشویم با اینکه خط اول مجموعه دستورات فوق درست بوده و کاملا صحیح اجرا میشود اما خطای رخ داده به کل دستورات اشاره دارد و مشخص نشده است که کدام دستور مشکل دارد. دقت کنید که دستور اول کاملا اجرا میشود!
راه حل ساده ای در اینجا برای حل این مشکل ارائه شده است. در این راه حل با استفاده از قابلیتهای این زبان، کل عملیات و مخصوصا خطاهای رخ داده در این مجموعه دستورات هندل میشود تا کنترل بهتری در این مورد بر روی فرایند وجود داشته باشد. نمونه این راه حل به صورت زیر است:
echo --------------------------------------------------------------------------- echo Copy "$(ProjectDir)$(OutDir)*.*" c:\test --Starting... Copy "$(ProjectDir)$(OutDir)*.*" c:\test if errorlevel 1 goto error echo Copy "$(ProjectDir)$(OutDir)*.*" c:\test --DONE! echo --------------------------------------------------------------------------- echo --------------------------------------------------------------------------- echo Copy $(OutDir)*.* c:\test --Starting... Copy $(OutDir)*.* c:\test if errorlevel 1 goto error echo Copy $(OutDir)*.* c:\test --DONE! echo --------------------------------------------------------------------------- goto ok :error echo POSTBUILDSTEP for $(ProjectName) FAILED notepad.exe exit 1 :ok echo POSTBUILDSTEP for $(ProjectName) COMPLETED OK
با استفاده از مجموعه دستوراتی شبیه دستورات بالا میتوان لحظه به لحظه اجرای عملیات را بررسی کرد.
نکته: خروجی تمام این دستورات و نیز خروجی دستورات echo در پنجره Output ویژوال استودیو به همراه سایر پیغامهای بیلد نمایش داده میشود.
نکته: در اسکرپیت فوق برای درک بیشتر مسئله با استفاده از دستور notepad.exe در قسمت error: از وقوع خطا اطمینان حاصل میشود. دقت کنید تا زمانیکه برنامه اجرا شده Notepad بسته نشود فوکس به ویژوال استودیو برنمیگردد و عملیات بیلد تمام نمیشود.
نکته: درصورت استفاده از دستور exit 0 در انتهای قسمت error: (به جای دستور exit 1 موجود) به دلیل اعلام خروج موفق از عملیات، ویژوال استودیو خطایی نمایش نخواهد داد و عملیات بیلد بدون نمایش خطا و با موفقیت به پایان خواهد رسید. درواقع استفاده از هر عددی غیر از صفر به معنی خروج با خطا است که این عدد غیرصفر کد خطا یا error level را مشخص میکند (^ و ^).
یکی از دستورات جالبی که میتوان در این رویدادها از آن استفاده کرد، دستور نصب نسخه ریلیز برنامه در GAC است. نحوه استفاده از آن میتواند به صورت زیر باشد:
if $(ConfigurationName) == Release ( gacutil.exe /i "$(SolutionDir)$(OutDir)$(TargetFileName)" )
نکته: درصورتیکه در دستورات مربوط به رویداد قبل از بیلد یعنی Pre-build خطایی رخ بدهد عملیات بیلد متوقف خواهد شد و برای پروژه فایلی تولید نمیشود. اما اگر این خطا در رویداد بعد از بیلد یعنی Post-build رخ دهد با اینکه ویژوال استودیو وقوع یک خطا را گزارش میدهد اما فایلهای خروجی پروژه حاصله از عملیات بیلد تولید خواهند شد.
نکته: توجه داشته باشید که در استفاه از این ویژگی زیادهروی نباید کرد. استفاده زیاد و بیش از حد (و با تعداد زیاد دستورات) از این رویدادها ممکن است عملیات بیلد را دچار مشکلاتی پیچیده کند. دیباگ این رویدادها و دستورات موجود در آنها بسیار مشکل خواهد بود. اگر تعداد خطوط دستورات موردنظر زیاد باشد بهتر است کل دستورات را درون یک فایل bat. ذخیره کنید و این فایل را بطور جداگانه مدیریت کنید که کار راحتتری است.
نکته: بهتر است قبل از وارد کردن دستورات درون این رویدادها، ابتدا تمام دستورات را در یک پنجره cmd آزمایش کنید تا از درستی ساختار و نتیجه آنها مطمئن شوید.
رویدادهای بیلد و MSBuild
همانطور که در اینجا توضیح داده شده است، ویژوال استودیو از ابزار MSBuild برای تولید اپلیکیشنها استفاده میکند. عملیات مدیریت رویدادهای بیلد نیز توسط این ابزار انجام میشود. اگر به فایل پروژه مربوط به مثال قبل مراجعه کنید به محتوایی شبیه خطوط زیر میرسید:
... <PropertyGroup> <PostBuildEvent>echo --------------------------------------------------------------------------- echo Copy "$(ProjectDir)$(OutDir)*.*" c:\test --Starting... Copy "$(ProjectDir)$(OutDir)*.*" c:\test if errorlevel 1 goto error echo Copy "$(ProjectDir)$(OutDir)*.*" c:\test --DONE! echo --------------------------------------------------------------------------- echo --------------------------------------------------------------------------- echo Copy $(OutDir)*.* c:\test --Starting... Copy $(OutDir)*.* c:\test if errorlevel 1 goto error echo Copy $(OutDir)*.* c:\test --DONE! echo --------------------------------------------------------------------------- goto ok :error echo POSTBUILDSTEP for $(ProjectName) FAILED notepad.exe exit 1 :ok echo POSTBUILDSTEP for $(ProjectName) COMPLETED OK</PostBuildEvent> </PropertyGroup> ...
منابع برای مطالعه بیشتر:
نظرات مطالب
WF:Windows Workflow #۶
می توانید پروژه wf را به صورت WCF WorkFlow Service Application در Solution مورد نظر اضافه کنید پس از ان سرویس را بر روی ویندوز سرور هاست کنید به کمک برنامه AppFabric که میتوانید ان را از لینک زیر دانلود کنید .
روش دیگر این است که شما مستقیما از کلاسهای WF در پروژه خود استفاده کنید و Activityهای خود را تولید کنید بدون اینکه احتیاج به Model Designer داشته باشید مانند کد زیر:
namespace LeadGenerator { public sealed class CreateLead : CodeActivity { public InArgument<string> ContactName { get; set; } public InArgument<string> ContactPhone { get; set; } public InArgument<string> Interests { get; set; } public InArgument<string> Notes { get; set; } public InArgument<string> ConnectionString { get; set; } public OutArgument<Lead> Lead { get; set; } protected override void Execute(CodeActivityContext context) { // Create a Lead class and populate it with the input arguments Lead l = new Lead(); l.ContactName = ContactName.Get(context); l.ContactPhone = ContactPhone.Get(context); l.Interests = Interests.Get(context); l.Comments = Notes.Get(context); l.WorkflowID = context.WorkflowInstanceId; l.Status = "Open"; // Insert a record into the Lead table LeadDataDataContext dc = new LeadDataDataContext(ConnectionString.Get(context)); dc.Leads.InsertOnSubmit(l); dc.SubmitChanges(); // Store the request in the OutArgument Lead.Set(context, l); } } }
فرض کنید قصد دارید عملیات نرمال سازی اطلاعات را بر روی یک رشته انجام داده و برای مثال اعداد فارسی و انگلیسی موجود در یک رشته را یکدست کنید. اولین روشی که برای اینکار به ذهن میرسد، استفاده از متد Replace است:
اما آیا این روش، کارآیی مناسبی را به همراه دارد؟ در ادامه چند روش دیگر را نیز جهت جایگزین کردن حروف، معرفی کرده و کارآیی آنها را با هم مقایسه میکنیم.
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
خواهیم داشت:
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
در کدهای فوق، ابتدا طول رشتهی نهایی بازگشتی از string.Create مشخص میشود. سپس توسط پارامتر دوم، دادههایی که قرار است بر روی آنها کاری صورت گیرد به متد string.Create ارسال میشوند. در آخر عملیات نهایی در action delegate تعریف شده رخ میدهد. در اینجا chars، به بافر درونی رشتهای که بازگشت داده میشود، اشاره میکند و باید پر شود (این بافر مستقیما در دسترس است). context همان پارامتر دوم متد string.Create است.
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
مزیت این متد، عدم نیاز به یک پیشبافر است؛ به این معنا که مستقیما بر روی قسمتی از حافظه کار میکند که ارجاعی را به رشتهی «بازگشتی» دارد. یعنی در حالت کار با string.Create، غیرقابل تغییر بودن رشتهها در دات نت دیگر صادق نخواهد بود و برای تغییر آن نیازی به تخصیص بافر، کپی کردن و تخصیص حافظهی نهایی برای بازگشت نتیجه نیست. پارامتر SpanAction آن، امکان دسترسی مستقیم به این ناحیهی از حافظه را میسر میکند.
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
در اینجا اگر در داخل action delegate، ارجاعی را به data داشته باشیم، یک closure تشکیل میشود و در این حالت کامپایلر برای مدیریت آن، نیاز به تولید یک کلاس را در پشت صحنه خواهد داشت که خودش سبب کاهش کارآیی میگردد. به همین جهت متد Create، پارامتر state را به صورت معمولی دریافت میکند و آنرا توسط context در اختیار delegate قرار میدهد تا نیازی نباشد delegate تعریف شده، یک closure را تشکیل دهد.
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); }
جایگزین کردن حروف با استفاده از Replace معمولی توسط رشتهها
نگارش اصلی تبدیل تمام اعداد موجود در یک رشته به اعداد فارسی، به صورت زیر است که در آن یک دست سازی اعداد عربی هم درنظر گرفته شدهاند (برای مثال طرز نگارش عدد 4 فارسی و عربی متفاوت است):
private static string toPersianNumbersUsingReplace(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return toEnglishNumbers(data) .Replace("0", "\u06F0") .Replace("1", "\u06F1") .Replace("2", "\u06F2") .Replace("3", "\u06F3") .Replace("4", "\u06F4") .Replace("5", "\u06F5") .Replace("6", "\u06F6") .Replace("7", "\u06F7") .Replace("8", "\u06F8") .Replace("9", "\u06F9"); } private static string toEnglishNumbers(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return data.Replace("\u0660", "0") //٠ .Replace("\u06F0", "0") //۰ .Replace("\u0661", "1") //١ .Replace("\u06F1", "1") //۱ .Replace("\u0662", "2") //٢ .Replace("\u06F2", "2") //۲ .Replace("\u0663", "3") //٣ .Replace("\u06F3", "3") //۳ .Replace("\u0664", "4") //٤ .Replace("\u06F4", "4") //۴ .Replace("\u0665", "5") //٥ .Replace("\u06F5", "5") //۵ .Replace("\u0666", "6") //٦ .Replace("\u06F6", "6") //۶ .Replace("\u0667", "7") //٧ .Replace("\u06F7", "7") //۷ .Replace("\u0668", "8") //٨ .Replace("\u06F8", "8") //۸ .Replace("\u0669", "9") //٩ .Replace("\u06F9", "9"); //۹ }
جایگزین کردن حروف با استفاده از Replace معمولی توسط کاراکترها
اینبار همان حالت قبل را درنظر بگیرید؛ با این تفاوت که بجای رشتهها از کاراکترها استفاده شود. برای مثال بجای:
.Replace("\u0669", "9") //٩
.Replace('\u0669', '9') //٩
جایگزین کردن حروف با استفاده از String Builder
در ادامه بجای استفاده از متد Replace متداول، آرایهای از حروف قابل جایگزینی را توسط یک StringBuilder ایجاد کرده و حروف را یکی یکی تبدیل میکنیم و به این ترتیب برخلاف متد Replace، هربار برای جایگزینی یک مورد خاص، مجددا از ابتدای رشته شروع به جستجو نمیشود:
private static string toPersianNumbersUsingStringBuilder(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var strBuilder = new StringBuilder(data); for (var i = 0; i < strBuilder.Length; i++) { switch (strBuilder[i]) { case '0': case '\u0660': strBuilder[i] = '\u06F0'; break; case '1': case '\u0661': strBuilder[i] = '\u06F1'; break; case '2': case '\u0662': strBuilder[i] = '\u06F2'; break; case '3': case '\u0663': strBuilder[i] = '\u06F3'; break; case '4': case '\u0664': strBuilder[i] = '\u06F4'; break; case '5': case '\u0665': strBuilder[i] = '\u06F5'; break; case '6': case '\u0666': strBuilder[i] = '\u06F6'; break; case '7': case '\u0667': strBuilder[i] = '\u06F7'; break; case '8': case '\u0668': strBuilder[i] = '\u06F8'; break; case '9': case '\u0669': strBuilder[i] = '\u06F9'; break; default: strBuilder[i] = strBuilder[i]; break; } } return strBuilder.ToString(); }
جایگزین کردن حروف با استفاده از ToCharArray
متد زیر دقیقا شبیه به حالت استفاده از String Builder است؛ با یک تفاوت مهم: بجای استفاده از String Builder برای تهیهی آرایهای از حروف قابل تغییر، از متد ToCharArray استفاده شدهاست:
private static string toPersianNumbersUsingToCharArray(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; var letters = data.ToCharArray(); for (var i = 0; i < letters.Length; i++) { switch (letters[i]) { case '0': case '\u0660': letters[i] = '\u06F0'; break; // مانند قبل } } return new string(letters); }
جایگزین کردن حروف با استفاده از string.Create
string.Create یکی از تازههای NET Core. است که امکان تغییر مستقیم یک قطعه string را میسر میکند:
private static string toPersianNumbersUsingStringCreate(string data) { if (string.IsNullOrWhiteSpace(data)) return string.Empty; return string.Create(data.Length, data, (chars, context) => { for (var i = 0; i < data.Length; i++) { switch (context[i]) { case '0': case '\u0660': chars[i] = '\u06F0'; break; // مانند قبل } } }); }
توضیحات بیشتر:
در دات نت، رشتهها نوعهای ارجاعی (reference type) غیرقابل تغییر (immutable) هستند. به این معنا که هر زمانیکه ایجاد شدند، دیگر نمیتوان محتوای آنها را تغییر داد. به همین جهت است که مجبور هستیم آنها را برای مثال توسط ToCharArray به یک آرایه تبدیل کنیم و سپس این آرایهی قابل تغییر را ویرایش کنیم. در حین کار با رشتهها، این غیرقابل تغییر بودن، سبب تخصیص حافظههای بیش از حدی میشوند. اگر بخواهیم قسمتی از یک رشته را جدا و یا جایگزین کنیم و یا تعدادی رشته را با هم جمع بزنیم، نتیجهی آن نیاز به یک تخصیص حافظهی جدید را دارد. راه حل استاندارد مواجه شدن با این مشکل، استفاده از StringBuilder است که از یک بافر داخلی برای انجام کارهای خودش استفاده میکند و زمانیکه نتیجهی نهایی را از آن درخواست میکنیم، تخصیص حافظهای را برای تولید رشتهی حاصل انجام میدهد. البته این مورد نیاز به اندازه گیری دارد و ارزش StringBuilder با حجم بالایی از اطلاعات متنی مشخص میشود؛ وگرنه همانطور که مشاهده میکنید (در نتیجهی نهایی بحث در ادامه)، الزاما کدهای سریعتری را به همراه نخواهد داشت.
هدف از string.Create، ایجاد رشتهها از دادههای موجود است. هدف اصلی آن کاهش تخصیصهای حافظه و کپی کردن اطلاعات است و امضای آن به صورت زیر میباشد:
public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);
هنگام کار با این متد، chars ای که در اختیار ما قرار میگیرد، یک <Span<char اشاره کننده به رشتهی نهایی است که قرار است بازگشت داده شود (در ابتدای کار بر اساس اندازهای که مشخص میشود، یک رشتهی خالی تخصیص داده میشود، اما بافر پر کردن آن اینبار در دسترس است و نیازی به تخصیص و کپی جداگانهای را ندارد). بنابراین روش کار با این متد، پر کردن بافر درونی رشتهی بازگشتی (همان chars در اینجا) به صورت مستقیم است؛ کاری که با یک رشتهی معمولی نمیتوان انجام داد.
State یا همان پارامتر دوم این متد، هر چیزی میتواند باشد. اگر نیاز است چندین رشته را در اینجا دریافت کنید تا بتوان بر اساس آن رشتهی نهایی را تشکیل داد، یک struct را تعریف کرده و بجای state به آن ارسال کنید. سپس این state توسط پارامتر context مربوط به SpanAction<char, string> action قابل دریافت و استفادهاست که در این مثال، context همان data ارسالی به این متد است.
سؤال: در حین کار با string.Create، باید از پارامتر data استفاده کنیم و یا از context دریافتی؟ به نظر در مثال فوق، data و context یکی هستند. اکنون داخل action delegate مهیا که جهت ساخت رشتهی نهایی بکار میرود، باید از data استفاده کرد و یا از context؟
return string.Create(data.Length, data, (chars, context) => {});
نتیجهی نهایی بررسی کارآیی روشهای مختلف جایگزین کردن حروف در یک رشته
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: ReplacePerformanceTests.zip
ستون op/s در اینجا، مهمترین ستون گزارش است و به معنای تعداد عملیات قابل انجام در یک ثانیه است. از 670 هزار عملیات در ثانیه با Replace معمولی، به 5 میلیون عملیات در ثانیه رسیدهایم که بسیار قابل توجهاست.
همانطور که مشاهده میکنید، string.Create، سریعترین نگارش موجود است. در این بین نگارشی که از ToCharArray استفاده میکند، قابلیت انتقال بیشتری را دارد؛ از این جهت که نگارشهای دیگر NET. هنوز دسترسی به string.Create را ندارند. همچنین نگارش کاراکتری متد Replace، از متد رشتهای آن سریعتر عمل کردهاست.