داخل آن یک پوشهی Samples هست که از برنامههای وب تا EF و دسکتاپ و غیره را پوشش میدهد.
مستندات آن در گروه PdfReport سایت ارائه شده و همچنین در راهنمای آن در سایت.
هم چنین در قسمت Add folders and core references تیک گزینهی Web Api را نیز فعال مینماییم.
حال احتیاج به نصب پکیج OData با استفاده از nuget package manager داریم. کافیست دستور زیر را در package manager console وارد نماییم.
Install-Package Microsoft.AspNet.Odata
این دستور آخرین ورژن Odata package را از nuget دانلود مینماید.
بعد از نصب شدن OData نیاز به اضافه کردن یک Model داریم. کلاسی را به نام Product در پوشهی Models میسازیم.
کلاس Product.cs حاوی فیلدهای زیر است.
namespace ProductService.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } public string Category { get; set; } } }
پراپرتی Id، کلید این entity است و کلاینت میتواند کوئری را بر روی entity، به وسیلهی key بزند. برای مثال برای گرفتن Product با Id برابر 2، باید این url را ارسال نمود "(2)Products/"
پرواضح است که Id در Database به عنوان Primary key در نظر گرفته شده است.
حال احتیاج به نصب Entity Framework داریم که با ارسال دستور زیر از طریق nuget نصب خواهد شد
Install-Package EntityFramework
بعد از نصب کردن ef نیاز به اضافه کردن connection string در web config داریم.
<connectionStrings> <add name="ProductsContext" connectionString="Data Source=.; Initial Catalog=ProductsContext; Integrated Security=True;MultipleActiveResultSets=True;" providerName="System.Data.SqlClient" /> </connectionStrings>
الان میتوانیم کلاس ProductsContext را درون پوشهی Models ایجاد نماییم. محتویات آن را به صورت زیر وارد مینماییم
using System.Data.Entity; namespace ProductService.Models { public class ProductsContext : DbContext { public ProductsContext() : base("name=ProductsContext") { } public DbSet<Product> Products { get; set; } } }
درون Constructor کلاس ProductsContext، داریم name=ProductsContext که باید برابر name درون connection string باشد.
حال نیاز به کانفیگ OData داریم. درون پوشهی App_Start و کلاس WebApiConfig.cs محتویات زیر را جایگزین متد register نمایید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products"); config.MapODataServiceRoute( routeName: "ODataRoute", routePrefix: null, model: builder.GetEdmModel()); } }
این کد دو فرآیند زیر را انجام میدهد
1) ساخت Entity Data Model (EDM)
2) اضافه کردن route
EDM یک مدل انتزاعی از data است. EDM برای تولید سند metadata استفاده میشود. کلاس ODataModelBuilder برای ساخت EDM با استفاده از default naming convention میباشد که باعث کاهش کدها میشود. ضمنا کلاس MapODataServiceRoute برای ساخت OData v4 route میباشد. همانگونه که اطلاع دارید، تعریف route برای مدیریت کردن WebApi و چگونگی مسیریابی درخواستهای http میباشد.
اگر application شما احتیاج به چند OData endpoint داشته باشد، میتوانید برای هر کدام routeهای جدا و همچنین نام یکتایی را برای routeName و routePrefix آن در نظر بگیرید.
اضافه کردن OData Controller
یک Controller، کلاسی برای مدیریت کردن درخواستهای http میباشد. شما باید Controllerهای مجزایی را برای هر entity set در OData service خود بسازید. در این مقاله Controller مربوط به موجودیت Product را میسازیم.
در Solution Explorer با کلیک راست بر روی پوشهی Controller، کلاسی به نام ProducsController را میسازیم. دقت کنید نام آن حتما باید به Controller ختم شود.
در OData V3 میتوانیم Controller را با استفاده از Scaffolding بسازیم؛ ولی در V4 این ویژگی وجود ندارد!
محتویات زیر را در این کنترلر اضافه مینماییم:
using ProductService.Models; using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Linq; using System.Net; using System.Threading.Tasks; using System.Web.Http; using System.Web.OData; namespace ProductService.Controllers { public class ProductsController : ODataController { ProductsContext db = new ProductsContext(); private bool ProductExists(int key) { return db.Products.Any(p => p.Id == key); } protected override void Dispose(bool disposing) { db.Dispose(); base.Dispose(disposing); } } }
این مرحلهی ابتدایی از پیاده سازی کنترلر میباشد و در قسمت بعد به پیاده سازی CRUD مربوط به آن میپردازیم.
Querying The Entity Set
این 2 متد را به کنترلر خود اضافه مینماییم
[EnableQuery] public IQueryable<Product> Get() { return db.Products; } [EnableQuery] public SingleResult<Product> Get([FromODataUri] int key) { IQueryable<Product> result = db.Products.Where(p => p.Id == key); return SingleResult.Create(result); }
ویژگی EnableQuery به معنای امکان Query زدن از سمت کلاینت به آن میباشد. FromODataUri نیز برای امکان پاس دادن پارامتر از طریق Uri است.
متد Get بدون پارامتر، قادر به برگرداندن تمامی Productها میباشد و متد Get با پارامتر، قادر به برگرداندن آن Product خاص با استفاده از unique Id است.
در صورت داشتن EnableQuery با استفاده از Query Option هایی مثل filter$ و sort$ و غیره از سمت کلاینت قادر به تغییر دادن کوئریهای خود هستیم.
Adding and Entity to Entity Set
برای اجازه دادن به کلاینت، جهت اضافه کردن یک Product به دیتابیس، متد Post زیر را اضافه مینماییم
public async Task<IHttpActionResult> Post(Product product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } db.Products.Add(product); await db.SaveChangesAsync(); return Created(product); }
Updation an Entity
OData از دو روش متفاوت برای Update کردن یک موجودیت استفاده مینماید.
1) Patch : امکان partial update برای موجودیت مربوطه را فراهم میسازد.
2) Put : موجودیت جدید را به صورت کامل جایگزین مینماید.
مشکل روش Put این است که کلاینت مجبور به ارسال تمامی فیلدهای مربوطه میباشد. حتی آن هایی که اساسا تغییری نکردهاند. بنابراین روش Patch ترجیح داده میشود.
در هر صورت ما به پیاده سازی هر دو روش میپردازیم:
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Product> product) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var entity = await db.Products.FindAsync(key); if (entity == null) { return NotFound(); } product.Patch(entity); try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(entity); } public async Task<IHttpActionResult> Put([FromODataUri] int key, Product update) { if (!ModelState.IsValid) { return BadRequest(ModelState); } if (key != update.Id) { return BadRequest(); } db.Entry(update).State = EntityState.Modified; try { await db.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProductExists(key)) { return NotFound(); } else { throw; } } return Updated(update); }
در قسمت Patch کنترلر از <Delta<T استفاده میکند که typeی است برای track کردن تغییرات در مدل مربوطه.
Deleting an Entity
برای حذف هر موجودیت نیز کافیست متد زیر را به کنترلر خود اضافه نمایید:
public async Task<IHttpActionResult> Delete([FromODataUri] int key) { var product = await db.Products.FindAsync(key); if (product == null) { return NotFound(); } db.Products.Remove(product); await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
من چند رکورد تستی را به صورت زیر وارد کردهام:
حال پروژهی خود را run نموده و آدرس زیر را وارد نمایید:
http://localhost:YourPort/Products
پاسخ، مجموعهای از entityهای زیر خواهد بود:
{ "@odata.context":"http://localhost:4516/$metadata#Products","value":[ { "Id":1,"Name":"Ali","Price":2.00,"Category":"aaa" },{ "Id":2,"Name":"Reza","Price":1.00,"Category":"bbb" },{ "Id":3,"Name":"Ahmad","Price":0.00,"Category":"ccc" } ] }
شما میتوانید از هر کدام از فیلترهای زیر برای کوئری زدن از کلاینت به سمت سرور استفاده نمایید. بطور مثال هر کدام از اینها پاسخ متفاوت و مربوط به خود را برگشت میدهد:
/Products(2)
Productی با آی دی 2 را بر میگرداند.
/Products?$filter=Id gt 1
محصولی را با آی دی بزرگتر از 1، بر میگرداند.
Products?$select=Name
روی محصولات select زده و فقط فیلد Name آنها را بر میگرداند.
Products?$select=Name,Price
آرایهای از objectهایی با پراپرتی Name و Price را بر میگرداند.
/Products?$top=3
فقط 3 رکورد اول را بر میگرداند.
همانطور که ملاحظه میفرمایید، استفاده از OData باعث کمتر شدن کدهای سمت سرور و همچنین امکان کوئری زدن از سمت کلاینت به سمت سرور را مهیا میکند.
بعد از خواندن این مقاله ممکن است به این مساله فکر کنید که این کار باعث کاهش امنیت میشود. باید عرض کنم که امکانات زیادی برای محدود کردن کوئریها، فراهم شده است و هیچ نگرانی از این بابت وجود ندارد. بطور مثال میتوانید تعیین کنید که از entity مربوطه فقط حداکثر 3 پراپرتی قابلیت کوئری زدن را دارند؛ یا اینکه حداکثر در هر کوئری، 10 رکورد قابلیت پاسخ دادن خواهد داشت.
پس بدین صورت میباشد که شما حداکثر امکانات ممکن را به سمت کلاینت میدهید و اختیار بدان واگذار شده که آیا از این امکانات حداکثری، استفاده نماید یا خیر.
امکانات این پروتکل منحصر به فرد است و در مقالههای بعدی به جزئیات بیشتر و دقیقتری خواهیم پرداخت.
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddMediatR(); }
public class Customer { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime RegistrationDate { get; set; } }
public class CustomerDto { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string RegistrationDate { get; set; } }
public class CreateCustomerCommand : IRequest<CustomerDto> { public CreateCustomerCommand(string firstName, string lastName) { FirstName = firstName; LastName = lastName; } public string FirstName { get; } public string LastName { get; } }
public class CreateCustomerCommandHandler : IRequestHandler<CreateCustomerCommand, CustomerDto> { readonly ApplicationDbContext _context; readonly IMapper _mapper; public CreateCustomerCommandHandler(ApplicationDbContext context, IMapper mapper) { _context = context; _mapper = mapper; } public async Task<CustomerDto> Handle(CreateCustomerCommand createCustomerCommand, CancellationToken cancellationToken) { Customer customer = _mapper.Map<Customer>(createCustomerCommand); await _context.Customers.AddAsync(customer, cancellationToken); await _context.SaveChangesAsync(cancellationToken); return _mapper.Map<CustomerDto>(customer); } }
public class DomainProfile : Profile { public DomainProfile() { CreateMap<CreateCustomerCommand, Customer>() .ForMember(c => c.RegistrationDate, opt => opt.MapFrom(_ => DateTime.Now)); CreateMap<Customer, CustomerDto>() .ForMember(cd => cd.RegistrationDate, opt => opt.MapFrom(c => c.RegistrationDate.ToShortDateString())); } }
[HttpPost] public async Task<IActionResult> CreateCustomer([FromBody] CreateCustomerCommand createCustomerCommand) { CustomerDto customer = await _mediator.Send(createCustomerCommand); return CreatedAtAction(nameof(GetCustomerById), new { customerId = customer.Id }, customer); }
طبق شکل فوق ما میتوانیم درون یک container یک volume داشته باشیم. وقتی ما چیزی را درون آن مینویسیم عملا داریم در قسمت خاصی به نام Docker Host عمل write کردن را انجام میدهیم که باعث میشود داکر متوجه آن شود. وقتی اسمی را به یک Volume انتساب میدهیم همانند /var/www، در واقع یک اسم مستعار (alias) میباشد که اشاره میکند به این Docker host موجود. در ادامه بیشتر با Volumeها آشنا خواهیم شد.
docker images
docker ps
docker pull kitematic/hello-world-nginx
docker run -p 80:80 kitematic/hello-world-nginx
بدین معناست که container شما اجرا شده و قابلیت مورد استفاده قرار گرفتن را خواهد داشت. حال اگر دستور docker ps را مجددا وارد نمایید، اطلاعات این container را از نوع id, status port و غیره، مشاهده خواهید کرد.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title></title> <link href="Content/font-awesome.css" rel="stylesheet" /> <link href="Content/froala_editor.css" rel="stylesheet" /> <script src="Scripts/jquery-1.10.2.min.js"></script> <script src="Scripts/froala_editor.min.js"></script> <script src="Scripts/langs/fa.js"></script> </head> <body> <form id="form1" runat="server"> </form> </body> </html>
@{ ViewBag.Title = "Index"; } <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> @using (Html.BeginForm(actionName: "Index", controllerName: "Home")) { @Html.TextArea(name: "Editor1") <input type="submit" value="ارسال" /> } @section Scripts { <script type="text/javascript"> $(function () { $('#Editor1').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: '@Url.Content("~/Content/img/preloader.gif")', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: '@Url.Action("FroalaAutoSave", "Home")', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: '@Url.Action("FroalaUploadImage", "Home")', imageParams: { postId: "123" }, enableScript: false }); }); </script> }
/// <summary> /// ذخیره سازی خودکار /// </summary> [HttpPost] [ValidateInput(false)] public ActionResult FroalaAutoSave(string body, int? postId) // نام پارامتر بادی را تغییر ندهید { //todo: save body ... return new EmptyResult(); }
// todo: مسایل امنیتی آپلود را فراموش نکنید /// <summary> /// ذخیره سازی تصاویر ارسالی /// </summary> [HttpPost] public ActionResult FroalaUploadImage(HttpPostedFileBase file, int? postId) // نام پارامتر فایل را تغییر ندهید { var fileName = Path.GetFileName(file.FileName); var rootPath = Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); return Json(new { link = "images/" + fileName }, JsonRequestBehavior.AllowGet); }
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" ValidateRequest="false" EnableEventValidation="false" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="FroalaWebFormsTest.Default" %> <%--اعتبارسنجی ورودی غیرفعال شده چون باید تگ ارسال شود--%> <%--همچنین در وب کانفیگ هم تنظیم دیگری نیاز دارد--%> <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server"> <%--حالت کلاینت آی دی بهتر است تنظیم شود در اینجا--%> <asp:TextBox ID="txtEditor" ClientIDMode="Static" runat="server" Height="199px" TextMode="MultiLine" Width="447px"></asp:TextBox> <br /> <asp:Button ID="btnSave" runat="server" OnClick="btnSave_Click" Text="ارسال" /> <style type="text/css"> /*تنظیم فونت پیش فرض ادیتور*/ .froala-element { } </style> <script type="text/javascript"> $(function () { $('#txtEditor').editable({ buttons: ["bold", "italic", "underline", "strikeThrough", "fontFamily", "fontSize", "color", "formatBlock", "align", "insertOrderedList", "insertUnorderedList", "outdent", "indent", "selectAll", "createLink", "insertImage", "insertVideo", "undo", "redo", "html", "save", "inserthorizontalrule"], inlineMode: false, inverseSkin: true, preloaderSrc: 'Content/img/preloader.gif', allowedImageTypes: ["jpeg", "jpg", "png"], height: 300, language: "fa", direction: "rtl", fontList: ["Tahoma, Geneva", "Arial, Helvetica", "Impact, Charcoal"], autosave: true, autosaveInterval: 2500, saveURL: 'FroalaHandler.ashx', saveParams: { postId: "123" }, spellcheck: true, plainPaste: true, imageButtons: ["removeImage", "replaceImage", "linkImage"], borderColor: '#00008b', imageUploadURL: 'FroalaHandler.ashx', imageParams: { postId: "123" }, enableScript: false }); }); </script> </asp:Content>
using System.IO; using System.Web; using System.Web.Script.Serialization; namespace FroalaWebFormsTest { public class FroalaHandler : IHttpHandler { //todo: برای اینکارها بهتر است از وب ای پی آی استفاده شود //todo: یا دو هندلر مجزا یکی برای تصاویر و دیگری برای ذخیره سازی متن public void ProcessRequest(HttpContext context) { var body = context.Request.Form["body"]; var postId = context.Request.Form["postId"]; if (!string.IsNullOrWhiteSpace(body) && !string.IsNullOrWhiteSpace(postId)) { //todo: save changes context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } var files = context.Request.Files; if (files.Keys.Count > 0) { foreach (string fileKey in files) { var file = context.Request.Files[fileKey]; if (file == null || file.ContentLength == 0) continue; //todo: در اینجا مسایل امنیتی آپلود فراموش نشود var fileName = Path.GetFileName(file.FileName); var rootPath = context.Server.MapPath("~/images/"); file.SaveAs(Path.Combine(rootPath, fileName)); var json = new JavaScriptSerializer().Serialize(new { link = "images/" + fileName }); // البته اینجا یک فایل بیشتر ارسال نمیشود context.Response.ContentType = "text/plain"; context.Response.Write(json); context.Response.End(); } } context.Response.ContentType = "text/plain"; context.Response.Write(""); context.Response.End(); } public bool IsReusable { get { return false; } } } }
<location path="upload"> <system.webServer> <handlers accessPolicy="Read" /> </system.webServer> </location>