<?xml version="1.0" encoding="UTF-8"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="LetsEncrypt" stopProcessing="true"> <match url=".well-known/acme-challenge/*" /> <conditions logicalGrouping="MatchAll" trackAllCaptures="false" /> <action type="None" /> </rule> </rules> </rewrite> </system.webServer> </configuration>
ASP.NET MVC #21
@{ var postUrl = Url.Action(actionName: "Delete", controllerName: "Student"); } <div class="deleteDialog"> <div> آیتمهای انتخاب شده حذف خواهند شد. آیا تأیید میکنید؟ </div> <p> <input type="submit" id="btn_SubmitDelete" value="حذف" /> <input type="submit" id="btn_CancelDelete" value="انصراف" /> </p> </div> <script type="text/javascript"> $(function () { $("#btn_SubmitDelete").click(function (e) { var button = $(this); e.preventDefault(); var data = "1,3,8,9"; $.ajax({ type: "POST", url: "@postUrl", data: JSON.stringify({ ids: data}), contentType: "application/json; charset=utf-8", dataType: 'json', cache: false, beforeSend: function () { }, success: function (html) { alert(html); $(".deleteDialog").parent("div").css("display", "none"); }, complete: function () { button.removeAttr('disabled'); button.val("حذف"); } }); }); $("#btn_CancelDelete").click(function (e) { e.preventDefault(); var button = $(this); $(".deleteDialog").parent("div").css("display", "none"); }); }); </script>
[HttpGet] public ActionResult Delete() { return PartialView("Pv_Delete"); } [HttpPost] [AjaxOnly] public ActionResult Delete(string ids) { var allIds = ids.Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries).ToList(); Thread.Sleep(2000); if (true) { return Json(new { result = "ok" }, JsonRequestBehavior.AllowGet); } return Json(new { result = "error" }); }
ASP.NET Web API فریم ورکی برای ساختن APIهای وب بر روی فریم ورک دات نت است. در این مقاله با استفاده از این فریم ورک، API وبی خواهیم ساخت که لیستی از محصولات را بر میگرداند. صفحه وب کلاینت، با استفاده از jQuery نتایج را نمایش خواهد داد.
یک پروژه Web API بسازید
در ویژوال استودیو 2013 پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را "ProductsApp" انتخاب کنید.
در دیالوگ New ASP.NET Project قالب Empty را انتخاب کنید و در قسمت "Add folders and core references for" گزینه Web API را انتخاب نمایید.
می توانید از قالب Web API هم استفاده کنید. این قالب با استفاده از ASP.NET MVC صفحات راهنمای API را خواهد ساخت. در این مقاله از قالب Empty استفاده میکنیم تا تمرکز اصلی، روی خود فریم ورک Web API باشد. بطور کلی برای استفاده از این فریم ورک لازم نیست با ASP.NET MVC آشنایی داشته باشید.
افزودن یک مدل
یک مدل (model) آبجکتی است که داده اپلیکیشن شما را معرفی میکند. ASP.NET Web API میتواند بصورت خودکار مدل شما را به JSON, XML و برخی فرمتهای دیگر مرتب (serialize) کند، و سپس داده مرتب شده را در بدنه پیام HTTP Response بنویسد. تا وقتی که یک کلاینت بتواند فرمت مرتب سازی دادهها را بخواند، میتواند آبجکت شما را deserialize کند. اکثر کلاینتها میتوانند XML یا JSON را تفسیر کنند. بعلاوه کلاینتها میتوانند فرمت مورد نظرشان را با تنظیم Accept header در پیام HTTP Request مشخص کنند.
بگذارید تا با ساختن مدلی ساده که یک محصول (product) را معرفی میکند شروع کنیم.
کلاس جدیدی در پوشه Models ایجاد کنید.
نام کلاس را به "Product" تغییر دهید، و خواص زیر را به آن اضافه کنید.
namespace ProductsApp.Models { public class Product { public int Id { get; set; } public string Name { get; set; } public string Category { get; set; } public decimal Price { get; set; } } }
افزودن یک کنترلر
در Web API کنترلرها آبجکت هایی هستند که درخواستهای HTTP را مدیریت کرده و آنها را به اکشن متدها نگاشت میکنند. ما کنترلری خواهیم ساخت که میتواند لیستی از محصولات، یا محصولی بخصوص را بر اساس شناسه برگرداند. اگر از ASP.NET MVC استفاده کرده اید، با کنترلرها آشنا هستید. کنترلرهای Web API مشابه کنترلرهای MVC هستند، با این تفاوت که بجای ارث بری از کلاس Controller از کلاس ApiController مشتق میشوند.
کنترلر جدیدی در پوشه Controllers ایجاد کنید.
در دیالوگ Add Scaffold گزینه Web API Controller - Empty را انتخاب کرده و روی Add کلیک کنید.
در دیالوگ Add Controller نام کنترلر را به "ProductsController" تغییر دهید و روی Add کلیک کنید.
توجه کنید که ملزم به ساختن کنترلرهای خود در پوشه Controllers نیستید، و این روش صرفا قراردادی برای مرتب نگاه داشتن ساختار پروژهها است. کنترلر ساخته شده را باز کنید و کد زیر را به آن اضافه نمایید.
using ProductsApp.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; namespace ProductsApp.Controllers { 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 IHttpActionResult GetProduct(int id) { var product = products.FirstOrDefault((p) => p.Id == id); if (product == null) { return NotFound(); } return Ok(product); } }
کنترلر ما دو متد برای دریافت محصولات تعریف میکند:
- متد GetAllProducts لیست تمام محصولات را در قالب یک <IEnumerable<Product بر میگرداند.
- متد GetProductById سعی میکند محصولی را بر اساس شناسه تعیین شده پیدا کند.
همین! حالا یک Web API ساده دارید. هر یک از متدهای این کنترلر، به یک یا چند URI پاسخ میدهند:
URI | Controller Method |
api/products/ | GetAllProducts |
api/products/id/ | GetProductById |
برای اطلاعات بیشتر درباره نحوه نگاشت درخواستهای HTTP به اکشن متدها توسط Web API به این لینک مراجعه کنید.
فراخوانی Web API با جاوا اسکریپت و jQuery
در این قسمت یک صفحه HTML خواهیم ساخت که با استفاده از AJAX متدهای Web API را فراخوانی میکند. برای ارسال درخواستهای آژاکسی و بروز رسانی صفحه بمنظور نمایش نتایج دریافتی از jQuery استفاده میکنیم.
در پنجره Solution Explorer روی نام پروژه کلیک راست کرده و گزینه Add, New Item را انتخاب کنید.
در دیالوگ Add New Item قالب HTML Page را انتخاب کنید و نام فایل را به "index.html" تغییر دهید.
حال محتوای این فایل را با لیست زیر جایگزین کنید.
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Product App</title> </head> <body> <div> <h2>All Products</h2> <ul id="products" /> </div> <div> <h2>Search by ID</h2> <input type="text" id="prodId" size="5" /> <input type="button" value="Search" onclick="find();" /> <p id="product" /> </div> <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.0.3.min.js"></script> <script> var uri = 'api/products'; $(document).ready(function () { // Send an AJAX request $.getJSON(uri) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); }); function formatItem(item) { return item.Name + ': $' + item.Price; } function find() { var id = $('#prodId').val(); $.getJSON(uri + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); } </script> </body> </html>
گرفتن لیستی از محصولات
برای گرفتن لیستی از محصولات، یک درخواست HTTP GET به آدرس "api/products/" ارسال کنید.
تابع getJSON یک درخواست آژاکسی ارسال میکند. پاسخ دریافتی هم آرایه ای از آبجکتهای JSON خواهد بود. تابع done در صورت موفقیت آمیز بودن درخواست، اجرا میشود. که در این صورت ما DOM را با اطلاعات محصولات بروز رسانی میکنیم.
$(document).ready(function () { // Send an AJAX request $.getJSON(apiUrl) .done(function (data) { // On success, 'data' contains a list of products. $.each(data, function (key, item) { // Add a list item for the product. $('<li>', { text: formatItem(item) }).appendTo($('#products')); }); }); });
گرفتن محصولی مشخص
برای گرفتن یک محصول توسط شناسه (ID) آن کافی است یک درخواست HTTP GET به آدرس "api/products/id/" ارسال کنید.
function find() { var id = $('#prodId').val(); $.getJSON(apiUrl + '/' + id) .done(function (data) { $('#product').text(formatItem(data)); }) .fail(function (jqXHR, textStatus, err) { $('#product').text('Error: ' + err); }); }
اجرای اپلیکیشن
اپلیکیشن را با F5 اجرا کنید. صفحه وب باز شده باید چیزی مشابه تصویر زیر باشد.
برای گرفتن محصولی مشخص، شناسه آن را وارد کنید و روی Search کلیک کنید.
اگر شناسه نامعتبری وارد کنید، سرور یک خطای HTTP بر میگرداند.
استفاده از F12 برای مشاهده درخواستها و پاسخ ها
هنگام کار با سرویسهای HTTP، مشاهدهی درخواستهای ارسال شده و پاسخهای دریافتی بسیار مفید است. برای اینکار میتوانید از ابزار توسعه دهندگان وب استفاده کنید، که اکثر مرورگرهای مدرن، پیاده سازی خودشان را دارند. در اینترنت اکسپلورر میتوانید با F12 به این ابزار دسترسی پیدا کنید. به برگه Network بروید و روی Start Capturing کلیک کنید. حالا صفحه وب را مجددا بارگذاری (reload) کنید. در این مرحله اینترنت اکسپلورر ترافیک HTTP بین مرورگر و سرور را تسخیر میکند. میتوانید تمام ترافیک HTTP روی صفحه جاری را مشاهده کنید.
به دنبال آدرس نسبی "api/products/" بگردید و آن را انتخاب کنید. سپس روی Go to detailed view کلیک کنید تا جزئیات ترافیک را مشاهده کنید. در نمای جزئیات، میتوانید headerها و بدنه درخواستها و پاسخها را ببینید. مثلا اگر روی برگه Request headers کلیک کنید، خواهید دید که اپلیکیشن ما در Accept header دادهها را با فرمت "application/json" درخواست کرده است.
اگر روی برگه Response body کلیک کنید، میتوانید ببینید چگونه لیست محصولات با فرمت JSON سریال شده است. همانطور که گفته شده مرورگرهای دیگر هم قابلیتهای مشابهی دارند. یک ابزار مفید دیگر Fiddler است. با استفاده از این ابزار میتوانید تمام ترافیک HTTP خود را مانیتور کرده، و همچنین درخواستهای جدیدی بسازید که این امر کنترل کاملی روی HTTP headers به شما میدهد.
قدمهای بعدی
ردیابی تغییرات در سمت کلاینت توسط Web API
فرض کنید میخواهیم از سرویسهای REST-based برای انجام عملیات CRUD روی یک Object graph استفاده کنیم. همچنین میخواهیم رویکردی در سمت کلاینت برای بروز رسانی کلاس موجودیتها پیاده سازی کنیم که قابل استفاده مجدد (reusable) باشد. علاوه بر این دسترسی دادهها توسط مدل Code-First انجام میشود.
در مثال جاری یک اپلیکیشن کلاینت (برنامه کنسول) خواهیم داشت که سرویسهای ارائه شده توسط پروژه Web API را فراخوانی میکند. هر پروژه در یک Solution مجزا قرار دارد، با این کار یک محیط n-Tier را شبیه سازی میکنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل مثال جاری مشتریان و شماره تماس آنها را ارائه میکند. میخواهیم مدلها و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند از آن استفاده کند. برای ساخت سرویس مذکور مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe4.Service تغییر دهید.
- کنترلر جدیدی با نام CustomerController به پروژه اضافه کنید.
- کلاسی با نام BaseEntity ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. تمام موجودیتها از این کلاس پایه مشتق خواهند شد که خاصیتی بنام TrackingState را به آنها اضافه میکند. کلاینتها هنگام ویرایش آبجکت موجودیتها باید این فیلد را مقدار دهی کنند. همانطور که میبینید این خاصیت از نوع TrackingState enum مشتق میشود. توجه داشته باشید که این خاصیت در دیتابیس ذخیره نخواهد شد. با پیاده سازی enum وضعیت ردیابی موجودیتها بدین روش، وابستگیهای EF را برای کلاینت از بین میبریم. اگر قرار بود وضعیت ردیابی را مستقیما از EF به کلاینت پاس دهیم وابستگیهای بخصوصی معرفی میشدند. کلاس DbContext اپلیکیشن در متد OnModelCreating به EF دستور میدهد که خاصیت TrackingState را به جدول موجودیت نگاشت نکند.
public abstract class BaseEntity { protected BaseEntity() { TrackingState = TrackingState.Nochange; } public TrackingState TrackingState { get; set; } } public enum TrackingState { Nochange, Add, Update, Remove, }
- کلاسهای موجودیت Customer و PhoneNumber را ایجاد کنید و کد آنها را مطابق لیست زیر تغییر دهید.
public class Customer : BaseEntity { public int CustomerId { get; set; } public string Name { get; set; } public string Company { get; set; } public virtual ICollection<Phone> Phones { get; set; } } public class Phone : BaseEntity { public int PhoneId { get; set; } public string Number { get; set; } public string PhoneType { get; set; } public int CustomerId { get; set; } public virtual Customer Customer { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاسی با نام Recipe4Context ایجاد کنید و کد آن را مطابق لیست زیر تغییر دهید. در این کلاس از یکی از قابلیتهای جدید EF 6 بنام "Configuring Unmapped Base Types" استفاده کرده ایم. با استفاده از این قابلیت جدید هر موجودیت را طوری پیکربندی میکنیم که خاصیت TrackingState را نادیده بگیرند. برای اطلاعات بیشتر درباره این قابلیت EF 6 به این لینک مراجعه کنید.
public class Recipe4Context : DbContext { public Recipe4Context() : base("Recipe4ConnectionString") { } public DbSet<Customer> Customers { get; set; } public DbSet<Phone> Phones { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { // Do not persist TrackingState property to data store // This property is used internally to track state of // disconnected entities across service boundaries. // Leverage the Custom Code First Conventions features from Entity Framework 6. // Define a convention that performs a configuration for every entity // that derives from a base entity class. modelBuilder.Types<BaseEntity>().Configure(x => x.Ignore(y => y.TrackingState)); modelBuilder.Entity<Customer>().ToTable("Customers"); modelBuilder.Entity<Phone>().ToTable("Phones"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe4ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Entity Framework Model Compatibility را غیرفعال میکند و به JSON serializer دستور میدهد که self-referencing loop خواص پیمایشی را نادیده بگیرد. این حلقه بدلیل رابطه bidirectional بین موجودیتهای Customer و PhoneNumber بوجود میآید.
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; ... }
- کلاسی با نام EntityStateFactory بسازید و کد آن را مطابق لیست زیر تغییر دهید. این کلاس مقدار خاصیت TrackingState که به کلاینتها ارائه میشود را به مقادیر متناظر کامپوننتهای ردیابی EF تبدیل میکند.
public static EntityState Set(TrackingState trackingState) { switch (trackingState) { case TrackingState.Add: return EntityState.Added; case TrackingState.Update: return EntityState.Modified; case TrackingState.Remove: return EntityState.Deleted; default: return EntityState.Unchanged; } }
- در آخر کد کنترلر CustomerController را مطابق لیست زیر بروز رسانی کنید.
public class CustomerController : ApiController { // GET api/customer public IEnumerable<Customer> Get() { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).ToList(); } } // GET api/customer/5 public Customer Get(int id) { using (var context = new Recipe4Context()) { return context.Customers.Include(x => x.Phones).FirstOrDefault(x => x.CustomerId == id); } } [ActionName("Update")] public HttpResponseMessage UpdateCustomer(Customer customer) { using (var context = new Recipe4Context()) { // Add object graph to context setting default state of 'Added'. // Adding parent to context automatically attaches entire graph // (parent and child entities) to context and sets state to 'Added' // for all entities. context.Customers.Add(customer); foreach (var entry in context.ChangeTracker.Entries<BaseEntity>()) { entry.State = EntityStateFactory.Set(entry.Entity.TrackingState); if (entry.State == EntityState.Modified) { // For entity updates, we fetch a current copy of the entity // from the database and assign the values to the orginal values // property from the Entry object. OriginalValues wrap a dictionary // that represents the values of the entity before applying changes. // The Entity Framework change tracker will detect // differences between the current and original values and mark // each property and the entity as modified. Start by setting // the state for the entity as 'Unchanged'. entry.State = EntityState.Unchanged; var databaseValues = entry.GetDatabaseValues(); entry.OriginalValues.SetValues(databaseValues); } } context.SaveChanges(); } return Request.CreateResponse(HttpStatusCode.OK, customer); } [HttpDelete] [ActionName("Cleanup")] public HttpResponseMessage Cleanup() { using (var context = new Recipe4Context()) { context.Database.ExecuteSqlCommand("delete from phones"); context.Database.ExecuteSqlCommand("delete from customers"); return Request.CreateResponse(HttpStatusCode.OK); } } }
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe4.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر تغییر دهید.
internal class Program { private HttpClient _client; private Customer _bush, _obama; private Phone _whiteHousePhone, _bushMobilePhone, _obamaMobilePhone; 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 completes await program.CleanupAsync(); program.CreateFirstCustomer(); // do not proceed until customer is added await program.AddCustomerAsync(); program.CreateSecondCustomer(); // do not proceed until customer is added await program.AddSecondCustomerAsync(); // do not proceed until customer is removed await program.RemoveFirstCustomerAsync(); // do not proceed until customers are fetched await program.FetchCustomersAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient { BaseAddress = new Uri("http://localhost:62799/") }; // 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 the cleanup method from the service _response = await _client.DeleteAsync("api/customer/cleanup/"); } private void CreateFirstCustomer() { // create customer #1 and two phone numbers _bush = new Customer { Name = "George Bush", Company = "Ex President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _whiteHousePhone = new Phone { Number = "212 222-2222", PhoneType = "White House Red Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bushMobilePhone = new Phone { Number = "212 333-3333", PhoneType = "Bush Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _bush.Phones.Add(_whiteHousePhone); _bush.Phones.Add(_bushMobilePhone); } private async Task AddCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _bush = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _bush.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _bush.Name, _bush.Phones.Count); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private void CreateSecondCustomer() { // create customer #2 and phone numbers _obama = new Customer { Name = "Barack Obama", Company = "President", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; _obamaMobilePhone = new Phone { Number = "212 444-4444", PhoneType = "Obama Mobile Phone", // set tracking state to 'Add' to generate a SQL Insert statement TrackingState = TrackingState.Add, }; // set tracking state to 'Modifed' to generate a SQL Update statement _whiteHousePhone.TrackingState = TrackingState.Update; _obama.Phones.Add(_obamaMobilePhone); _obama.Phones.Add(_whiteHousePhone); } private async Task AddSecondCustomerAsync() { // construct call to invoke UpdateCustomer action method in Web API service _response = await _client.PostAsync("api/customer/updatecustomer/", _obama, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created customer entity from service, which will include // database-generated Ids for all entities _obama = await _response.Content.ReadAsAsync<Customer>(); _whiteHousePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); _bushMobilePhone = _bush.Phones.FirstOrDefault(x => x.CustomerId == _obama.CustomerId); Console.WriteLine("Successfully created Customer {0} and {1} Phone Numbers(s)", _obama.Name, _obama.Phones.Count); foreach (var phoneType in _obama.Phones) { Console.WriteLine("Added Phone Type: {0}", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } private async Task RemoveFirstCustomerAsync() { // remove George Bush from underlying data store. // first, fetch George Bush entity, demonstrating a call to the // get action method on the service while passing a parameter var query = "api/customer/" + _bush.CustomerId; _response = _client.GetAsync(query).Result; if (_response.IsSuccessStatusCode) { _bush = await _response.Content.ReadAsAsync<Customer>(); // set tracking state to 'Remove' to generate a SQL Delete statement _bush.TrackingState = TrackingState.Remove; // must also remove bush's mobile number -- must delete child before removing parent foreach (var phoneType in _bush.Phones) { // set tracking state to 'Remove' to generate a SQL Delete statement phoneType.TrackingState = TrackingState.Remove; } // construct call to remove Bush from underlying database table _response = await _client.PostAsync("api/customer/updatecustomer/", _bush, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { Console.WriteLine("Removed {0} from database", _bush.Name); foreach (var phoneType in _bush.Phones) { Console.WriteLine("Remove {0} from data store", phoneType.PhoneType); } } else Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } private async Task FetchCustomersAsync() { // finally, return remaining customers from underlying data store _response = await _client.GetAsync("api/customer/"); if (_response.IsSuccessStatusCode) { var customers = await _response.Content.ReadAsAsync<IEnumerable<Customer>>(); foreach (var customer in customers) { Console.WriteLine("Customer {0} has {1} Phone Numbers(s)", customer.Name, customer.Phones.Count()); foreach (var phoneType in customer.Phones) { Console.WriteLine("Phone Type: {0}", phoneType.PhoneType); } } } else { Console.WriteLine("{0} ({1})", (int)_response.StatusCode, _response.ReasonPhrase); } } }
- در آخر کلاسهای Customer, Phone و BaseEntity را به پروژه کلاینت اضافه کنید. چنین کدهایی بهتر است در لایه مجزایی قرار گیرند و بین لایههای مختلف اپلیکیشن به اشتراک گذاشته شوند.
اگر اپلیکیشن کلاینت را اجرا کنید با خروجی زیر مواجه خواهید شد.
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید و روی خط اول کد فایل program.cs یک breakpoint قرار داده و آن را اجرا کنید. ابتدا آدرس سرویس را نگاشت میکنیم و از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
سپس توسط متد DeleteAsync که روی آبجکت HttpClient تعریف شده است اکشن متد Cleanup را روی سرویس فراخوانی میکنیم. این فراخوانی تمام دادههای پیشین را حذف میکند.
در قدم بعدی یک مشتری بهمراه دو شماره تماس میسازیم. توجه کنید که برای هر موجودیت مشخصا خاصیت TrackingState را مقدار دهی میکنیم تا کامپوننتهای Change-tracking در EF عملیات لازم SQL برای هر موجودیت را تولید کنند.
سپس توسط متد PostAsync که روی آبجکت HttpClient تعریف شده اکشن متد UpdateCustomer را روی سرویس فراخوانی میکنیم. اگر به این اکشن متد یک breakpoint اضافه کنید خواهید دید که موجودیت مشتری را بعنوان یک پارامتر دریافت میکند و آن را به context جاری اضافه مینماید. با اضافه کردن موجودیت به کانتکست جاری کل object graph اضافه میشود و EF شروع به ردیابی تغییرات آن میکند. دقت کنید که آبجکت موجودیت باید Add شود و نه Attach.
قدم بعدی جالب است، هنگامی که از خاصیت DbChangeTracker استفاده میکنیم. این خاصیت روی آبجکت context تعریف شده و یک <IEnumerable<DbEntityEntry را با نام Entries ارائه میکند. در اینجا بسادگی نوع پایه EntityType را تنظیم میکنیم. این کار به ما اجازه میدهد که در تمام موجودیت هایی که از نوع BaseEntity هستند پیمایش کنیم. اگر بیاد داشته باشید این کلاس، کلاس پایه تمام موجودیتها است. در هر مرحله از پیمایش (iteration) با استفاده از کلاس EntityStateFactory مقدار خاصیت TrackingState را به مقدار متناظر در سیستم ردیابی EF تبدیل میکنیم. اگر کلاینت مقدار این فیلد را به Modified تنظیم کرده باشد پردازش بیشتری انجام میشود. ابتدا وضعیت موجودیت را از Modified به Unchanged تغییر میدهیم. سپس مقادیر اصلی را با فراخوانی متد GetDatabaseValues روی آبجکت Entry از دیتابیس دریافت میکنیم. فراخوانی این متد مقادیر موجود در دیتابیس را برای موجودیت جاری دریافت میکند. سپس مقادیر بدست آمده را به کلکسیون OriginalValues اختصاص میدهیم. پشت پرده، کامپوننتهای EF Change-tracking بصورت خودکار تفاوتهای مقادیر اصلی و مقادیر ارسالی را تشخیص میدهند و فیلدهای مربوطه را با وضعیت Modified علامت گذاری میکنند. فراخوانیهای بعدی متد SaveChanges تنها فیلدهایی که در سمت کلاینت تغییر کرده اند را بروز رسانی خواهد کرد و نه تمام خواص موجودیت را.
در اپلیکیشن کلاینت عملیات افزودن، بروز رسانی و حذف موجودیتها توسط مقداردهی خاصیت TrackingState را نمایش داده ایم.
متد UpdateCustomer در سرویس ما مقادیر TrackingState را به مقادیر متناظر EF تبدیل میکند و آبجکتها را به موتور change-tracking ارسال میکند که نهایتا منجر به تولید دستورات لازم SQL میشود.
نکته: در اپلیکیشنهای واقعی بهتر است کد دسترسی دادهها و مدلهای دامنه را به لایه مجزایی منتقل کنید. همچنین پیاده سازی فعلی change-tracking در سمت کلاینت میتواند توسعه داده شود تا با انواع جنریک کار کند. در این صورت از نوشتن مقادیر زیادی کد تکراری جلوگیری خواهید کرد و از یک پیاده سازی میتوانید برای تمام موجودیتها استفاده کنید.
بررسی آماری واژههای بکار رفته در شاهنامه
مرحله اول: ایجاد ایندکس
using System; using System.Collections.Generic; using System.IO; using Lucene.Net.Analysis.Standard; using Lucene.Net.Documents; using Lucene.Net.Index; using Lucene.Net.Store; namespace ShaahnamehAnalysis { public static class CreateIndex { static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_CURRENT; static HashSet<string> getStopWords() { var result = new HashSet<string>(); var stopWords = new[] { "به", "با", "از", "تا", "و", "است", "هست", "هستم", "هستیم", "هستید", "هستند", "نیست", "نیستم", "نیستیم", "نیستند", "اما", "یا", "این", "آن", "اینجا", "آنجا", "بود", "باد", "برای", "که", "دارم", "داری", "دارد", "داریم", "دارید", "دارند", "چند", "را", "ها", "های", "می", "هم", "در", "باشم", "باشی", "باشد", "باشیم", "باشید", "باشند", "اگر", "مگر", "بجز", "جز", "الا", "اینکه", "چرا", "کی", "چه", "چطور", "چی", "چیست", "آیا", "چنین", "اینچنین", "نخست", "اول", "آخر", "انتها", "صد", "هزار", "میلیون", "ملیون", "میلیارد", "ملیارد", "یکهزار", "تریلیون", "تریلیارد", "میان", "بین", "زیر", "بیش", "روی", "ضمن", "همانا", "ای", "بعد", "پس", "قبل", "پیش", "هیچ", "همه", "واما", "شد", "شده", "شدم", "شدی", "شدیم", "شدند", "یک", "یکی", "نبود", "میکند", "میکنم", "میکنیم", "میکنید", "میکنند", "میکنی", "طور", "اینطور", "آنطور", "هر", "حال", "مثل", "خواهم", "خواهی", "خواهد", "خواهیم", "خواهید", "خواهند", "داشته", "داشت", "داشتی", "داشتم", "داشتیم", "داشتید", "داشتند", "آنکه", "مورد", "کنید", "کنم", "کنی", "کنند", "کنیم", "نکنم", "نکنی", "نکند", "نکنیم", "نکنید", "نکنند", "نکن", "بگو", "نگو", "مگو", "بنابراین", "بدین", "من", "تو", "او", "ما", "شما", "ایشان", "ی", "ـ", "هایی", "خیلی", "بسیار", "1", "بر", "l", "شود", "کرد", "کرده", "نیز", "خود", "شوند", "اند", "داد", "دهد", "گشت", "ز", "گفت", "آمد", "اندر", "چون", "بد", "چو", "همی", "پر", "سوی", "دو", "گر", "بی", "گرد", "زین", "کس", "زان", "جای", "آید" }; foreach (var item in stopWords) result.Add(item); return result; } public static void CreateShaahnamehIndex(string file = "shaahnameh.txt") { var directory = FSDirectory.Open(new DirectoryInfo(Environment.CurrentDirectory + "\\LuceneIndex")); var analyzer = new StandardAnalyzer(_version, getStopWords()); using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED)) { var section = string.Empty; foreach (var line in File.ReadAllLines(file)) { int result; if (int.TryParse(line, out result)) { var postDocument = new Document(); postDocument.Add(new Field("Id", result.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); postDocument.Add(new Field("Body", section, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS)); writer.AddDocument(postDocument); section = string.Empty; } else section += line; } writer.Optimize(); writer.Commit(); writer.Close(); directory.Close(); } } } }
با ایجاد ایندکسهای لوسین پیشتر در این سایت آشنا شدهاید . روش کار نیز همانند سابق است. اطلاعات خود را، به هر فرمتی که تهیه شده باید تبدیل به اشیاء Document لوسین کرد. برای مثال در اینجا فقط یک فایل txt داریم که تشکیل شده است از تمام صفحات. به ازای هر صفحه، یک شیء Document تهیه و نوشته خواهد شد. همچنین در تهیه ایندکس از یک سری از واژههای بسیار متداول مانند «از»، «به»، «اندر»، (stopWords) صرفنظر شده است.
مرحله دوم: ایجاد ابر واژهها
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Lucene.Net.Index; using Lucene.Net.Store; namespace ShaahnamehAnalysis { [DebuggerDisplay("{Frequency}, {Text}")] public class Tag { public string Text { set; get; } /// <summary> /// The frequency of a term is defined as the number of /// documents in which a specific term appears. /// </summary> public int Frequency { set; get; } } public static class WordsCloud { /// <summary> /// Create Words Cloud /// </summary> /// <param name="threshold">every term that appears in more than x Body</param> public static IList<Tag> Create(int threshold = 200) { var path = Environment.CurrentDirectory + "\\LuceneIndex"; var results = new List<Tag>(); var field = "Body"; IndexReader indexReader = IndexReader.Open(FSDirectory.Open(path ), true); var termFrequency = indexReader.Terms(); while (termFrequency.Next()) { if (termFrequency.DocFreq() >= threshold && termFrequency.Term.Field == field) { results.Add(new Tag { Text = termFrequency.Term.Text, Frequency = termFrequency.DocFreq() }); } } return results.OrderByDescending(x => x.Frequency).ToList(); } } }
پس از اینکه ایندکس لوسین تهیه شد، میتوان به مداخل موجود در آن توسط متد indexReader.Terms دسترسی یافت.
نکته جالب آن فراهم بودن DocFreq هر واژه ایندکس شده است (فرکانس تکرار واژه؛ تعداد اشیاء Document ایی که واژه مورد نظر در آنها تکرار شده است). برای مثال در اینجا اگر واژهای 200 بار یا بیشتر در صفحات مختلف شاهنامه تکرار شده باشد، به عنوان یک واژه پر اهمیت انتخاب شده و به ابر واژههای نهایی اضافه میگردد.
مرحله سوم: استفاده از نتایج
using System; using System.Diagnostics; using System.IO; using System.Linq; namespace ShaahnamehAnalysis { class Program { static void Main(string[] args) { CreateIndex.CreateShaahnamehIndex(); var wordsCloudList = WordsCloud.Create(); var data = wordsCloudList.Select(x => x.Text + ", " + x.Frequency) .Aggregate((s1, s2) => s1 + Environment.NewLine + s2); var output = "ShaahnamehAnalysis.txt"; File.WriteAllText(output, data); Process.Start(output); } } }
که نتیجه 15 مورد اول آن به صورت زیر است:
واژه | فرکانس
شاه, 1191
دل, 1088
سر, 1070
کار, 840
لشکر, 801
تخت, 755
روز, 745
ایران, 740
جهان, 724
مرد, 660
دست, 630
تاج, 623
نزدیک, 623
گیتی, 585
راه, 584
فایلهای کامل این مثال را از اینجا میتوانید دریافت کنید:
ShaahnamehAnalysis.zip
mkdir ps_cmdlet_with_csharp && cd "$_" dotnet new classlib dotnet add package PowerShellStandard.Library mv Class1.cs GetHelloCommand.cs
namespace ps_cmdlet_with_csharp; using System.Management.Automation; [Cmdlet(VerbsCommon.Get, "Hello")] public class GetHelloCommand : PSCmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] public string Name { get; set; } protected override void BeginProcessing() { WriteObject("Start processing"); } protected override void ProcessRecord() { WriteObject("Hello " + Name); } protected override void EndProcessing() { WriteObject("End processing"); } }
using System.Collections.ObjectModel; using System.Management.Automation.Host; namespace System.Management.Automation { public abstract class PSCmdlet : Cmdlet { protected PSCmdlet(); public PSEventManager Events { get; } public PSHost Host { get; } public CommandInvocationIntrinsics InvokeCommand { get; } public ProviderIntrinsics InvokeProvider { get; } public JobManager JobManager { get; } public JobRepository JobRepository { get; } public InvocationInfo MyInvocation { get; } public PagingParameters PagingParameters { get; } public string ParameterSetName { get; } public SessionState SessionState { get; } public PathInfo CurrentProviderLocation(string providerId); public Collection<string> GetResolvedProviderPathFromPSPath(string path, out ProviderInfo provider); public string GetUnresolvedProviderPathFromPSPath(string path); public object GetVariableValue(string name); public object GetVariableValue(string name, object defaultValue); } }
PS /> dotnet build PS /> Import-Module ./bin/Debug/net7.0/ps_cmdlet_with_csharp.dll
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ------ Cmdlet Get-Hello 1.0.0.0 ps_cmdlet_with_csharp
namespace ps_cmdlet_with_csharp; using System.Management.Automation; [Cmdlet(VerbsCommon.Get, "Hello")] public class GetHelloCommand : PSCmdlet { // as before } [Cmdlet(VerbsCommon.Get, "Greetings")] public class GetGreetingsCommand : PSCmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] public string Name { get; set; } protected override void BeginProcessing() { WriteObject("Start processing"); } protected override void ProcessRecord() { WriteObject("Greetings " + Name); } protected override void EndProcessing() { WriteObject("End processing"); } }
Import-Module: Invalid assembly public key.
<Project Sdk="Microsoft.NET.Sdk"> <!-- other tags --> <ItemGroup> <Compile Include="./GetHelloCommand.cs" /> </ItemGroup> </Project>
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ------ Cmdlet Get-Greetings 1.0.0.0 ps_cmdlet_with_csharp Cmdlet Get-Hello 1.0.0.0 ps_cmdlet_with_csharp
namespace ps_cmdlet_with_csharp; using System.Management.Automation; using System.Net.Http.Headers; [Cmdlet(VerbsCommon.Push, "SlackMessage")] [Alias("ssm")] [OutputType(typeof(string))] public class SlackMessageCommand : PSCmdlet { [Parameter(Mandatory = true)] [Alias("m")] public string Message { get; set; } [Parameter(Mandatory = true)] [Alias("t")] [ValidatePattern(@"xoxp-[0-9]{11}-[0-9]{12}-[0-9]{13}-[0-9a-zA-Z]{32}")] public string Token { get; set; } [Parameter(Mandatory = true)] [Alias("cid")] public string ChannelID { get; set; } protected async override void ProcessRecord() { base.ProcessRecord(); try { using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", this.Token); var values = new Dictionary<string, string> { { "channel", this.ChannelID }, { "text", this.Message } }; var content = new FormUrlEncodedContent(values); var response = await client.PostAsync("https://slack.com/api/chat.postMessage", content); var responseString = await response.Content.ReadAsStringAsync(); WriteObject("Message sent"); } catch (Exception ex) { WriteObject(ex.Message); } } }
PS /> $PROFILE | Get-Member -Type NoteProperty | Select-Object Name, Definitionbject Name, Definition Name Definition ---- ---------- AllUsersAllHosts string AllUsersAllHosts=/usr/local/microsoft/powershell/7/… AllUsersCurrentHost string AllUsersCurrentHost=/usr/local/microsoft/powershell… CurrentUserAllHosts string CurrentUserAllHosts=/Users/sirwanafifi/.config/powe… CurrentUserCurrentHost string CurrentUserCurrentHost=/Users/sirwanafifi/.config/p…
$SlackProjectPath = "/Users/sirwanafifi/Desktop/ps_cmdlet_with_csharp/bin/Debug/net7.0/ps_cmdlet_with_csharp.dll" Import-Module $SlackProjectPath
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ---- Cmdlet Push-SlackMessage 1.0.0.0 ps_…
Pro Business Applications with Silverlight 4 - APress
Silverlight 4 Data and Services Cookbook - Packt Publishing
Microsoft Silverlight 4 Business Application Development - Packt Publishing
و ...
با استفاده از WCF RIA Services یا حتی WCF Services و غیره دسترسی کامل برای مثال به entity framework خواهید داشت. همچنین کاربران شما مجددا به تجربهی کار با برنامههای دسکتاپ باز خواهند گشت، هر چند برنامه تحت وب است. قابلیت نصب خارج از مرورگر را هم دارد (مثل یک برنامه دسکتاپ معمولی).
- برای خارج از سازمان هم مشکلی نیست. چون برنامه شما یکبار فقط باید دریافت شود. بعد کش میشود و افزونهی سیلورلایت به صورت خودکار هربار چک میکند که آیا برنامه روی سرور تغییر کرده یا نه. اگر خیر از کش خوانده خواهد شد و کاربر راه دور معطل نخواهد شد. همچنین با استفاده از MEF (managed extensibility framework) میشود قسمتهای مختلف برنامه را به صورت افزونه درآورد. یعنی کاربری که نیاز به تمام قسمتها را ندارد خوب لزومی هم ندارد که همهی آنها را دریافت کند.
مدلهای AuditLog (اصلاحیه)و ActivityLog
- استفاده از جداول جدا برای هر کدام از جداول به صورتیکه یک ارتباط یک به چند مابین آنها برقرار است. از این جداول تحت عنوان HistoryTable یاد میشود.
- استفاده از یک جدول برای نگهداری تاریخچهی تغییرات جداولی که نیازمند این امکان هستند.
/// <summary> /// Represent The Operation's log /// </summary> public class AuditLog { #region Ctor /// <summary> /// Create One Instance Of <see cref="AuditLog"/> /// </summary> public AuditLog() { Id = SequentialGuidGenerator.NewSequentialGuid(); OperatedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// sets or gets identifier of AuditLog /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets Type of Modification(create,softDelet,Delete,update) /// </summary> public virtual AuditAction Action { get; set; } /// <summary> /// sets or gets description of Log /// </summary> public virtual string Description { get; set; } /// <summary> /// sets or gets when log is operated /// </summary> public virtual DateTime OperatedOn { get; set; } /// <summary> /// sets or gets Type Of Entity /// </summary> public virtual string Entity { get; set; } /// <summary> /// gets or sets Old value of Properties before modification /// </summary> public virtual string XmlOldValue { get; set; } /// <summary> /// gets or sets XML Base OldValue of Properties (NotMapped) /// </summary> public virtual XElement XmlOldValueWrapper { get { return XElement.Parse(XmlOldValue); } set { XmlOldValue = value.ToString(); } } /// <summary> /// gets or sets new value of Properties after modification /// </summary> public virtual string XmlNewValue { get; set; } /// <summary> /// gets or sets XML Base NewValue of Properties (NotMapped) /// </summary> public virtual XElement XmlNewValueWrapper { get { return XElement.Parse(XmlNewValue); } set { XmlNewValue = value.ToString(); } } /// <summary> /// gets or sets Identifier Of Entity /// </summary> public virtual string EntityId { get; set; } /// <summary> /// gets or sets user agent information /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets user's ip address /// </summary> public virtual string OperantIp { get; set; } #endregion #region NavigationProperties /// <summary> /// sets or gets log's creator /// </summary> public virtual User Operant { get; set; } /// <summary> /// sets or gets identifier of log's creator /// </summary> public virtual long OperantId { get; set; } #endregion } public enum AuditAction { Create, Update, Delete, SoftDelete, }
- Action : از نوع AdutiAction است و برای مشخص کردن نوع عملیاتی که انجام شده است، میباشد.
- Description : اگر نیاز باشد توضیحاتی اضافی ثبت شوند، از این خصوصیت استفاده میشود.
- Entity : مشخص کنندهی نام مدل خواهد بود. شاید بهتر بود از یک Enum استفاده میشد. ولی این سیستم به احتمال زیاد قرار است افزونه پذیر باشد و استفاده از Enum، یعنی محدودیت و این امکان وجود نخواهد داشت که سایر افزونهها بتوانند از مدل بالا استفاده کنند. برا ی مثال BlogPost , NewsItem , ForumPost , ...
- EntitytId : آی دی رکوردی است که تاریخچهی آن ثبت شده است. از آنجائیکه بعضی از موجودیتها دارای آی دی از نوع long و برخی دیگر Guid ، لذا ذخیرهی رشتهای آن مفید خواهد بود.
- XmlOldValue : در برگیرندهی مقدار (قبل از اعمال تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشتهای ذخیره شوند.
- XmlNewValue : در برگیرندهی مقدار (بعد از تغییرات) خصوصیاتی است که لازم است از یک موجودیت مشخص، در قالب XML رشتهای ذخیره شوند.
- Operant, OperantId: برای برقراری ارتباط یک به چند مابین مدل کاربر و مدل بالا در نظر گرفته شدهاند که به عنوان انجام دهندهی این تغییرات بوده است.
/// <summary> /// Represents Activity Log record /// </summary> public class ActivityLog { #region Ctor /// <summary> /// Create one instance of <see cref="ActivityLog"/> /// </summary> public ActivityLog() { Id = SequentialGuidGenerator.NewSequentialGuid(); OperatedOn=DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets the comment of this activity /// </summary> public virtual string Comment { get; set; } /// <summary> /// gets or sets the date that this activity was done /// </summary> public virtual DateTime OperatedOn { get; set; } /// <summary> /// gets or sets the page url . /// </summary> public virtual string Url { get; set; } /// <summary> /// gets or sets the title of page if Url is Not null /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets user agent information /// </summary> public virtual string Agent { get; set; } /// <summary> /// gets or sets user's ip address /// </summary> public virtual string OperantIp { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the type of this activity /// </summary> public virtual ActivityLogType Type{ get; set; } /// <summary> /// gets or sets the type's id of this activity /// </summary> public virtual Guid TypeId { get; set; } /// <summary> /// gets or sets User that done this activity /// </summary> public virtual User Operant { get; set; } /// <summary> /// gets or sets Id of User that done this activity /// </summary> public virtual long OperantId { get; set; } #endregion } /// <summary> /// Represents Activity Log Type Record /// </summary> public class ActivityLogType { #region Ctor /// <summary> /// Create one Instance of <see cref="ActivityLogType"/> /// </summary> public ActivityLogType() { Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// gets or sets the system name /// </summary> public virtual string Name{ get; set; } /// <summary> /// gets or sets the display name /// </summary> public virtual string DisplayName { get; set; } /// <summary> /// gets or sets the description /// </summary> public virtual string Description { get; set; } /// <summary> /// indicate this log type is enable for logging /// </summary> public virtual bool IsEnabled { get; set; } #endregion }
- Comment : توضیحات کوتاهی از اکشنی که کاربر انجام داده است.
- Url : آدرس صفحهای که این عملیات در آنجا انجام شده است. این خصوصیت نالپذیر میباشد.
- Title : عنوان صفحهای که این عملیات در آنجا انجام شده است؛ اگر Url نال نباشد.
- Operant , OperantId : برای برقراری ارتباط یک به چند بین کاربر و مدل فعالیتها در نظر گرفته شدهاند.
- Type : از نوع ActivityLogType پیاده سازی شده در بالا میباشد. با استفاده از مدل ActivityLogType میتوان مثلا لاگ فعالیت مربوط به بخش اخبار را غیر فعال کند یا بالعکس و از این موارد.
- Name : نام سیستمی آن است. برای مثال : Login ، NewsComment ، NewsItem و ...
- IsEnabled : نشان دهندهی این است که این نوع لاگ فعال است یا خیر.
کلاس پایه تمام مدلها (اصلاحیه)
/// <summary> /// Represents the entity /// </summary> /// <typeparam name="TForeignKey">type of user's Id that can be long or long? </typeparam> public abstract class Entity<TForeignKey> { #region Properties /// <summary> /// gets or sets date that this entity was created /// </summary> public virtual DateTime CreatedOn { get; set; } /// <summary> /// gets or sets Date that this entity was updated /// </summary> public virtual DateTime ModifiedOn { get; set; } /// <summary> /// gets or sets IP Address of Creator /// </summary> public virtual string CreatorIp { get; set; } /// <summary> /// gets or set IP Address of Modifier /// </summary> public virtual string ModifierIp { get; set; } /// <summary> /// indicate this entity is Locked for Modify /// </summary> public virtual bool ModifyLocked { get; set; } /// <summary> /// indicate this entity is deleted softly /// </summary> public virtual bool IsDeleted { get; set; } /// <summary> /// gets or sets information of user agent of modifier /// </summary> public virtual string ModifierAgent { get; set; } /// <summary> /// gets or sets information of user agent of Creator /// </summary> public virtual string CreatorAgent { get; set; } /// <summary> /// gets or sets date that this entity repoted last time /// </summary> public virtual DateTime? ReportedOn { get; set; } /// <summary> /// gets or sets counter for Content's report /// </summary> public virtual int ReportsCount { get; set; } /// <summary> /// gets or sets count of Modification Default is 1 /// </summary> public virtual int Version { get; set; } /// <summary> /// gets or sets action (create,update,softDelete) /// </summary> public virtual AuditAction Action { get; set; } /// <summary> /// gets or sets TimeStamp for prevent concurrency Problems /// </summary> public virtual byte[] RowVersion { get; set; } #endregion #region NavigationProperties /// <summary> /// gets ro sets User that Modify this entity /// </summary> public virtual User ModifiedBy { get; set; } /// <summary> /// gets ro sets Id of User that modify this entity /// </summary> public virtual TForeignKey ModifiedById { get; set; } /// <summary> /// gets ro sets User that Create this entity /// </summary> public virtual User CreatedBy { get; set; } /// <summary> /// gets ro sets User that Create this entity /// </summary> public virtual TForeignKey CreatedById { get; set; } #endregion } /// <summary> /// Represents the base Entity /// </summary> /// <typeparam name="TKey">type of Id</typeparam> /// <typeparam name="TForeignKey">type of User's Id that can be long or long?</typeparam> public abstract class BaseEntity<TKey,TForeignKey> : Entity<TForeignKey> { #region Properties /// <summary> /// gets or sets Identifier of this Entity /// </summary> public virtual TKey Id { get; set; } #endregion }
مدل سیستم آگاه سازی
/// <summary> /// Represents the Notification Record /// </summary> public class Notification { #region Ctor /// <summary> /// create one instance of <see cref="Notification"/> /// </summary> public Notification() { Id = SequentialGuidGenerator.NewSequentialGuid(); ReceivedOn = DateTime.Now; } #endregion #region Properties /// <summary> /// gets or sets identifier /// </summary> public virtual Guid Id { get; set; } /// <summary> /// indicate that this notification is read by owner /// </summary> public virtual bool IsRead { get; set; } /// <summary> /// gets or sets notification's text body /// </summary> public virtual string Message { get; set; } /// <summary> /// gets or sets page url that this notification is related with it /// </summary> public virtual string Url { get; set; } /// <summary> /// gets or sets date that this Notification Received /// </summary> public virtual DateTime ReceivedOn { get; set; } /// <summary> /// gets or sets the type of notification /// </summary> public virtual NotificationType Type { get; set; } #endregion #region NavigationProperties /// <summary> /// gets or sets the id of user that is owner of this notification /// </summary> public virtual long OwnerId { get; set; } /// <summary> /// gets or sets the user that is owner of this notification /// </summary> public virtual User Owner { get; set; } #endregion } public enum NotificationType { NewConversation, NewConversationReply, ... }
- IsRead : مشخص کنندهی این است که یک اطلاع رسانی خوانده شده است یا خیر. در آخر هر روز اطلاع رسانیهایی که دارای خصوصیت IsRead با مقدار true هستند، حذف خواهند شد.
- Url : برای مواردی که لازم است کاربر با کلیک بر روی آن به صفحهی خاصی هدایت شود.
- OwnerId, Owner : برای برقراری ارتباط یک به چند بین کاربر و مدل Notification در نظر گرفته شدهاند.
- Type : از نوع NotificationType و مشخص کنندهی نوع اطلاع رسانی میباشد .
مدل Observation
public class Observation { #region Ctor /// <summary> /// create one instance of <see cref="Observation"/> /// </summary> public Observation() { LastObservedOn = DateTime.Now; Id = SequentialGuidGenerator.NewSequentialGuid(); } #endregion #region Properties public virtual Guid Id { get; set; } /// <summary> /// gets or sets datetime of last visit /// </summary> public virtual DateTime LastObservedOn { get; set; } /// <summary> /// gets or sets Id Of section That user is observing the entity /// </summary> public virtual string SectionId { get; set; } /// <summary> /// gets or sets section That user is observing in it /// </summary> public virtual string Section { get; set; } #endregion #region NavigationProperites /// <summary> /// gets or sets user that observed the entity /// </summary> public virtual User Observer { get; set; } /// <summary> /// gets or sets identifier of user that observed the entity /// </summary> public virtual long ObserverId { get; set; } #endregion }
مدل صفحات داینامیک
/// <summary> /// represents one custom page /// </summary> public class Page : BaseEntity<long, long> { #region Properties /// <summary> /// gets or sets the blog pot body /// </summary> public virtual string Body { get; set; } /// <summary> /// gets or sets the content title /// </summary> public virtual string Title { get; set; } /// <summary> /// gets or sets value indicating Custom Slug /// </summary> public virtual string SlugUrl { get; set; } /// <summary> /// gets or sets meta title for seo /// </summary> public virtual string MetaTitle { get; set; } /// <summary> /// gets or sets meta keywords for seo /// </summary> public virtual string MetaKeywords { get; set; } /// <summary> /// gets or sets meta description of the content /// </summary> public virtual string MetaDescription { get; set; } /// <summary> /// gets or sets /// </summary> public virtual string FocusKeyword { get; set; } /// <summary> /// gets or sets value indicating whether the content use CanonicalUrl /// </summary> public virtual bool UseCanonicalUrl { get; set; } /// <summary> /// gets or sets CanonicalUrl That the Post Point to it /// </summary> public virtual string CanonicalUrl { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Follow for Seo /// </summary> public virtual bool UseNoFollow { get; set; } /// <summary> /// gets or sets value indicating whether the content user no Index for Seo /// </summary> public virtual bool UseNoIndex { get; set; } /// <summary> /// gets or sets value indicating whether the content in sitemap /// </summary> public virtual bool IsInSitemap { get; set; } /// <summary> /// gets or sets title for snippet /// </summary> public string SocialSnippetTitle { get; set; } /// <summary> /// gets or sets description for snippet /// </summary> public string SocialSnippetDescription { get; set; } /// <summary> /// gets or sets section's type that this page show on /// </summary> public virtual ShowPageSection Section { get; set; } /// <summary> /// indicate this page has not any body /// </summary> public virtual bool IsCategory { get; set; } /// <summary> /// gets or sets order for display forum /// </summary> public virtual int DisplayOrder { get; set; } #endregion #region NavigationProeprties /// <summary> /// gets or sets Parent of this page /// </summary> public virtual Page Parent { get; set; } /// <summary> /// gets or sets parent'id of this page /// </summary> public virtual long? ParentId { get; set; } /// <summary> /// get or set collection of page that they are children of this page /// </summary> public virtual ICollection<Page> Children { get; set; } #endregion } public enum ShowPageSection { Menu, Footer, SideBar }
نتیجهی تا این قسمت
آموزش سیلورلایت 4 - قسمتهای 21 تا 27
ASP.NET MVC #17
فیلترهای امنیتی ASP.NET MVC
ASP.NET MVC به همراه تعدادی فیلتر امنیتی توکار است که در این قسمت به بررسی آنها خواهیم پرداخت.
بررسی اعتبار درخواست (Request Validation) در ASP.NET MVC
ASP.NET MVC امکان ارسال اطلاعاتی را که دارای تگهای HTML باشند، نمیدهد. این قابلیت به صورت پیش فرض فعال است و جلوی ارسال انواع و اقسام اطلاعاتی که ممکن است سبب بروز حملات XSS Cross site scripting attacks شود را میگیرد. نمونهای از خطای نمایش داده:
A potentially dangerous Request.Form value was detected from the client (Html="<a>").
بنابراین تصمیم گرفته شده صحیح است؛ اما ممکن است در قسمتی از سایت نیاز باشد تا کاربران از یک ویرایشگر متنی پیشرفته استفاده کنند. خروجی این نوع ویرایشگرها هم HTML است. در این حالت میتوان صرفا برای متدی خاص امکانات Request Validation را به کمک ویژگی ValidateInput غیرفعال کرد:
[HttpPost]
[ValidateInput(false)]
public ActionResult CreateBlogPost(BlogPost post)
از ASP.NET MVC 3.0 به بعد راه حل بهتری به کمک ویژگی AllowHtml معرفی شده است. غیرفعال کردن ValidateInput ایی که معرفی شد، بر روی تمام خواص شیء BlogPost اعمال میشود. اما اگر فقط بخواهیم که مثلا خاصیت Text آن از مکانیزم بررسی اعتبار درخواست خارج شود، بهتر است دیگر از ویژگی ValidateInput استفاده نشده و به نحو زیر عمل گردد:
using System;
using System.Web.Mvc;
namespace MvcApplication14.Models
{
public class BlogPost
{
public int Id { set; get; }
public DateTime AddDate { set; get; }
public string Title { set; get; }
[AllowHtml]
public string Text { set; get; }
}
}
در اینجا فقط خاصیت Text مجاز به دریافت محتوای HTML ایی خواهد بود. اما خاصیت Title چنین مجوزی را ندارد. همچنین دیگر نیازی به استفاده از ویژگی ValidateInput غیرفعال شده نیز نخواهد بود.
به علاوه همانطور که در قسمتهای قبل نیز ذکر شد، خروجی Razor به صورت پیش فرض Html encoded است مگر اینکه صریحا آنرا تبدیل به HTML کنیم (مثلا استفاده از متد Html.Raw). به عبارتی خروجی Razor در حالت پیش فرض در مقابل حملات XSS مقاوم است مگر اینکه آگاهانه بخواهیم آنرا غیرفعال کنیم.
مطلب تکمیلی
مقابله با XSS ؛ یکبار برای همیشه!
فیلتر RequireHttps
به کمک ویژگی یا فیلتر RequireHttps، تمام درخواستهای رسیده به یک متد خاص باید از طریق HTTPS انجام شوند و حتی اگر شخصی سعی به استفاده از پروتکل HTTP معمولی کند، به صورت خودکار به HTTPS هدایت خواهد شد:
[RequireHttps]
public ActionResult LogOn()
{
}
فیلتر ValidateAntiForgeryToken
نوع دیگری از حملات که باید در برنامههای وب به آنها دقت داشت به نام CSRF یا Cross site request forgery معروف هستند.
برای مثال فرض کنید کاربری قبل از اینکه بتواند در سایت شما کار خاصی را انجام دهد، نیاز به اعتبار سنجی داشته باشد. پس از لاگین شخص و ایجاد کوکی و سشن معتبر، همین شخص به سایت دیگری مراجعه میکند که در آن مهاجمی بر اساس وضعیت جاری اعتبار سنجی او مثلا لینک حذف کاربری یا افزودن اطلاعات جدیدی را به برنامه ارائه میدهد. چون سشن شخص و کوکی مرتبط به سایت اول هنوز معتبر هستند و شخص سایت را نبسته است، «احتمال» اجرا شدن درخواست مهاجم بالا است (خصوصا اگر از مرورگرهای قدیمی استفاده کند).
بنابراین نیاز است بررسی شود آیا درخواست رسیده واقعا از طریق فرمهای برنامه ما صادر شده است یا اینکه شخصی از طریق سایت دیگری اقدام به جعل درخواستها کرده است.
برای مقابله با این نوع خطاها ابتدا باید داخل فرمهای برنامه از متد Html.AntiForgeryToken استفاده کرد. کار این متد ایجاد یک فیلد مخفی با مقداری منحصربفرد بر اساس اطلاعات سشن جاری کاربر است، به علاوه ارسال یک کوکی خودکار تا بتوان از تطابق اطلاعات اطمینان حاصل کرد:
@using (Html.BeginForm()) {
@Html.AntiForgeryToken()
در مرحله بعد باید فیلتر ValidateAntiForgeryToken جهت بررسی مقدار token دریافتی به متد ثبت اطلاعات اضافه شود:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateBlogPost(BlogPost post)
در اینجا مقدار دریافتی از فیلد مخفی فرم :
<input name="__RequestVerificationToken" type="hidden" value="C0iPfy/3T....=" />
با مقدار موجود در کوکی سایت بررسی و تطابق داده خواهند شد. اگر این مقادیر تطابق نداشته باشند، یک استثنا صادر شده و از پردازش اطلاعات رسیده جلوگیری میشود.
علاوه بر اینها بهتر است حین استفاده از متد و فیلتر یاد شده، از یک salt مجزا نیز به ازای هر فرم، استفاده شود:
[ValidateAntiForgeryToken(Salt="1234")]
@Html.AntiForgeryToken(salt:"1234")
به این ترتیب tokenهای تولید شده در فرمهای مختلف سایت یکسان نخواهند بود.
به علاوه باید دقت داشت که ValidateAntiForgeryToken فقط با فعال بودن کوکیها در مرورگر کاربر کار میکند و اگر کاربری پذیرش کوکیها را غیرفعال کرده باشد، قادر به ارسال اطلاعاتی به برنامه نخواهد بود. همچنین این فیلتر تنها در حالت HttpPost قابل استفاده است. این مورد هم در قسمتهای قبل تاکید گردید که برای مثال بهتر است بجای داشتن لینک delete در برنامه که با HttpGet ساده کار میکند، آنرا تبدیل به HttpPost نمود تا میزان امنیت برنامه بهبود یابد. از HttpGet فقط برای گزارشگیری و خواندن اطلاعات از برنامه استفاده کنید و نه ثبت اطلاعات.
بنابراین استفاده از AntiForgeryToken را به چک لیست اجباری تولید تمام فرمهای برنامه اضافه نمائید.
مطلب مشابه
Anti CSRF module for ASP.NET
فیلتر سفارشی بررسی Referrer
یکی دیگر از روشهای مقابله با CSRF، بررسی اطلاعات هدر درخواست ارسالی است. اگر اطلاعات Referrer آن با دومین جاری تطابق نداشت، به معنای مشکل دار بودن درخواست رسیده است. فیلتر سفارشی زیر میتواند نمونهای باشد جهت نمایش نحوه بررسی UrlReferrer درخواست رسیده:
using System.Web.Mvc;
namespace MvcApplication14.CustomFilter
{
public class CheckReferrerAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (filterContext.HttpContext != null)
{
if (filterContext.HttpContext.Request.UrlReferrer == null)
throw new System.Web.HttpException("Invalid submission");
if (filterContext.HttpContext.Request.UrlReferrer.Host != "mysite.com")
throw new System.Web.HttpException("This form wasn't submitted from this site!");
}
base.OnAuthorization(filterContext);
}
}
}
و برای استفاده از آن:
[HttpPost]
[CheckReferrer]
[ValidateAntiForgeryToken]
public ActionResult DeleteTask(int id)
نکتهای امنیتی در مورد آپلود فایلها در ASP.NET
هر جایی که کاربر بتواند فایلی را به سرور شما آپلود کند، مشکلات امنیتی هم از همانجا شروع خواهند شد. مثلا در متد Upload قسمت 11 این سری، منعی در آپلود انواع فایلها نیست و کاربر میتواند انواع و اقسام شلها را جهت تحت کنترل گرفتن سایت و سرور آپلود و اجرا کند. راه حل چیست؟
از همان روش امنیتی مورد استفاده توسط تیم ASP.NET MVC استفاده میکنیم. فایل web.config قرار گرفته در پوشه Views را باز کنید (نه فایل وب کانفیگ ریشه اصلی سایترا). چنین تنظیمی را میتوان مشاهده کرد:
برای IIS6 :
<system.web>
<httpHandlers>
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>
</system.web>
<system.webServer>
<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
</handlers>
</system.webServer>
تنظیم فوق، موتور اجرایی ASP.NET را در این پوشه خاص از کار میاندازد. به عبارتی اگر شخصی مسیر یک فایل aspx یا cshtml یا هر فایل قرار گرفته در پوشه Views را مستقیما در مرورگر خود وارد کند، با پیغام HttpNotFound مواجه خواهد شد.
این روش هم با ASP.NET Web forms سازگار است و هم با ASP.NET MVC؛ چون مرتبط است به موتور اجرایی ASP.NET که هر دوی این فریم ورکها برفراز آن معنا پیدا میکنند.
بنابراین در پوشه فایلهای آپلودی به سرور خود یک web.config را با محتوای فوق ایجاد کنید (و فقط باید مواظب باشید که این فایل حین آپلود فایلهای جدید، overwrite نشود. مهم!). به این ترتیب این مسیر دیگر از طریق مرورگر قابل دسترسی نخواهد بود (با هر محتوایی). سپس برای ارائه فایلهای آپلودی به کاربران از روش زیر استفاده کنید:
public ActionResult Download()
{
return File(Server.MapPath("~/Myfiles/test.txt"), "text/plain");
}
مزیت مهم روش ذکر شده این است که کاربران مجاز به آپلود هر نوع فایلی خواهند بود و نیازی نیست لیست سیاه تهیه کنید که مثلا فایلهایی با پسوندهای خاص آپلود نشوند (که در این بین ممکن است لیست سیاه شما کامل نباشد ...).
علاوه بر تمام فیلترهای امنیتی که تاکنون بررسی شدند، فیلتر دیگری نیز به نام Authorize وجود دارد که در قسمتهای بعدی بررسی خواهد شد.