نصب Swashbuckle (سوواَش باکِل)
اگر عبارت Swashbuckle.AspNetCore را در سایت NuGet جستجو کنیم، چندین بستهی مختلف مرتبط با آنرا خواهیم یافت. ما در این بین، بیشتر به این بستهها علاقمندیم:
- Swashbuckle.AspNetCore.Swagger: کار آن ارائهی خروجی OpenAPI تولیدی بر اساس ASP.NET Core API برنامهی ما، به صورت یک JSON Endpoint است.
- Swashbuckle.AspNetCore.SwaggerGen: کار آن ساخت Swagger document objects است؛ یا همان OpenAPI Specification.
عموما این دو بسته را با هم جهت ارائهی OpenAPI Specification استفاده میکنند.
- Swashbuckle.AspNetCore.SwaggerUI: این بسته، نگارش جایگذاری شدهی (embedded) ابزار swagger-UI را به همراه دارد. کار آن، ارائهی یک UI خودکار، بر اساس OpenAPI Specification است که از آن برای آزمایش API نیز میتوان استفاده کرد.
یک نکته: اگر صرفا بستهی Swashbuckle.AspNetCore را نصب کنیم، هر سه بستهی فوق را با هم دریافت خواهیم کرد و اگر از Visual Studio برای نصب آنها استفاده میکنید، انتخاب گزینهی Include prerelease را فراموش نکنید؛ از این جهت که قصد داریم از نگارش 5 آنها استفاده کنیم. چون این نگارش است که از OpenAPI 3x، پشتیبانی میکند. خلاصهی این موارد، افزودن PackageReference زیر به فایل پروژهی OpenAPISwaggerDoc.Web.csproj است و سپس اجرای دستور dotnet restore:
<Project Sdk="Microsoft.NET.Sdk.Web"> <ItemGroup> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.0.0-rc2" /> </ItemGroup> </Project>
تنظیم میانافزار Swashbuckle
پس از افزودن ارجاعی به Swashbuckle.AspNetCore، اکنون نوبت انجام تنظیمات میانافزارهای آن است. برای این منظور ابتدا به کلاس Startup و متد ConfigureServices آن مراجعه میکنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddSwaggerGen(setupAction => { setupAction.SwaggerDoc( name: "LibraryOpenAPISpecification", info: new Microsoft.OpenApi.Models.OpenApiInfo() { Title = "Library API", Version = "1", Description = "Through this API you can access authors and their books.", Contact = new Microsoft.OpenApi.Models.OpenApiContact() { Email = "name@site.com", Name = "DNT", Url = new Uri("https://www.dntips.ir") }, License = new Microsoft.OpenApi.Models.OpenApiLicense() { Name = "MIT License", Url = new Uri("https://opensource.org/licenses/MIT") } }); }); }
اکنون در متد Configure، میانافزار آنرا خواهیم افزود:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); // ... }
تا اینجا اگر برنامه را اجرا کنید، میتوان OpenAPI Specification تولیدی را در آدرس زیر یافت:
https://localhost:5001/swagger/LibraryOpenAPISpecification/swagger.json
در این آدرس، LibraryOpenAPISpecification، همان نامی است که در قسمت setupAction.SwaggerDoc تنظیم کردیم.
نگاهی به OpenAPI Specification تولیدی
در ابتدای swagger.json تولیدی، همانطور که در تصویر فوق نیز مشخص است، همان مشخصات ذکر شدهی در قسمت info متد setupAction.SwaggerDoc، قابل مشاهدهاست. سپس لیست مسیرهای این API مشخص شدهاند:
اینها مسیرهایی هستند که توسط دو کنترلر کتابها و نویسندگان برنامهی Web API ما عمومی شدهاند. در اینجا مقابل هر مسیر، تعداد آیتمهای متناظری نیز ذکر شدهاند. این موارد مرتبط هستند با HTTP methods پشتیبانی شده:
که هر کدام به همراه نام متدها و پارامترهای متناظر با آنها نیز میشوند. به علاوه نوع responseهای پشتیبانی شدهی توسط این متدها نیز ذکر شدهاند. هر کدام از خروجیها نیز نوع مشخصی دارند که توسط قسمت components -> schemas تصاویر فوق، جزئیات دقیق آنها بر اساس نوع مدلهای متناظر، استخراج و ارائه شدهاند.
مشکل: نوع Response تولیدی در OpenAPI Specification صحیح نیست
اگر به جزئیات مسیر /api/authors/{authorId} دقت کنیم، نوع response آنرا صرفا 200 یا Ok ذکر کردهاست؛ در حالیکه GetAuthor تعریف شده، حالت NotFound را نیز دارد:
[HttpGet("{authorId}")] public async Task<ActionResult<Author>> GetAuthor(Guid authorId) { var authorFromRepo = await _authorsService.GetAuthorAsync(authorId); if (authorFromRepo == null) { return NotFound(); } return Ok(_mapper.Map<Author>(authorFromRepo)); }
افزودن و راه اندازی Swagger UI
در ادامه میخواهیم یک رابط کاربری خودکار را بر اساس OpenAPI Specification تولیدی، ایجاد کنیم:
namespace OpenAPISwaggerDoc.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseHttpsRedirection(); app.UseSwagger(); app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); }); // ... }
پس از این تنظیم اگر آدرس https://localhost:5001/swagger/index.html را در مرورگر باز کنیم، چنین خروجی قابل مشاهده خواهد بود:
و اگر بر روی هر کدام کلیک کنیم، ریز جزئیات آنها بر اساس OpenAPI Specification ای که بررسی کردیم، تولید شدهاست (از پارامترها تا نوع خروجی):
اکنون اگر بر روی دکمهی try it out آن نیز کلیک کنید، در همینجا میتوان این API را آزمایش کرد. برای مثال Controls Accept header را بر روی application/json قرار داده و سپس بر روی دکمهی execute که پس از کلیک بر روی دکمهی try it out ظاهر شدهاست، کلیک کنید تا بتوان خروجی Web API را مشاهده کرد.
در انتهای این صفحه، در قسمت schemas آن، مشخصات مدلهای بازگشت داده شدهی توسط Web API نیز ذکر شدهاند:
یک نکته: تغییر آدرس https://localhost:5001/swagger/index.html به ریشهی سایت
اگر علاقمند باشید تا زمانیکه برای اولین بار آدرس ریشهی سایت را در مسیر https://localhost:5001 باز میکنید، Swagger UI نمایان شود، میتوانید تنظیم RoutePrefix زیر را اضافه کنید:
app.UseSwaggerUI(setupAction => { setupAction.SwaggerEndpoint( "/swagger/LibraryOpenAPISpecification/swagger.json", "Library API"); setupAction.RoutePrefix = ""; });
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: OpenAPISwaggerDoc-02.zip
در قسمت بعد، به بهبود و غنی سازی جزئیات OpenAPI Specification تولیدی خواهیم پرداخت.
خوب ابتدا فرض میکنیم برای نمایش تصاویر چند حالت داریم مثلا کوچک، متوسط، بزرگ و حالت واقعی (اندازه اصلی).
البته دقت نمایید که این طبقه بندی فرضی بوده و ممکن است برای پروژههای مختلف این طبقه بندی متفاوت باشد. (در این پست قصد فقط اشنایی با تغییر اندازه تصاویر است و شاید کد به درستی refactor نشده باشد).
برای تغییر اندازه تصاویر در زمان اجرا یکی از روش ها، میتواند استفاده از Handler باشد. خوب برای ایجاد Handler ابتدا در پروژه وب خود بروی پروژه راست کلیک کرده، و گزینه New Item را برگزینید، و در پنجره ظاهر شده مانند تصویر زیر گزینه Generic Handler را انتخاب نمایید.
پس از ایجاد هندلر، فایل کد آن مانند زیر خواهد بود، ما باید کدهای خود را در متد ProcessRequestبنویسیم.
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace PWS.UI.Handler { /// <summary> /// Summary description for PhotoHandler /// </summary> public class PhotoHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "text/plain"; context.Response.Write("Hello World"); } public bool IsReusable { get { return false; } } } }
خوب برای نوشتن کد در این مرحله ما باید چند کار انجام دهیم.
1- گرفتن پارامترهای ورودی کاربر جهت تغییر سایز از طریق روشهای انتقال مقادیر بین صفحات (در اینجا استفاده از Query String ).
2-بازیابی تصویر از دیتابیس یا از دیسک به صورت یک آرایه بایتی.
3- تغییر اندازه تصویر مرحله 2 و ارسال تصویر به خروجی.
using System; using System.Data.SqlClient; using System.Diagnostics; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Web; namespace PWS.UI.Handler { /// <summary> /// Summary description for PhotoHandler /// </summary> public class PhotoHandler : IHttpHandler { /// <summary> /// بازیابی تصویر اصلی از بانک اطلاعاتی /// </summary> /// <param name="photoId">کد تصویر</param> /// <returns></returns> private byte[] GetImageFromDatabase(int photoId) { using (var connection = new SqlConnection("ConnectionString")) { using (var command = new SqlCommand("Select Photo From tblPhotos Where Id = @PhotoId", connection)) { command.Parameters.Add(new SqlParameter("@PhotoId", photoId)); connection.Open(); var result = command.ExecuteScalar(); return ((byte[])result); } } } /// <summary> /// بازیابی فایل از دیسک /// </summary> /// <param name="photoId">با فرض اینکه نام فایل این است</param> /// <returns></returns> private byte[] GetImageFromDisk(string photoId /* or somting */) { using (var sourceStream = new FileStream("Original File Path + id", FileMode.Open, FileAccess.Read)) { return StreamToByteArray(sourceStream); } } /// <summary> /// Streams to byte array. /// </summary> /// <param name="inputStream">The input stream.</param> /// <returns></returns> /// <exception cref="System.ArgumentException"></exception> static byte[] StreamToByteArray(Stream inputStream) { if (!inputStream.CanRead) { throw new ArgumentException(); } // This is optional if (inputStream.CanSeek) { inputStream.Seek(0, SeekOrigin.Begin); } var output = new byte[inputStream.Length]; int bytesRead = inputStream.Read(output, 0, output.Length); Debug.Assert(bytesRead == output.Length, "Bytes read from stream matches stream length"); return output; } /// <summary> /// Enables processing of HTTP Web requests by a custom HttpHandler that implements the <see cref="T:System.Web.IHttpHandler" /> interface. /// </summary> /// <param name="context">An <see cref="T:System.Web.HttpContext" /> object that provides references to the intrinsic server objects (for example, Request, Response, Session, and Server) used to service HTTP requests.</param> public void ProcessRequest(HttpContext context) { // Set up the response settings context.Response.ContentType = "image/jpeg"; context.Response.Cache.SetCacheability(HttpCacheability.Public); context.Response.BufferOutput = false; // مرحله اول int size = 0; switch (context.Request.QueryString["Size"]) { case "S": size = 100; //100px break; case "M": size = 198; //198px break; case "L": size = 500; //500px break; } byte[] changedImage; var id = Convert.ToInt32(context.Request.QueryString["PhotoId"]); byte[] sourceImage = GetImageFromDatabase(id); // یا //byte[] sourceImage = GetImageFromDisk(id.ToString(CultureInfo.InvariantCulture)); //مرحله 2 if (size != 0) //غیر از حالت واقعی تصویر { changedImage = Helpers.ResizeImageFile(sourceImage, size, ImageFormat.Jpeg); } else { changedImage = (byte[])sourceImage.Clone(); } // مرحله 3 if (changedImage == null) return; context.Response.AddHeader("Content-Length", changedImage.Length.ToString(CultureInfo.InvariantCulture)); context.Response.BinaryWrite(changedImage); } public bool IsReusable { get { return false; } } } }
2- متد GetImageFromDisk: این متد نام تصویر (با فرض اینکه یک عدد میباشد) را به عنوان پارامتر گرفته و آنرا بازیابی میکند (در صورتی که تصویر در دیسک ذخیره شده باشد.)
3- متد StreamToByteArray: زمانی که تصویر از فایل خوانده میشود به صورت Stream است این متد یک Stream را گرفته و تبدیل به یک آرایه بایتی میکند.
در نهایت در متد ProcessRequestتصویر خوانده شده با توجه به پارامترهای ورودی تغییر اندازه داده شده و در نهایت به خروجی نوشته میشود.
برای استفاده این هندلر، کافی است در توصیر خود به عنوان مسیر رشته ای شبیه زیر وارد نمایید:
PhotoHandler.ashx?PhotoId=10&Size=S مانند <img src='PhotoHandler.ashx?PhotoId=10&Size=S' alt='تصویر ازمایشی' />
نظرات اقای موسوی تا حدودی اعمال شد و در پست تغییراتی انجام شد.
موفق وموید باشید
برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:
- اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق میشود.
- اضافه کردن مسیرهای جدید به متد Application_Start.
یک پروژه Web Forms بسازید
ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.
کنترلر و مدل اپلیکیشن را ایجاد کنید
کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.
public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; } }
در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.
کنترلر ایجاد شده شامل یک سری متد است که بصورت خودکار برای شما اضافه شده اند، آنها را حذف کنید و کد زیر را به کنترلر خود اضافه کنید.
namespace WebForms { using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; public class ProductsController : ApiController { Product[] products = new Product[] { new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } }; public IEnumerable<Product> GetAllProducts() { return products; } public Product GetProductById(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return product; } public IEnumerable<Product> GetProductsByCategory(string category) { return products.Where( (p) => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase)); } } }
اطلاعات مسیریابی را اضافه کنید
مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری میخواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.
using System.Web.Http;
RouteTable.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = System.Web.Http.RouteParameter.Optional } );
برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.
دریافت اطلاعات بصورت آژاکسی در کلاینت
تا اینجا شما یک API دارید که کلاینتها میتوانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی میکند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:
<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %> <asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent"> </asp:Content> <asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent"> <h2>Products</h2> <table> <thead> <tr><th>Name</th><th>Price</th></tr> </thead> <tbody id="products"> </tbody> </table> </asp:Content>
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent"> <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script> </asp:Content>
همانطور که میبینید در مثال جاری از فایل محلی استفاده شده است اما در اپلیکیشنهای واقعی بهتر است از CDNها استفاده کنید.
نکته: برای ارجاع دادن اسکریپتها میتوانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.
زیر تگ jQuery اسکریپت زیر را اضافه کنید.
<script type="text/javascript"> function getProducts() { $.getJSON("api/products", function (data) { $('#products').empty(); // Clear the table body. // Loop through the list of products. $.each(data, function (key, val) { // Add a table row for the product. var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>'; $('<tr/>', { text: row }) // Append the name. .appendTo($('#products')); }); }); } $(document).ready(getProducts); </script>
هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال میکند. سرویس ما لیستی از محصولات را با فرمت JSON بر میگرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه میکند.
اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید:
در قسمت قبلی بروز رسانی موجودیتهای منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه میتوان تغییرات موجودیتها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.
تشخیص تغییرات با Web API
فرض کنید میخواهیم از سرویسهای Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر میخواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت دادهها هم با مدل Code-First پیاده سازی میشود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی میکند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما آژانسهای مسافرتی و رزرواسیون آنها را ارائه میکند. میخواهیم مدل و کد دسترسی دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
- کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
- دو کلاس جدید با نامهای TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent { public TravelAgent() { this.Bookings = new HashSet<Booking>(); } public int AgentId { get; set; } public string Name { get; set; } public virtual ICollection<Booking> Bookings { get; set; } } public class Booking { public int BookingId { get; set; } public int AgentId { get; set; } public string Customer { get; set; } public DateTime BookingDate { get; set; } public bool Paid { get; set; } public virtual TravelAgent TravelAgent { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext { public Recipe3Context() : base("Recipe3ConnectionString") { } public DbSet<TravelAgent> TravelAgents { get; set; } public DbSet<Booking> Bookings { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId); modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents"); modelBuilder.Entity<Booking>().ToTable("Bookings"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings> <add name="Recipe3ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال میکند. همچنین به JSON serializer میگوییم که self-referencing loop خاصیتهای پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیتها بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "ActionMethodSave", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }); }
- در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController { // GET api/travelagent [HttpGet] public IEnumerable<TravelAgent> Retrieve() { using (var context = new Recipe3Context()) { return context.TravelAgents.Include(x => x.Bookings).ToList(); } } /// <summary> /// Update changes to TravelAgent, implementing Action-Based Routing in Web API /// </summary> public HttpResponseMessage Update(TravelAgent travelAgent) { using (var context = new Recipe3Context()) { var newParentEntity = true; // adding the object graph makes the context aware of entire // object graph (parent and child entities) and assigns a state // of added to each entity. context.TravelAgents.Add(travelAgent); if (travelAgent.AgentId > 0) { // as the Id property has a value greater than 0, we assume // that travel agent already exists and set entity state to // be updated. context.Entry(travelAgent).State = EntityState.Modified; newParentEntity = false; } // iterate through child entities, assigning correct state. foreach (var booking in travelAgent.Bookings) { if (booking.BookingId > 0) // assume booking already exists if ID is greater than zero. // set entity to be updated. context.Entry(booking).State = EntityState.Modified; } context.SaveChanges(); HttpResponseMessage response; // set Http Status code based on operation type response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent); return response; } } [HttpDelete] public HttpResponseMessage Cleanup() { using (var context = new Recipe3Context()) { context.Database.ExecuteSqlCommand("delete from [bookings]"); context.Database.ExecuteSqlCommand("delete from [travelagents]"); } return Request.CreateResponse(HttpStatusCode.OK); } }
- در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program { private HttpClient _client; private TravelAgent _agent1, _agent2; private Booking _booking1, _booking2, _booking3; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up is completed await program.CleanupAsync(); program.CreateFirstAgent(); // do not proceed until agent is created await program.AddAgentAsync(); program.CreateSecondAgent(); // do not proceed until agent is created await program.AddSecondAgentAsync(); program.ModifyAgent(); // do not proceed until agent is updated await program.UpdateAgentAsync(); // do not proceed until agents are fetched await program.FetchAgentsAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")}; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } private async Task CleanupAsync() { // call cleanup method in service _response = await _client.DeleteAsync("api/travelagent/cleanup/"); } private void CreateFirstAgent() { // create new Travel Agent and booking _agent1 = new TravelAgent {Name = "John Tate"}; _booking1 = new Booking { Customer = "Karen Stevens", Paid = false, BookingDate = DateTime.Parse("2/2/2010") }; _booking2 = new Booking { Customer = "Dolly Parton", Paid = true, BookingDate = DateTime.Parse("3/10/2010") }; _agent1.Bookings.Add(_booking1); _agent1.Bookings.Add(_booking2); } private async Task AddAgentAsync() { // call generic update method in Web API service to add agent and bookings _response = await _client.PostAsync("api/travelagent/update/", _agent1, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include // database-generated Ids for each entity _agent1 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens"); _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void CreateSecondAgent() { // add new agent and booking _agent2 = new TravelAgent {Name = "Perry Como"}; _booking3 = new Booking { Customer = "Loretta Lynn", Paid = true, BookingDate = DateTime.Parse("3/15/2010")}; _agent2.Bookings.Add(_booking3); } private async Task AddSecondAgentAsync() { // call generic update method in Web API service to add agent and booking _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service _agent2 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent2.Name, _agent2.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void ModifyAgent() { // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1 _agent2.Name = "Perry Como, Jr."; _agent2.Bookings.Add(_booking1); } private async Task UpdateAgentAsync() { // call generic update method in Web API service to update agent 2 _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result; Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private async Task FetchAgentsAsync() { // call Get method on service to fetch all Travel Agents and Bookings _response = _client.GetAsync("api/travelagent/retrieve").Result; if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>(); foreach (var agent in agents) { Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count()); } } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } }
- در آخر کلاسهای TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژهها به اشتراک گذاشته شوند.
اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.
(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت میکنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی میکنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام دادههای پیشین را حذف میکند.
در قدم بعدی سه آبجکت جدید میسازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکتها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال میکنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت میکند و آن را به موجودیت TravelAgents در Context جاری اضافه مینماید. این کار آبجکت آژانس مسافرتی و تمام آبجکتهای فرزند آن را در حالت Added اضافه میکند و باعث میشود که context جاری شروع به ردیابی (tracking) آنها کند.
نکته: قابل ذکر است که اگر موجودیتهای متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکتهای خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر میکند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.
بعد از آن بر اساس مقدار خاصیت Id مشخص میکنیم که موجودیتها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر میدهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها میکنیم.
سپس تمام آبجکتهای فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال میکنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر میدهیم. در نهایت متد SaveChanges را فراخوانی میکنیم. در این مرحله برای موجودیتهای جدید اسکریپتهای Insert و برای موجودیتهای تغییر کرده اسکریپتهای Update تولید میشود. سپس کد وضعیت مناسب را به کلاینت بر میگردانیم. برای موجودیتهای اضافه شده کد وضعیت 201 (Created) و برای موجودیتهای بروز رسانی شده کد وضعیت 200 (OK) باز میگردد. کد 201 به کلاینت اطلاع میدهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر میدهد. هنگام تولید سرویسهای REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.
پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی میسازیم و آنها را به سرویس ارسال میکنیم. سپس نام آژانس مسافرتی دوم را تغییر میدهیم، و یکی از رزرواسیونها را از آژانس اولی به آژانس دومی منتقل میکنیم. اینبار هنگام فراخوانی متد Update تمام موجودیتها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر میدهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.
در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی میکند. این فراخوانی با کمک متد GetAsync انجام میشود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانسهای مسافرتی بهمراه رزرواسیونهای متناظرشان را دریافت میکند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری میکنیم.
دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز میگرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.
نکته دیگر آنکه در مثال جاری از قراردادهای توکار Web API برای نگاشت درخواستهای HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواستهای POST به متدهایی نگاشت میشوند که نام آنها با "Post" شروع میشود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشنهای واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویسهای REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواستهای متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
public class Program { public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); }); }
در نگارش سوم، هاستهای دیگری هم معرفی شدهاند؛ مانند امکان اجرای یک worker service بدون راه اندازی یک وب سرور و یا Blazor از روش هاستینگ متفاوتی درون یک web assembly استفاده میکند: «ارتقاء به NET Core 3.0.: پشتیبانی از ایجاد سرویسهای پسزمینه»
یکی از اشتباهاتی که همهی ما کم و بیش به آن دچار هستیم ایجاد کلاسهایی هستند که «زیاد میدانند». اصطلاحا به آنها God Classes هم میگویند و برای نمونه، پسوند یا پیشوند Util دارند. این نوع کلاسها اصل SRP را زیر سؤال میبرند (Single responsibility principle). برای مثال یک فایل ایجاد میشود و داخل آن از انواع و اقسام متدهای «کمکی» کار با دیتابیس تا رسم نمودار تا تبدیل تاریخ میلادی به شمسی و ... در طی بیش از 10 هزار سطر قرار میگیرند. یا برای مثال گروه بندیهای خاصی را در این یک فایل از طریق کامنتهای نوشته شده برای قسمتهای مختلف میتوان یافت. Refactoring مرتبط با این نوع کلاسهایی که «زیاد میدانند»، تجزیه آنها به کلاسهای کوچکتر، با تعداد وظیفهی کمتر است.
به عنوان نمونه کلاس CustomerService زیر، دو گروه کار متفاوت را انجام میدهد. ثبت و بازیابی اطلاعات ثبت نام یک مشتری و همچنین محاسبات مرتبط با سفارشات مشتریها:
using System;
using System.Collections.Generic;
namespace Refactoring.Day8.RemoveGodClasses.Before
{
public class CustomerService
{
public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public bool CustomerIsValid(string customer, int order)
{
// do work
throw new NotImplementedException();
}
public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public void Register(string customer)
{
// do work
}
public void ForgotPassword(string customer)
{
// do work
}
}
}
بهتر است این دو گروه، به دو کلاس مجزا بر اساس وظایفی که دارند، تجزیه شوند. به این ترتیب نگهداری این نوع کلاسهای کوچکتر در طول زمان سادهتر خواهند شد:
using System;
using System.Collections.Generic;
namespace Refactoring.Day8.RemoveGodClasses.After
{
public class CustomerOrderService
{
public decimal CalculateOrderDiscount(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
public bool CustomerIsValid(string customer, int order)
{
// do work
throw new NotImplementedException();
}
public IEnumerable<string> GatherOrderErrors(IEnumerable<string> products, string customer)
{
// do work
throw new NotImplementedException();
}
}
}
namespace Refactoring.Day8.RemoveGodClasses.After
{
public class CustomerRegistrationService
{
public void Register(string customer)
{
// do work
}
public void ForgotPassword(string customer)
{
// do work
}
}
}
یک CMS تجاری بزرگ با قابلیتهای زیر