به روز رسانیهای خوشبینانهی UI
پیاده سازی اعمال CRUD توسط Axios در قسمت قبل، به همراه یک مشکل مهم است: اعمال کار با شبکه و سرور، زمانبر هستند و مدتی طول میکشد تا پاسخ عملیات از سمت سرور دریافت شود. در این بین اگر خطایی رخ دهد، مابقی کدهای نوشته شدهی در متدهایی مانند Update و Delete، اجرا نمیشوند. به این حالت «به روز رسانی بدبینانهی UI» گفته میشود. در حالت خوشبینانه، فرض بر این است که در اکثر موارد، فراخوانی سرور با موفقیت به پایان میرسد. در یک چنین حالتی، ابتدا UI به روز رسانی میشود و سپس فراخوانیهای سمت سرور صورت میگیرند. اگر این فراخوانی با شکست مواجه شد، مجددا UI را به حالت قبلی آن باز میگردانیم:
handleDelete = async post => { const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); await axios.delete(`${apiEndpoint}/${post.id}`); };
اما اگر در این بین خطایی رخ داد، چه باید کرد؟ باید آخرین تغییر انجام شده را به حالت اول باز گرداند. انجام یک چنین کاری در React سادهاست. چون ما state را به صورت مستقیم ویرایش نمیکنیم، همیشه میتوان ارجاعی را به state قبلی، ذخیره و سپس در صورت نیاز آنرا بازیابی کرد:
handleDelete = async post => { const originalPosts = this.state.posts; const posts = this.state.posts.filter(item => item.id !== post.id); this.setState({ posts }); // Optimistic Update try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { alert("An error occurred when deleting a post!"); this.setState({ posts: originalPosts }); // Undo changes } };
مدیریت خطاهای رخ دادهی در حین فراخوانی سرور
تا اینجا مشاهده کردیم که یک روش مدیریت خطاها در کدهای Axios، قرار دادن آنها در یک قطعه کد try/catch است. در اینجا نیز باید بتوان بین خطاهای پیش بینی شده و نشده، تفاوت قائل شد.
- خطاهای پیش بینی شده: برای مثال اگر درخواست حذف رکوردی را دادیم که در بانک اطلاعاتی موجود نیست، انتظار داریم سرور، خطای 404 یا return NotFound را بازگشت دهد و یا 400 که معادل bad request است و در حالت ارسال دادههایی غیرمعتبر، رخ میدهد. در این موارد بهتر است خطاهایی خاص را به کاربران نمایش داد؛ برای مثال رکورد درخواستی وجود ندارد یا پیشتر حذف شدهاست.
- خطاهای پیش بینی نشده: این نوع خطاها نباید و یا قرار نیست در شرایط عادی رخ دهند. برای مثال اگر شبکه در دسترس نیست، امکان ارتباط با سرور نیز میسر نخواهد بود و یا حتی ممکن است خطایی در کدهای سمت سرور، سبب بروز خطایی شده باشد. این نوع خطاها ابتدا باید لاگ شوند تا با بررسیهای آتی آنها، بتوان مشکلات پیش بینی نشده را بهتر برطرف کرد. همچنین در یک چنین مواردی، باید یک پیام خطای خیلی عمومی را به کاربر نمایش داد؛ برای مثال «یک خطای پیش بینی نشده رخ دادهاست.».
برای مدیریت این دو حالت باید به جزئیات شیء ex، در بدنهی catch، دقت کرد که دارای دو خاصیت request و response است. اگر ex.response تنظیم شده بود، یعنی دریافت خروجی از سرور موفقیت آمیز بودهاست. اگر سرور در دسترس نباشد و یا برنامهی سمت سرور کرش کرده باشد، ex.response نال خواهد بود. اگر ex.request نال نبود، یعنی ارسال درخواست به سمت سرور با موفقیت انجام شدهاست. برای مثال جهت بررسی خطای مورد انتظار 404، میتوان در قسمت catch(ex) به صورت زیر عمل کرد:
try { await axios.delete(`${apiEndpoint}/${post.id}`); } catch (ex) { if (ex.response && ex.response.status === 404) { alert("This post has already been deleted!"); } else { console.log("Error", ex); alert("An unexpected error occurred when deleting a post!"); } this.setState({ posts: originalPosts }); // Undo changes }
عموما خطاهای پیشبینی شده را لاگ نمیکنیم؛ چون ممکن است کاربر، یک صفحه را در چندین برگه باز کرده باشد و در یکی، رکوردی را حذف کند. در این حال، این رکورد هنوز در برگههای دیگر موجود است و اگر مجددا درخواست حذف آنرا صادر کند، مشکل خاصی از دیدگاه برنامه رخ ندادهاست و نیازی به پیگیریهای آتی را ندارد. یعنی صرفا یک client error است.
مدیریت سراسری خطاهای رخ دادهی در حین فراخوانی سرور
برای مدیریت خطاها، نیاز است یک چنین try/catchهایی را در تمام قسمتهای برنامه که با سرور کار میکنند، قرار دهیم. برای کاهش این کدهای تکراری، از interceptors کتابخانهی Axios استفاده میشود. در این کتابخانه میتوان در جاهائیکه درخواستی به سمت سرور ارسال میشود و یا پاسخی از سمت سرور دریافت میشود، قطعه کدهایی سراسری را قرار داد و بر روی درخواست و یا پاسخ، تغییراتی را اعمال کرد و یا حتی اطلاعات مربوطه را لاگ کرد؛ به این نوع قطعه کدها، interceptor گفته میشود و برای تعریف آنها میتوان از axios.interceptors.request و یا axios.interceptors.response، خارج از کلاس جاری استفاده کرد. برای مثال بر روی شیء axios.interceptors.response، میتوان متد use را فراخوانی کرد که دو پارامتر را که هر کدام یک callback function هستند، میپذیرد. اولی در صورت موفقیت آمیز بودن response فراخوانی میشود و دومی در صورت شکست آن. اگر نیازی به هر کدام نبود، میتوان آنرا به null مقدار دهی کرد. اگر مدیریت قسمت شکست علمیات مدنظر است، نیاز خواهد بود در پایان این callback function، یک Rejected Promise را بازگشت داد تا ادامهی برنامه، به درستی مدیریت شود. در این حالت اگر خطایی رخ دهد، ابتدا این interceptor فراخوانی میشود و سپس کنترل به بدنهی catch منتقل خواهد شد:
import "./App.css"; import axios from "axios"; import React, { Component } from "react"; axios.interceptors.response.use(null, error => { console.log("interceptor called."); return Promise.reject(error); }); const apiEndpoint = "https://localhost:5001/api/posts"; class App extends Component {
axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); });
یک نکته: استفاده از try/catchها فقط برای بازگشت UI به حالت قبلی و یا نمایش خطایی خاص به کاربر توصیه میشوند. اگر از روش «به روز رسانیهای خوشبینانهی UI» استفاده نمیکنید و همچنین خطاهای ویژهای بجز خطای عمومی لاگ شدهی در interceptor فوق مدنظر شما نیست، نیازی هم به try/catch نخواهد بود و پس از بروز خطا، قسمتهای بعدی کد اجرا نمیشوند؛ اما خطای عمومی فوق نمایش داده خواهد شد.
ایجاد یک HTTP Service با قابلیت استفادهی مجدد
تا اینجا تعریف interceptor را پیش از کلاس کامپوننت جاری قرار دادهایم که هم سبب شلوغی این ماژول شدهاست و هم در صورت نیاز به آن در سایر برنامهها، باید همین قطعه کد را مجددا در آنها کپی کرد. به همین جهت پوشهی جدید src\services را ایجاد کرده و سپس فایل src\services\httpService.js را در آن با محتوای زیر ایجاد میکنیم:
import axios from "axios"; axios.interceptors.response.use(null, error => { const expectedError = error.response && error.response.status >= 400 && error.response.status < 500; if (!expectedError) { console.log("Error", error); alert("An unexpected error occurred when deleting a post!"); } return Promise.reject(error); }); export default { get: axios.get, post: axios.post, put: axios.put, delete: axios.delete };
سپس به app.js مراجعه کرده و این ماژول را با یک نام دلخواه import میکنیم:
import http from "./services/httpService";
ایجاد یک ماژول Config
بهبود دیگری را که میتوانیم اعمال کنیم، انتقال const apiEndpoint تعریف شده، به یک ماژول مجزا است؛ تا اگر نیاز به استفادهی از آن در قسمتهای دیگری نیز وجود داشت، به سادگی بتوان آنرا مدیریت کرد. به همین جهت فایل جدید src\config.json را با محتوای زیر ایجاد میکنیم:
{ "apiEndpoint" : "https://localhost:5001/api/posts" }
import config from "./config.json";
نمایش بهتر خطاها به کاربر توسط کتابخانهی react-toastify
بجای alert توکار مرورگرها، میتوان یک صفحهی دیالوگ زیباتر را برای نمایش خطاها درنظر گرفت. به همین جهت ابتدا کتابخانهی react-toastify را نصب میکنیم:
> npm i react-toastify --save
import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css";
render() { return ( <React.Fragment> <ToastContainer />
import { toast } from "react-toastify"; // ... axios.interceptors.response.use(null, error => { // ... if (!expectedError) { // ... toast.error("An unexpected error occurrred."); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-22-backend-part-03.zip و sample-22-frontend-part-03.zip
1. Attribute Routing
Config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{Controller}/{id}", defaults: new { id = RouteParameter.Optional } );
URI Pattern --> books/1/authors [Route("books/{bookId}/authors")] public IEnumerable<Author> GetAuthorByBook(int bookId) { ..... }
2. CORS - Cross Origin Resource Sharing
3. OWIN (Open Web Interface for .NET) self-hosting
OWIN یک اینترفیس استاندارد بین سرورهای دات نت و اپلیکیشنهای وب تعریف میکند. هدف این اینترفیس جداسازی (decoupling) سرور و اپلیکیشن است. تشویق به توسعه ماژولهای ساده برای توسعه اپلیکیشنهای وب دات نت. و بعنوان یک استاندارد باز (open standard) اکوسیستم نرم افزارهای متن باز را تحریک کند تا ابزار توسعه اپلیکیشنهای وب دات نت توسعه یابند.
4. IHttpActionResult
5. Web API OData
- expand$: بصورت نرمال، هنگام کوئری گرفتن از یک کالکشن OData، پاسخ سرور موجودیتهای مرتبط (related entities) را شامل نمیشود. با استفاده از expand$ میتوانیم موجودیتهای مرتبط را بصورت inline در پاسخ سرور دریافت کنیم.
- select$: از این متد برای انتخاب چند خاصیت بخصوص از پاسخ سرور استفاده میشود، بجای آنکه تمام خاصیتها بارگذاری شوند.
- value$: با این متد مقدار خام (raw) فیلدها را بدست میآورید، بجای دریافت آنها در فرمت OData.
چند مقاله خوب دیگر
نحوه استفاده از ViewModel در ASP.NET MVC
public class ResumeViewModel { public ResumeViewModel() { } public ResumeViewModel(IEnumerable<Resume> resum, IEnumerable<Work_Experience_Job_Seeker> workExperienceJobSeekerOfViewModel, IEnumerable<Job_Expertises> jobExpertisesOfViewModel, IEnumerable<Degrees_Work_Experience_Required> degreesWorkExperienceRequiredOfViewModel, IEnumerable<Specialized_Course> specializedCourseOfViewModel, IEnumerable<Book_Published> bookPublishedOfViewModel, IEnumerable<Basic_Table> basicTable) { ResumeOfViewModel = resum; WorkExperienceJobSeekerOfViewModel = workExperienceJobSeekerOfViewModel; JobExpertisesOfViewModel = jobExpertisesOfViewModel; DegreesWorkExperienceRequiredOfViewModel = degreesWorkExperienceRequiredOfViewModel; SpecializedCourseOfViewModel = specializedCourseOfViewModel; BookPublishedOfViewModel = bookPublishedOfViewModel; BasicTable = basicTable; } public IEnumerable<Resume> ResumeOfViewModel { get; set; } public IEnumerable<Work_Experience_Job_Seeker> WorkExperienceJobSeekerOfViewModel { get; set; } public IEnumerable<Job_Expertises> JobExpertisesOfViewModel { get; set; } public IEnumerable<Degrees_Work_Experience_Required> DegreesWorkExperienceRequiredOfViewModel { get; set; } public IEnumerable<Specialized_Course> SpecializedCourseOfViewModel { get; set; } public IEnumerable<Book_Published> BookPublishedOfViewModel { get; set; } public IEnumerable<Basic_Table> BasicTable { get; set; } public int NumberForm { get; set; } // EditResumes.chtml & ShowResumes.chtml ===> baraye select kardan formha :D }
[HttpGet] public ActionResult EditResumes(int id) { var contex = new Final_My_ProjectEntities2(); var res1 = contex.Resumes.Where(rec => rec.Resume_ID == id); var res2 = contex.Work_Experience_Job_Seeker.Where(rec => rec.Resume_ID == id).ToList(); var res3 = contex.Job_Expertises.Where(rec => rec.Resume_ID == id).ToList(); var res4 = contex.Degrees_Work_Experience_Required.Where(rec => rec.Resume_ID == id).ToList(); var res5 = contex.Specialized_Course.Where(rec => rec.Resume_ID == id).ToList(); var res6 = contex.Book_Published.Where(rec => rec.Resume_ID == id).ToList(); var res12 = contex.Basic_Table.ToList(); var viewModel = new ResumeViewModel(res1, res2, res3, res4, res5, res6,res12); var items = new SelectList( new[] { new {Value = "1", Text = "فرم مهارت ها"}, new {Value = "2", Text = "فرم کتاب/مقالات منتشر شده"}, new {Value = "3", Text = "فرم سابقه کاری"}, new {Value = "4", Text = "فرم دورههای تخصصی گذرانده"}, new {Value = "5", Text = "فرم تخصصهای شغلی"}, new {Value = "6", Text = "فرم مدارک تحصیلی"} }, "Value", "Text"); ViewBag.Form = new SelectList(items, "Value", "Text"); var res7 = contex.Basic_Table.Where(rec => rec.Domain == "MilitaryStatus").ToList(); ViewBag.MilitaryStatus = new SelectList(res7, "Value", "Meaning",res1); var res8 = contex.Basic_Table.Where(rec => rec.Domain == "Sex"); ViewBag.Sex = new SelectList(res8, "Value", "Meaning", res1); var res9 = contex.Basic_Table.Where(rec => rec.Domain == "MartialStatus").ToList(); ViewBag.MartialStatus = new SelectList(res9, "Value", "Meaning", res1); var res10 = contex.Basic_Table.Where(rec => rec.Domain == "Degree").ToList(); ViewBag.Degree = new SelectList(res10, "Value", "Meaning"); var res11 = contex.Basic_Table.Where(rec => rec.Domain == "Ability").ToList(); ViewBag.Ability = new SelectList(res11, "Value", "Meaning"); return View(viewModel); }
@model Final_My_Project.ViewModels.ResumeViewModel @{ ViewBag.Title = "ویرایش رزومه"; ViewBag.PartOne = "فرم مهارت ها"; ViewBag.PartTwo = "فرم کتاب/مقالات منتشر شده"; ViewBag.Part3 = "فرم سابقه کاری"; ViewBag.Part4 = "فرم دورههای تخصصی گذرانده"; ViewBag.Part5 = "فرم تخصصهای شغلی"; ViewBag.Part6 = "فرم مدارک تحصیلی"; } <h2 style="font-family: Arial;">@ViewBag.Title</h2><br/> <script type="text/javascript"> $(function () { $('#Gender').change(function () { var selectKind = $(this).find('option:selected').text(); var divMilitary; if (selectKind == "زن") { divMilitary = $('#Military'); divMilitary.hide(); divMilitary.css('display', 'none'); } else if (selectKind == "مرد") { divMilitary = $('#Military'); divMilitary.show(); divMilitary.css('display', 'block'); } }); }); </script> <script type="text/javascript"> $(function () { $('#SelectForm').change(function () { var selectFrom = $(this).find('option:selected').text(); if (selectFrom == "فرم مهارت ها") { $('#PartOne').show(); $('#PartOne').css('display', 'block'); $('#PartTwo').hide(); $('#PartTwo').css('display', 'none'); $('#Part3').hide(); $('#Part3').css('display', 'none'); $('#Part4').hide(); $('#Part4').css('display', 'none'); $('#Part5').hide(); $('#Part5').css('display', 'none'); $('#Part6').hide(); $('#Part6').css('display', 'none'); } if (selectFrom == "فرم کتاب/مقالات منتشر شده") { $('#PartTwo').show(); $('#PartTwo').css('display', 'block'); $('#PartOne').show(); $('#PartOne').css('display', 'none'); $('#Part3').hide(); $('#Part3').css('display', 'none'); $('#Part4').hide(); $('#Part4').css('display', 'none'); $('#Part5').hide(); $('#Part5').css('display', 'none'); $('#Part6').hide(); $('#Part6').css('display', 'none'); } if (selectFrom == "فرم سابقه کاری") { $('#Part3').show(); $('#Part3').css('display', 'block'); $('#PartTwo').hide(); $('#PartTwo').css('display', 'none'); $('#PartOne').show(); $('#PartOne').css('display', 'none'); $('#Part4').hide(); $('#Part4').css('display', 'none'); $('#Part5').hide(); $('#Part5').css('display', 'none'); $('#Part6').hide(); $('#Part6').css('display', 'none'); } if (selectFrom == "فرم دورههای تخصصی گذرانده") { $('#Part4').show(); $('#Part4').css('display', 'block'); $('#PartTwo').hide(); $('#PartTwo').css('display', 'none'); $('#PartOne').show(); $('#PartOne').css('display', 'none'); $('#Part3').hide(); $('#Part3').css('display', 'none'); $('#Part5').hide(); $('#Part5').css('display', 'none'); $('#Part6').hide(); $('#Part6').css('display', 'none'); } if (selectFrom == "فرم تخصصهای شغلی") { $('#Part5').show(); $('#Part5').css('display', 'block'); $('#PartTwo').hide(); $('#PartTwo').css('display', 'none'); $('#PartOne').show(); $('#PartOne').css('display', 'none'); $('#Part3').hide(); $('#Part3').css('display', 'none'); $('#Part4').hide(); $('#Part4').css('display', 'none'); $('#Part6').hide(); $('#Part6').css('display', 'none'); } if (selectFrom == "فرم مدارک تحصیلی") { $('#Part6').show(); $('#Part6').css('display', 'block'); $('#Part5').show(); $('#Part5').css('display', 'none'); $('#PartTwo').hide(); $('#PartTwo').css('display', 'none'); $('#PartOne').show(); $('#PartOne').css('display', 'none'); $('#Part3').hide(); $('#Part3').css('display', 'none'); $('#Part4').hide(); $('#Part4').css('display', 'none'); } }); }); </script> @Html.DropDownListFor(m=>m.NumberForm, (SelectList)ViewBag.Form, new { id = "SelectForm" }) @using (Html.BeginForm()) { @Html.ValidationSummary(true) <div id="PartOne" > <h3 style="font-family: Arial; color: #008080; font-weight: bold; ">@ViewBag.PartOne</h3><br/> @foreach (var item in Model.ResumeOfViewModel) { <table dir="rtl"> <tr> <td> @Html.Label("عنوان رزومه") </td> <td> @Html.TextBoxFor(model =>item.Title_Of_Resume , new {@class = "text", style = "width:100 px"}) @Html.ValidationMessageFor(model => item.Title_Of_Resume) </td> </tr> <tr> <td> <div id="Gender" > @Html.Label("نوع جنسیت") @Html.DropDownList("نوع جنسیت", new SelectList(ViewBag.Sex, "Value", "Text", item.Sex_ID == 0 ? 0 : item.Sex_ID)) @Html.ValidationMessageFor(model => item.Sex_ID) </div> </td> <td> <div > @Html.Label("وضعیت تاهل") @Html.DropDownList("وضعیت تاهل", new SelectList(ViewBag.MartialStatus, "Value", "Text", item.Martial_Status_ID == 0 ? 0 : item.Martial_Status_ID)) @Html.ValidationMessageFor(model => item.Martial_Status_ID) </div> </td> </tr> <tr id="Military" style="display: none;"> <td> @Html.Label("وضعیت نظام وظیفه") </td> <td> @Html.DropDownList("وضعیت نظام وظیفه", new SelectList(ViewBag.MilitaryStatus, "Value", "Text", item.Military_Status_ID == 0 ? 0 : item.Military_Status_ID), new { id = "Gender" }) @Html.ValidationMessageFor(model => item.Military_Status_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با رایانه") </td> <td> @Html.DropDownListFor(model => item.Knowledge_Of_Computers_ID, (SelectList)ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Of_Computers_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با امور اداری و دفتری") </td> <td> @Html.DropDownListFor(model => item.Knowledge_Administrative_and_Clerical_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Administrative_and_Clerical_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با زبان انگلیسی") </td> <td> @Html.DropDownListFor(model => item.Knowledge_Of_English_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Of_English_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با زبان عربی") </td> <td> @Html.DropDownListFor(model => item.Knowledge_Of_Arabic_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Of_Arabic_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با ماکروسافت آفیس") </td> <td> @Html.DropDownListFor(model =>item.Knowledge_Of_Office_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Of_Office_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با امور مالی و حسابداری") </td> <td> @Html.DropDownListFor(model =>item.Knowledge_Of_Finance_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model => item.Knowledge_Of_Finance_ID) </td> </tr> <tr> <td> @Html.Label("آشنایی با مدیریت") </td> <td> @Html.DropDownListFor(model =>item.Knowledge_Of_Manage_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Knowledge_Of_Manage_ID) </td> </tr> <tr> <td> @Html.Label("گواهینامه رانندگی پایه یک") </td> <td> @Html.DropDownListFor(model =>item.Driving_license_One_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Driving_license_One_ID) </td> </tr> <tr> <td> @Html.Label("گواهینامه رانندگی پایه دو") </td> <td> @Html.DropDownListFor(model =>item.Driving_license_Two_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Driving_license_Two_ID) </td> </tr> <tr> <td> @Html.Label("گواهینامه رانندگی پایه موتورسیکلت") </td> <td> @Html.DropDownListFor(model =>item.Certificate_Motor_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Certificate_Motor_ID) </td> </tr> <tr> <td> @Html.Label("ماشین شخصی") </td> <td> @Html.DropDownListFor(model =>item.Personal_Vehicle_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Personal_Vehicle_ID) </td> </tr> <tr> <td> @Html.Label("روابط عمومی") </td> <td> @Html.DropDownListFor(model =>item.Public_Relationship_ID, (SelectList) ViewBag.Ability) @Html.ValidationMessageFor(model =>item.Public_Relationship_ID) </td> </tr> <tr> <td> @Html.Label("دیگر توانایی ها") </td> <td> @Html.EditorFor(model =>item.Etc_Ability) @Html.ValidationMessageFor(model =>item.Etc_Ability) </td> </tr> </table> } </div> <p> <input type="submit" value="Save" onclick="return confirm('از ثبت اطلاعات مطمئن هستید؟')" /> </p> } <div> @Html.ActionLink("بازگشت به مدیریت رزومه ها", "ManageOfResumes") </div> @section scripts { @Scripts.Render("~/bundles/jqueryval") }
[HttpPost] public ActionResult EditResumes(ResumeViewModel model) // model null mishe ! CHERAA??! { var contex = new Final_My_ProjectEntities2(); try { if (ModelState.IsValid) { // Code... che cody? contex.SaveChanges(); } } catch (Exception) { ViewBag.wrong = "لطفا دادههای ورودی را بررسی نمایید"; } return View(model); }
Blazor 5x - قسمت 23 - احراز هویت و اعتبارسنجی کاربران Blazor Server - بخش 3 - کار با نقشهای کاربران
کار با Authentication State از طریق کدنویسی
فرض کنید در کامپوننت HotelRoomUpsert.razor نمیخواهیم دسترسیها را به کمک اعمال ویژگی attribute [Authorize]@ محدود کنیم؛ میخواهیم اینکار را از طریق کدنویسی مستقیم انجام دهیم:
// ... @*@attribute [Authorize]*@ @code { [CascadingParameter] public Task<AuthenticationState> AuthenticationState { get; set; } protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); } // ...
- سپس یک پارامتر ویژه را از نوع CascadingParameter، به نام AuthenticationState تعریف کردیم. این خاصیت از طریق کامپوننت CascadingAuthenticationState که در قسمت قبل به فایل BlazorServer.App\App.razor اضافه کردیم، تامین میشود.
- در آخر در روال رویدادگردان OnInitializedAsync، بر اساس آن میتوان به اطلاعات User جاری وارد شدهی به سیستم دسترسی یافت و برای مثال اگر اعتبارسنجی نشده بود، با استفاده از NavigationManager، او را به صفحهی لاگین هدایت میکنیم.
- در اینجا روش ارسال آدرس صفحهی فعلی را نیز مشاهده میکنید. این امر سبب میشود تا پس از لاگین، کاربر مجددا به همین صفحه هدایت شود.
authenticationState، امکانات بیشتری را نیز در اختیار ما قرار میدهد؛ برای مثال با استفاده از متد ()authenticationState.User.IsInRole آن میتوان دسترسی به قسمتی را بر اساس نقشهای خاصی محدود کرد.
ثبت کاربر ادمین Identity
در ادامه میخواهیم دسترسی به کامپوننتهای مختلف را بر اساس نقشها، محدود کنیم. به همین جهت نیاز است تعدادی نقش و یک کاربر ادمین را به بانک اطلاعاتی برنامه اضافه کنیم. برای اینکار به پروژهی BlazorServer.Common مراجعه کرده و تعدادی نقش ثابت را تعریف میکنیم:
namespace BlazorServer.Common { public static class ConstantRoles { public const string Admin = nameof(Admin); public const string Customer = nameof(Customer); public const string Employee = nameof(Employee); } }
سپس در فایل BlazorServer.App\appsettings.json، مشخصات ابتدایی کاربر ادمین را ثبت میکنیم:
{ "AdminUserSeed": { "UserName": "vahid@dntips.ir", "Password": "123@456#Pass", "Email": "vahid@dntips.ir" } }
namespace BlazorServer.Models { public class AdminUserSeed { public string UserName { get; set; } public string Password { get; set; } public string Email { get; set; } } }
namespace BlazorServer.App { public class Startup { public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.AddOptions<AdminUserSeed>().Bind(Configuration.GetSection("AdminUserSeed")); // ...
using System; using System.Linq; using System.Threading.Tasks; using BlazorServer.Common; using BlazorServer.DataAccess; using BlazorServer.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace BlazorServer.Services { public class IdentityDbInitializer : IIdentityDbInitializer { private readonly ApplicationDbContext _dbContext; private readonly UserManager<IdentityUser> _userManager; private readonly RoleManager<IdentityRole> _roleManager; private readonly IOptions<AdminUserSeed> _adminUserSeedOptions; public IdentityDbInitializer( ApplicationDbContext dbContext, UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, IOptions<AdminUserSeed> adminUserSeedOptions) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); _roleManager = roleManager ?? throw new ArgumentNullException(nameof(roleManager)); _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); _adminUserSeedOptions = adminUserSeedOptions ?? throw new ArgumentNullException(nameof(adminUserSeedOptions)); } public async Task SeedDatabaseWithAdminUserAsync() { if (_dbContext.Roles.Any(role => role.Name == ConstantRoles.Admin)) { return; } await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Admin)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Customer)); await _roleManager.CreateAsync(new IdentityRole(ConstantRoles.Employee)); await _userManager.CreateAsync( new IdentityUser { UserName = _adminUserSeedOptions.Value.UserName, Email = _adminUserSeedOptions.Value.Email, EmailConfirmed = true }, _adminUserSeedOptions.Value.Password); var user = await _dbContext.Users.FirstAsync(u => u.Email == _adminUserSeedOptions.Value.Email); await _userManager.AddToRoleAsync(user, ConstantRoles.Admin); } } }
پس از تعریف این سرویس، نیاز است آنرا به سیستم تزریق وابستگیهای برنامه اضافه کرد:
namespace BlazorServer.App { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IIdentityDbInitializer, IdentityDbInitializer>(); // ...
using System; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Polly; namespace BlazorServer.DataAccess.Utils { public static class MigrationHelpers { public static void MigrateDbContext<TContext>( this IServiceProvider serviceProvider, Action<IServiceProvider> postMigrationAction ) where TContext : DbContext { using var scope = serviceProvider.CreateScope(); var scopedServiceProvider = scope.ServiceProvider; var logger = scopedServiceProvider.GetRequiredService<ILogger<TContext>>(); using var context = scopedServiceProvider.GetService<TContext>(); logger.LogInformation($"Migrating the DB associated with the context {typeof(TContext).Name}"); var retry = Policy.Handle<Exception>().WaitAndRetry(new[] { TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(15) }); retry.Execute(() => { context.Database.Migrate(); postMigrationAction(scopedServiceProvider); }); logger.LogInformation($"Migrated the DB associated with the context {typeof(TContext).Name}"); } } }
کار متد الحاقی فوق، دریافت یک IServiceProvider است که به سرویسهای اصلی برنامه اشاره میکند. سپس بر اساس آن، یک Scoped ServiceProvider را ایجاد میکند تا درون آن بتوان با Context برنامه در طی مدت کوتاهی کار کرد و در پایان آن، سرویسهای ایجاد شده را Dispose کرد.
در این متد ابتدا Database.Migrate فراخوانی میشود تا اگر مرحلهای از Migrations برنامه هنوز به بانک اطلاعاتی اعمال نشده، کار اجرا و اعمال آن انجام شود. سپس یک متد سفارشی را از فراخوان دریافت کرده و اجرا میکند. برای مثال توسط آن میتوان IIdentityDbInitializer در فایل BlazorServer.App\Program.cs به صوت زیر فراخوانی کرد:
public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); host.Services.MigrateDbContext<ApplicationDbContext>( scopedServiceProvider => scopedServiceProvider.GetRequiredService<IIdentityDbInitializer>() .SeedDatabaseWithAdminUserAsync() .GetAwaiter() .GetResult() ); host.Run(); }
و همچنین کاربر پیشفرض سیستم را نیز میتوان مشاهده کرد:
که نقش ادمین و کاربر پیشفرض، به این صورت به هم مرتبط شدهاند (یک رابطهی many-to-many برقرار است):
محدود کردن دسترسی کاربران بر اساس نقشها
پس از ایجاد کاربر ادمین و تعریف نقشهای پیشفرض، اکنون محدود کردن دسترسی به کامپوننتهای برنامه بر اساس نقشها، سادهاست. برای این منظور فقط کافی است لیست نقشهای مدنظر را که میتوانند توسط کاما از هم جدا شوند، به ویژگی Authorize کامپوننتها معرفی کرد:
@attribute [Authorize(Roles = ConstantRoles.Admin)]
protected override async Task OnInitializedAsync() { var authenticationState = await AuthenticationState; if (!authenticationState.User.Identity.IsAuthenticated || !authenticationState.User.IsInRole(ConstantRoles.Admin)) { var uri = new Uri(NavigationManager.Uri); NavigationManager.NavigateTo($"/identity/account/login?returnUrl={uri.LocalPath}"); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-23.zip
برنامههای Vue.jsای از چندین کامپوننت برای بخش بندی هر قسمت تشکیل میشوند و این بخش بندی برای مدیریت بهتر تغییرات، خطایابی، نگهداری و استفاده مجدد (reusable) میباشد. فرض کنید تعدادی کامپوننت در برنامه داریم و اطلاعات این کامپوننتها بهم وابسته میباشند؛ بطور مثال یک کامپوننت انتخاب دسته بندی را داریم و به محض تغییر این مقدار، میخواهیم لیستی از محصولات پیشنهادی یا پرفروشِ آن دسته بندی، در کامپوننت پایین صفحه نمایش داده شود و یا خرید یک محصول را در نظر بگیرید که بلافاصله محتوای نمایش سبد خرید، بروزرسانی شود. در این مقاله ارتباط از نوع Parent و Child بین کامپوننتها بررسی میشود.
نکته: برای ارسال اطلاعات از کامپوننتِ Parent به Child، از Props استفاده میشود و برای ارتباط از Child به Parent، از emit$ استفاده میشود.
یک برنامه Vue.js با نام vue-communication-part-1 ایجاد نمایید. سپس دو کامپوننت را با نامهای Parent و Child، در پوشه components ایجاد کنید:
در کامپوننت Parent، یک تابع با نام increase وجود دارد که مقدار متغیر parentCounter را افزایش میدهد. چون قصد داریم مقدار متغیر parentCounter در کامپوننت Child نیز بروزرسانی شود، آن را به کامپوننت Child پاس میدهیم:
<Child :childCounter="parentCounter"/>
محتوای کامپوننت Parent:
<template> <div> <div> <h2>Parent Component</h2> <!-- را نمایش میدهید parentCounter مقدار --> <h1>{{ parentCounter }}</h1> <button @click="increase">Increase Parent</button> </div> <div> <!-- پاس میدهید Child در کامپوننت childCounter را به پراپرتی parentCounter مقدار --> <!--از طریق decreaseParent سبب اتصال و فراخوانی تابع @callDecreaseParent به decreaseParent با انتساب --> <!-- میشود Child در کامپوننت callDecreaseMethodInParent تابع --> <Child :childCounter="parentCounter" @callDecreaseParent="decreaseParent"/> </div> </div> </template> <script> //برای استفاده در کامپوننت جاری Child ایمپورت کردن کامپوننت import Child from "./Child.vue"; export default { // در این بخش متغیرهای مورد نیاز کامپوننت را تعریف میکنیم data() { return { parentCounter: 0 }; }, components: { // میتوان آرایه ای از کامپوننتها را در یک کامپوننت استفاده نمود // در این مثال فقط از یک کامپوننت استفاده شده Child }, methods: { //را یک واحد افزایش میدهد parentCounter این متد مقدار increase() { this.parentCounter++; }, decreaseParent() { this.parentCounter--; } } }; </script> <style> .parent-block, .child { text-align: center; margin: 20px; padding: 20px; border: 2px gray solid; } </style>
در کامپوننت Child قصد دریافت مقدار پراپرتیِ childCounter را داریم که از طریق کامپوننت Parent، مقدارش تنظیم و بروزرسانی میشود. به این منظور در قسمت props یک متغیر بنام childCounter را ایجاد میکنیم.
Data is the private memory of each component where you can store any variables you need. Props are how you pass this data from a parent component down to a child component
محتوای کامپوننت Child
<template> <div> <h3>Child Component</h3> <!-- را نمایش میدهید childCounter مقدار --> <h3>{{ childCounter }}</h3> <button @click="increase">Increase Me</button> <button @click="callDecreaseMethodInParent">Call Decrease Method In Parent</button> </div> </template> <script> export default { // استفاده میشود Child به Parent برای ارتباط بین کامپوننت props از props: { childCounter: Number }, data() { return {}; }, methods: { //را یک واحد افزایش میدهد childCounter این متد مقدار increase() { this.childCounter++; }, // فراخوانی میکند Parent را در کامپوننت decreaseParent تابع callDecreaseMethodInParent() { this.$emit("callDecreaseParent"); } } }; </script>
محتوای کامپوننت اصلی برنامه App.vue:
<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <parent></parent> </div> </template> <script> import Parent from "./components/Parent.vue"; export default { name: "app", components: { parent: Parent } }; </script> <style> #app { width: 50%; margin: 0 auto; text-align: center; } </style>
اکنون برنامه را با دستور زیر اجرا کنید:
npm run serve
بعد از اجرای دستور فوق، روی گزینه زیر ctrl+click میکنیم تا نتیجه کار در مرورگر قابل رویت باشد:
نمایش صفحه زیر نشان دهندهی درستی انجام کار تا اینجا است:
اکنون روی دکمهی Increase Parent کلیک میکنیم. همزمان مقدار شمارشگر، در هر دو کامپوننت Parent و Child افزایش مییابد و این بدین معناست که با استفاده از Props میتوانیم دادههای دلخواهی را در کامپوننت Child بروز رسانی کنیم. هر زمانی روی دکمهی Increase Me در کامپوننت Child کلیک کنیم، فقط به مقدار شمارشگر درون خودش اضافه میشود و تاثیری را بر شمارشگر Parent ندارد. در واقع یک کپی از مقدار شمارشگر Parent را درون خود دارد.
در ادامه قصد داریم بروزرسانی داده را از Child به Parent انجام دهیم. برای انجام اینکار از emit$ استفاده میکنیم. در دیکشنری Cambridge Dictionary معنی emit به ارسال یک سیگنال ترجمه شدهاست. در واقع بااستفاده از emit میتوانیم یک تابع را در کامپوننت Parent فراخوانی کنیم و در آن تابع، کد دلخواهی را برای دستکاری دادهها مینویسیم.
در تابع callDecreaseMethodInParent در کامپوننت Child، کد زیر را قرار میدهیم:
this.$emit("callDecreaseParent");
هر زمانکه این تابع اجرا شود، یک سیگنال از طریق کد زیر برای کامپوننت Parent ارسال میشود:
<Child @callDecreaseParent="decreaseParent"/>
در کد فوق مشخص شده که با ارسال سیگنال callDecreaseParent، تابع decreaseParent در کامپوننت Parent فراخوانی شود.
نکته: برای اجرای برنامه و دریافت پکیجهای مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:
npm install
چند نکته
this.$emit //dispatches an event to its parent component
کد فوق سبب اجرای یک تابع در کامپوننتِ Parent خودش میشود.
this.$parent // gives you a reference to the parent component
ارجاعی به کامپوننت Parent خودش را فراهم میکند:
this.$root // gives you a reference to the root component
زمانیکه چندین کامپوننت تو در تو را داریم یا به اصطلاح nested component، سبب ارجاعی به بالاترین کامپوننت Parent میگردد.
this.$parent.$emit // will make the parent dispatch the event to its parent
سبب اجرای تابعِ Parent کامپوننتِ Parent جاری میشود. به بیان ساده اگر این کد در کامپوننت فرزند فراخوانی شود، سبب اجرای تابعی در کامپوننت پدربزرگِ خود میشود.
this.$root.$emit // will make the root dispatch the event to itself
سبب اجرای تابعی در کامپوننت root میشود (بالاترین کامپوننتِ پدرِ کامپوننت جاری).
تابع emit$ دارای آگومانهای دیگری برای پاس دادن اطلاعات از کامپوننت Child به Parent میباشد؛ مثل زمانیکه قصد دارید اطلاعاتی در مورد محصول خریداری شده را به سبد خرید پاس دهید. در مثال دیگری که در ادامه قرار میگیرد نحوه کارکرد ارتباط کامپوننت Parent و Child را در یک برنامه بهتر تجربه میکنیم.
پیاده سازی یک سبد خرید ساده با روش مقالهی جاری
نکته: برای اجرای برنامه و دریافت پکیجهای مورد استفاده در مثال جاری، نیاز است دستور زیر را اجرا کنید:
npm install
همچنین نیاز هست تا پکیچ node-sass را با دستور زیر برای این مثال نصب کنید.
npm install node-sass
<Button Command="{Binding IncreaseStepsCountCommand}" Text="Button with command binding" /> <Button Text="Button with event to command"> <Button.Behaviors> <prismBehaviors:EventToCommandBehavior Command="{Binding IncreaseStepsCountCommand}" EventName="Clicked" /> </Button.Behaviors> </Button>
xmlns:prismBehaviors="clr-namespace:Prism.Behaviors;assembly=Prism.Forms"
<Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}"> <Label.GestureRecognizers> <SwipeGestureRecognizer Command="{Binding IncreaseStepsCountCommand}" Direction="Left" /> </Label.GestureRecognizers> </Label>
<Label Text="{Binding StepsCount, StringFormat='{}Button tapped {0} times!'}" />
<Entry x:Name="MyEntry" /> <Label Text="{Binding Text, Source={x:Reference MyEntry}}" />
<ContentPage ... xmlns:vm="clr-namespace:XamApp.ViewModels" x:DataType="vm:HelloWorldViewModel">
private int _StepsCount; public int StepsCount { get => _StepsCount; set => SetProperty(ref _StepsCount, value); }
public int StepsCount { get; set; }
معماری N-Tier چالشهای بخصوصی را برای قابلیتهای change-tracking در EF اضافه میکند. در ابتدا دادهها توسط یک آبجکت EF Context بارگذاری میشوند اما این آبجکت پس از ارسال دادهها به کلاینت از بین میرود. تغییراتی که در سمت کلاینت روی دادهها اعمال میشوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیتها نمیداند.
در نسخههای قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities میتوانستند تغییرات موجودیتها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی میشود، آبجکت DbContext از آن پشتیبانی نمیکند.
در این سری از مقالات روی عملیات پایه CRUD تمرکز میکنیم که در اکثر اپلیکیشنهای n-Tier استفاده میشوند. همچنین خواهیم دید چگونه میتوان تغییرات موجودیتها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیتهای منفصل (disconnected) توسط سرویسهای Web API نگاهی خواهیم داشت.
بروز رسانی موجودیتهای منفصل با Web API
سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده میشود. همچنین مدیریت دادهها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی میکند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژهها برای شبیه سازی یک محیط n-Tier انجام شده است.
فرض کنید مدلی مانند تصویر زیر داریم.
همانطور که میبینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی میکند. میخواهیم مدل و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده میکند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
- کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
- کلاس جدیدی با نام Order در پوشه مدلها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order { public int OrderId { get; set; } public string Product { get; set; } public int Quantity { get; set; } public string Status { get; set; } public byte[] TimeStamp { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext { public Recipe1Context() : base("Recipe1ConnectionString") { } public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>().ToTable("Orders"); // Following configuration enables timestamp to be concurrency token modelBuilder.Entity<Order>().Property(x => x.TimeStamp) .IsConcurrencyToken() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe1ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال میکند.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); ... }
- در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController { // GET api/order public IEnumerable<Order> Get() { using (var context = new Recipe1Context()) { return context.Orders.ToList(); } } // GET api/order/5 public Order Get(int id) { using (var context = new Recipe1Context()) { return context.Orders.FirstOrDefault(x => x.OrderId == id); } } // POST api/order public HttpResponseMessage Post(Order order) { // Cleanup data from previous requests Cleanup(); using (var context = new Recipe1Context()) { context.Orders.Add(order); context.SaveChanges(); // create HttpResponseMessage to wrap result, assigning Http Status code of 201, // which informs client that resource created successfully var response = Request.CreateResponse(HttpStatusCode.Created, order); // add location of newly-created resource to response header response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = order.OrderId })); return response; } } // PUT api/order/5 public HttpResponseMessage Put(Order order) { using (var context = new Recipe1Context()) { context.Entry(order).State = EntityState.Modified; context.SaveChanges(); // return Http Status code of 200, informing client that resouce updated successfully return Request.CreateResponse(HttpStatusCode.OK, order); } } // DELETE api/order/5 public HttpResponseMessage Delete(int id) { using (var context = new Recipe1Context()) { var order = context.Orders.FirstOrDefault(x => x.OrderId == id); context.Orders.Remove(order); context.SaveChanges(); // Return Http Status code of 200, informing client that resouce removed successfully return Request.CreateResponse(HttpStatusCode.OK); } } private void Cleanup() { using (var context = new Recipe1Context()) { context.Database.ExecuteSqlCommand("delete from [orders]"); } } }
در قدم بعدی اپلیکیشن کلاینت را میسازیم که از سرویس Web API استفاده میکند.
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
- کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.
نکته: قسمت هایی از اپلیکیشن که باید در لایههای مختلف مورد استفاده قرار گیرند - مانند کلاسهای موجودیتها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا میتوانید پروژه ای از نوع Class Library بسازید و تمام موجودیتها را در آن تعریف کنید. سپس لایههای مختلف این پروژه را ارجاع خواهند کرد.
فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.
private HttpClient _client; private Order _order; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { // create instance of the program class var program = new Program(); program.ServiceSetup(); program.CreateOrder(); // do not proceed until order is added await program.PostOrderAsync(); program.ChangeOrder(); // do not proceed until order is changed await program.PutOrderAsync(); // do not proceed until order is removed await program.RemoveOrderAsync(); } private void ServiceSetup() { // map URL for Web API cal _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") }; // add Accept Header to request Web API content // negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept. Add(new MediaTypeWithQualityHeaderValue("application/json")); } private void CreateOrder() { // Create new order _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" }; } private async Task PostOrderAsync() { // leverage Web API client side API to call service var response = await _client.PostAsJsonAsync("api/order", _order); Uri newOrderUri; if (response.IsSuccessStatusCode) { // Capture Uri of new resource newOrderUri = response.Headers.Location; // capture newly-created order returned from service, // which will now include the database-generated Id value _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully created order. Here is URL to new resource: {0}", newOrderUri); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private void ChangeOrder() { // update order _order.Quantity = 10; } private async Task PutOrderAsync() { // construct call to generate HttpPut verb and dispatch // to corresponding Put method in the Web API Service var response = await _client.PutAsJsonAsync("api/order", _order); if (response.IsSuccessStatusCode) { // capture updated order returned from service, which will include new quanity _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully updated order: {0}", response.StatusCode); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private async Task RemoveOrderAsync() { // remove order var uri = "api/order/" + _order.OrderId; var response = await _client.DeleteAsync(uri); if (response.IsSuccessStatusCode) Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode); else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); }
Successfully updated order: OK
Sucessfully deleted order: OK
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر Web API دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله اپلیکیشن در حال اجرا است و سرویسهای ما قابل دسترسی هستند.
حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی میکنیم. با این کار از سرویس مورد نظر درخواست میکنیم که دادهها را با فرمت JSON بازگرداند. سپس یک آبجکت Order میسازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال میکنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت میکند و آن را به لیست موجودیتها در Context جاری اضافه مینماید. این عمل باعث میشود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات میکند. در آخر با فراخوانی متد SaveChanges دادهها را ذخیره میکنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار میدهیم و به کلاینت ارسال میکنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینتها درخواستهای ایجاد رکورد جدید را بصورت POST ارسال میکنند. درخواستهای HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت میشوند.
در مرحله بعد عملیات بعدی را اجرا میکنیم، تعداد سفارش را تغییر میدهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال میکنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت میشود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر میدهیم، که این کار موجودیت را به Context جاری میچسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی میکنیم. در شمارههای بعدی این سری از مقالات، خواهیم دید چگونه میتوان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام میرسانیم.
در مرحله بعد، عملیات نهایی را اجرا میکنیم که موجودیت Order را از منبع داده حذف میکند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه میکنیم و متد DeleteAsync را فراخوانی میکنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی میکنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری میکند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد میشود.
در یک اپلیکیشن واقعی بهتر است کد دسترسی دادهها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.