services.AddOptions<BearerTokensOptions>() .Bind(configuration.GetSection("BearerTokens")) .Validate(bearerTokens => { return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes; }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
معرفی تنظیمات برنامه
فرض کنید فایل appsettings.json برنامه یک چنین محتوایی را دارد:
{ "ApiSettings": { "AllowedEndpoints": [ { "Name": "Service 1", "Timeout": 30, "Url": "http://service1.site.com" }, { "Name": "Service 2", "Timeout": 10, "Url": "https://service2.site.com" } ] } }
ایجاد مدلهای معادل تنظیمات JSON برنامه
بر اساس تعاریف JSON فوق، میتوان به مدلهای زیر رسید:
using System; using System.Collections.Generic; namespace FluentValidationSample.Models { public class AllowedEndpoint { public string Name { get; set; } public int Timeout { get; set; } public Uri Url { get; set; } } public class ApiSettings { public IEnumerable<AllowedEndpoint> AllowedEndpoints { get; set; } } }
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings)));
تعریف شرطهای اعتبارسنجی مدلهای تنظیمات برنامه
پس از مدلسازی تنظیمات برنامه و همچنین اتصال آن به <IOptions<ApiSettings، اکنون میخواهیم این مدلها، شرایط زیر را برآورده کنند:
- باید مدخل ApiSettings در فایل تنظیمات برنامه وجود خارجی داشته باشد.
- میخواهیم AllowedEndpointها نامدار بوده و هر نام نیز منحصربفرد باشد.
- مقادیر timeoutها باید بین 1 و 90 تعریف شده باشند.
- تمام URLها باید منحصربفرد باشند.
- تمام URLها باید HTTPS باشند.
برای این منظور میتوان تنظیمات زیر را توسط Fluent Validation تعریف کرد:
using System; using System.Linq; using FluentValidation; using FluentValidationSample.Models; namespace FluentValidationSample.ModelsValidations { public class ApiSettingsValidator : AbstractValidator<ApiSettings> { public ApiSettingsValidator() { RuleFor(apiSetting => apiSetting).NotNull() .WithMessage("مدخل ApiSettings تعریف نشدهاست."); RuleFor(apiSetting => apiSetting.AllowedEndpoints).NotNull().NotEmpty() .WithMessage("مدخل AllowedEndpoints تعریف نشدهاست."); When(apiSetting => apiSetting.AllowedEndpoints != null, () => { RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Name).Count() == endpoints.Count()) .WithMessage("نامهای سرویسها باید منحصربفرد باشند."); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => !endpoints.Any(endpoint => endpoint.Timeout > 90 || endpoint.Timeout < 1)) .WithMessage("مقدار timeout باید بین 1 و 90 باشد"); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.GroupBy(endpoint => endpoint.Url.ToString().ToLower()).Count() == endpoints.Count()) .WithMessage("آدرسهای سرویسها باید منحصربفرد باشند."); RuleFor(apiSetting => apiSetting.AllowedEndpoints) .Must(endpoints => endpoints.All(endpoint => endpoint.Url.Scheme.Equals("https", StringComparison.CurrentCultureIgnoreCase))) .WithMessage("تمام آدرسها باید HTTPS باشند."); }); } } }
- چگونه میتوان از تعریف و وجود یک مدخل فایل JSON، اطمینان حاصل کرد (اعمال RuleFor به کل مدل).
- چگونه میتوان اگر مدخلی تعریف شده بود، آنگاه برای آن اعتبارسنجی خاصی را تعریف کرد (متد When).
- چگونه میتوان شرایط سفارشی خاصی را مانند بررسی منحصربفرد بودنها، بررسی کرد (متد Must).
یکپارچه کردن اعتبارسنجی کتابخانهی FluentValidation با اعتبارسنجی توکار مدلهای تنظیمات برنامه توسط ASP.NET Core
در ابتدای بحث، امکان تعریف متد Validate را که از نگارش ASP.NET Core 2.2 اضافه شدهاست، مشاهده کردید:
services.AddOptions<BearerTokensOptions>() .Bind(configuration.GetSection("BearerTokens")) .Validate(bearerTokens => { return bearerTokens.AccessTokenExpirationMinutes < bearerTokens.RefreshTokenExpirationMinutes; }, "RefreshTokenExpirationMinutes is less than AccessTokenExpirationMinutes. Obtaining new tokens using the refresh token should happen only if the access token has expired.");
namespace Microsoft.Extensions.Options { public interface IValidateOptions<TOptions> where TOptions : class { ValidateOptionsResult Validate(string name, TOptions options); } }
در ادامه یک نمونه پیاده سازی جنریک IValidateOptions استاندارد ASP.NET Core را مشاهده میکنید:
using System.Linq; using FluentValidation; using Microsoft.Extensions.Options; namespace FluentValidationSample.ModelsValidations { public class AppConfigValidator<TOptions> : IValidateOptions<TOptions> where TOptions : class { private readonly IValidator<TOptions> _validator; public AppConfigValidator(IValidator<TOptions> validator) { _validator = validator; } public ValidateOptionsResult Validate(string name, TOptions options) { if (options is null) { return ValidateOptionsResult.Fail("Configuration object is null."); } var validationResult = _validator.Validate(options); return validationResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validationResult.Errors.Select(error => error.ToString())); } } }
در آخر روش معرفی آن به سیستم به صورت زیر است:
namespace FluentValidationSample.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.Configure<ApiSettings>(Configuration.GetSection(nameof(ApiSettings))); services.AddTransient<IValidateOptions<ApiSettings>, AppConfigValidator<ApiSettings>>();
namespace FluentValidationSample.Web.Controllers { public class HomeController : Controller { private readonly IUsersService _usersService; private readonly ApiSettings _apiSettings; public HomeController(IUsersService usersService, IOptions<ApiSettings> apiSettings) { _usersService = usersService; _apiSettings = apiSettings.Value; }
An unhandled exception occurred while processing the request. OptionsValidationException: تمام آدرسها باید HTTPS باشند.
کدهای کامل این سری را تا این قسمت از اینجا میتوانید دریافت کنید: FluentValidationSample-part05.zip
وهله سازی یک کلاس موجود توسط Reflection.Emit
کدهای کامل مثالی را در این زمینه در ادامه ملاحظه میکنید:
using System; using System.Reflection.Emit; namespace FastReflectionTests { public class Order { public string Name { set; get; } public Order() { Name = "Order01"; } } class Program { static void Main(string[] args) { var myMethod = new DynamicMethod(name: "myMethod", returnType: typeof(Order), parameterTypes: Type.EmptyTypes, m: typeof(Program).Module); var il = myMethod.GetILGenerator(); il.Emit(OpCodes.Newobj, typeof(Order).GetConstructor(Type.EmptyTypes)); il.Emit(OpCodes.Ret); var getOrderMethod = (Func<Order>)myMethod.CreateDelegate(typeof(Func<Order>)); Console.WriteLine(getOrderMethod().Name); } } }
سپس با دسترسی به ILGenerator سعی خواهیم کرد تا وهله جدیدی را از کلاس Order ایجاد کنیم. برای این منظور باید از OpCode جدیدی به نام Newobj استفاده کنیم که مخفف new object است. این OpCode برای عملکرد خود، نیاز به دریافت اشارهگری به سازنده کلاسی دارد که قرار است آنرا وهله سازی کند. در اینجا با Ret، کار متد را خاتمه داده و در ادامه برای استفاده از آن تنها کافی است یک delegate را ایجاد نمائیم.
بنابراین به مجموعه متدهای سریع خود، متد ذیل را نیز میتوان افزود:
public static Func<T> CreatFastObjectInstantiater<T>() { var t = typeof(T); var ctor = t.GetConstructor(Type.EmptyTypes); if (ctor == null) return null; var dynamicCtor = new DynamicMethod("_", t, Type.EmptyTypes, t, true); var il = dynamicCtor.GetILGenerator(); il.Emit(OpCodes.Newobj, ctor); il.Emit(OpCodes.Ret); return (Func<T>)dynamicCtor.CreateDelegate(typeof(Func<T>)); }
اگر علاقمند بودید که سرعت این روش را با روش متداول Activator.CreateInstance مقایسه کنید، مطلب زیر بسیار مفید است:
Creating objects - Perf implications
یک کاربرد مهم این مساله در نوشتن ORM مانندهایی است که قرار است لیستی جنریک را خیلی سریع تولید کنند؛ از این جهت که در حلقه DataReader آنها مدام نیاز است یک وهله جدید از شیء مدنظر ایجاد و مقدار دهی شود:
Mapping Datareader to Objects Using Reflection.Emit
Serialization #1
- انتقال اشیاء از طریق یک شبکه یا مرز یک نرم افزار .
- ذخیره اشیاء در یک فایل یا بانک اطلاعاتی.
- سریالیکننده قرارداد داده (Data Contract Serializer)
- سریالیکننده باینری
- سریالیکننده XML مبتنی بر صفت
- سریالیکننده اینترفیس IXmlSerializer
- DataContractSerializer، که اصطلاحاً loosely Coupled شده است به نوع سریالیکننده
Data Contract.
- NetDataContractSerializer که اصطلاحاً tightly Coupled شده است به نوع سریالیکننده
Data Contract.
public class News { public int Id; public string Body; public DateTime NewsDate; }
- فضای نام System.Runtime.Serialization را به کد برنامه اضافه کنیم.
- صفت [DataContract] را به نوعی که تعریف کردهایم، اضافه کنیم.
- صفت [DataMember] را به اعضایی که میخواهیم در سریالی شدن شرکتکنند، اضافه کنیم.
using System.Runtime.Serialization; using System.Xml; using System; using System.IO; namespace ConsoleApplication1 { [DataContract] public class News { [DataMember] public int Id; [DataMember] public string Body; [DataMember] public DateTime NewsDate; } }
var news = new News { Id = 1, Body = "NewsBody", NewsDate = new DateTime(2012, 10, 4) }; var ds = new DataContractSerializer(typeof (News)); using (Stream s=File.Create("News.Xml")) { ds.WriteObject(s, news);//سریالیکردن }
<News xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication7" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Body>NewsBody</Body><Id>1</Id><NewsDate>2012-10-04T00:00:00</NewsDate></News>
News deserializednews; using (Stream s = File.OpenRead("News.Xml")) { deserializednews = (News)ds.ReadObject(s);//Deserializing } Console.WriteLine(deserializednews.Body);
XmlWriterSettings settings = new XmlWriterSettings {Indent = true}; using (XmlWriter writer = XmlWriter.Create("News.Xml", settings)) { ds.WriteObject(writer, news); } System.Diagnostics.Process.Start("News.Xml");
<?xml version="1.0" encoding="utf-8"?> <News xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/ConsoleApplication7"> <Body>NewsBody</Body> <Id>1</Id> <NewsDate>2012-10-04T00:00:00</NewsDate> </News>
قبل ازاین مقاله، درباره راه اندازی و استفاده از کتابخانه Automapper بحث شده ولی موردی که شاید کمتر به آن توجه شده سرعت این نگاشت میباشد. در این مقاله با استفاده از نوشتن تست، این موضوع بررسی میشود.
کلاس ساده زیر را در نظر بگیرید که
برای مثال از سمت لایه دسترسی به داده گرفته شده است:
public enum PersonType { Real =0, Legal=1 } public class Person { public long PersonId { get; set; } public string Name { get; set; } public string Family { get; set; } public PersonType PersonType { get; set; } public Person(long personId, string name, string family, PersonType personType) { PersonId = personId; Name = name; Family = family; PersonType = personType; } }
از سازنده آن برای دریافت مقادیر مربوط به خصوصیات شیء استفاده شد.
در طرف دیگر نیز کلاسی برای نگاشت از آبجکت رسیده از سمت لایه داده ساخته میشود که برای نمایش در ویوها ایجاد شده است:
public class PersonDto { public long PersonId { get; set; } public string Name { get; set; } public string Family { get; set; } public PersonType PersonType { get; set; } public PersonDto(long personId, string name, string family, PersonType personType) { PersonId = personId; Name = name; Family = family; PersonType = personType; } }
همانطور که مشاهده میکنید در سازنده این کلاس نیز مقادیر خصوصیات، دریافت شدهاست.
برای ایجاد لیستی که در تست مورد استفاده قرار میگیرد نیز کلاس زیر را فراهم میکنیم:
public class PersonList { readonly List<Person> _list = new List<Person>(); public ReadOnlyCollection<Person> GetPersons() { if (!_list.Any()) { for (int i = 0; i < 100*1000; i++) { _list.Add(new Person(i + 1, "Person Name" + i, "Person Family" + i, (PersonType)(i % 2))); } } return _list.AsReadOnly(); } }
در اینجا برای
محسوس بودن نتیجه تست میتوان تعداد آبجکتهای لازم برای تست را تعیین کرد، فعلا 100
هزار آبجکت در نظر گرفته شده است:
for (int i = 0; i < 100*1000; i++) { _list.Add(new Person(i + 1, "Person Name" + i, "Person Family" + i, (PersonType)(i % 2))); }
برای
ارجاع به AutoMapper، با
استفاده از نیوگت، پکیج را به پروژه تست
ارجاع میدهیم: (در حال حاضر نسخه 5.1.1 استفاده شده است)
<package id="AutoMapper" version="5.1.1" targetFramework="net452" />
در سمت
تست نگاشت نیز از دو متد برای مقایسه استفاده میکنیم؛ یکی با استفاده از AutoMapper و دیگری بدون استفاده از آن:
[TestMethod] public void FillPersonDtoList_AutoMapperShouldMapPersonListToPersonDtoList_WhenLargeAmountOfPerson() { // arrange var personDtoList = new List<PersonDto>(); persons = new PersonList().GetPersons(); // act personDtoList = Mapper.Map<List<PersonDto>>(persons); //assert Assert.AreEqual(persons.Count, personDtoList.Count); } [TestMethod] public void FillPersonDtoList_UsingHandlyAssignment_WhenLargeAmountOfPerson() { // arrange var personDtoList = new List<PersonDto>(); persons = new PersonList().GetPersons(); // act foreach (var person in persons) { personDtoList.Add(new PersonDto(person.PersonId, person.Name, person.Family, person.PersonType)); } //assert Assert.AreEqual(persons.Count, personDtoList.Count); }
سرعت
نگاشت AutoMapper در نسخه حال حاضر تقریبا سه بار کندتر از استفاده معمول برای تهیه نگاشت
جدید از یک آبجکت است:
نکته: این تست با نسخه قدیمی تر(4.0.4.0) نیز انجام شده که این اختلاف سرعت نزدیک به 13 بار کندتر هم رسیده است.
پ.ن: سورس پروژه تست
public class Employee { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } }
<Grid> <StackPanel Name="Display"> <StackPanel Orientation="Horizontal"> <TextBlock>First Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock>Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"></TextBlock> </StackPanel> </StackPanel> </Grid>
public MainWindow() { InitializeComponent(); DataContext = Employee.GetEmployee(); }
public class Employee :INotifyPropertyChanged { public string Name { get; set; } public string Title { get; set; } public static Employee GetEmployee() { var emp = new Employee { Name = "Mani", Title = "CEO" }; return emp; } public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged ([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
private string _name; private string _title; public string Name { get { return _name; } set { _name = value; OnPropertyChanged(); } } public string Title { get { return _title; } set { _title = value; OnPropertyChanged(); } }
private Employee emp; public MainWindow() { InitializeComponent(); emp = new Employee() { Name = "Mani", Title = "CEO" }; DataContext = emp; }
<StackPanel Orientation="Horizontal"> <Button Click="btnClick" Width="70" Height="30" Content="Change"/> </StackPanel>
private void btnClick(object sender, RoutedEventArgs e) { emp.Name = "Amir"; emp.Title = "Manager"; }
<Grid> <StackPanel Name="Display" > <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Name, Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBox Margin="5,0,0,0" Text="{Binding Title,Mode=TwoWay}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Name :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Name}"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Margin="5,0,0,0">Title :</TextBlock> <TextBlock Margin="5,0,0,0" Text="{Binding Title}"/> </StackPanel> </StackPanel> </Grid>
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 به شما میدهد.
قدمهای بعدی
استخراج متن از فایلهای PDF توسط iTextSharp
PDFLibNet.PDFWrapper wrapper = new PDFLibNet.PDFWrapper(); wrapper.LoadPDF(pdfPath); string page1Text = wrapper.Pages[1].Text;
شروع به کار با RavenDB
- مروری بر مفاهیم مقدماتی NoSQL
- ردهها و انواع مختلف بانکهای اطلاعاتی NoSQL
- چه زمانی بهتر است از بانکهای اطلاعاتی NoSQL استفاده کرد و چه زمانی خیر؟
لطفا یکبار این پیشنیازها را پیش از شروع به کار مطالعه نمائید؛ چون بسیاری از مفاهیم پایهای و اصطلاحات مرسوم دنیای NoSQL در این سه قسمت بررسی شدهاند و از تکرار مجدد آنها در اینجا صرفنظر خواهد شد.
RavenDB چیست؟
RavenDB یک بانک اطلاعاتی سورس باز NoSQL سندگرای تهیه شده با دات نت است. ساختار کلی بانکهای اطلاعاتی NoSQL سندگرا، از لحاظ نحوه ذخیره سازی اطلاعات، با بانکهای اطلاعاتی رابطهای متداول، کاملا متفاوت است. در اینگونه بانکهای اطلاعاتی، رکوردهای اطلاعات، به صورت اشیاء JSON ذخیره میشوند. اشیاء JSON یا JavaScript Object Notation بسیار شبیه به anonymous objects سی شارپ هستند. JSON روشی است که توسط آن JavaScript اشیاء خود را معرفی و ذخیره میکند. به عنوان رقیبی برای XML مطرح است؛ نسبت به XML اندکی فشردهتر بوده و عموما دارای اسکیمای خاصی نیست و در بسیاری از اوقات تفسیر المانهای آن به مصرف کننده واگذار میشود.
در JSON عموما سه نوع المان پایه مشاهده میشوند:
- اشیاء که به صورت {object} تعریف میشوند.
- مقادیر "key":"value" که شبیه به نام خواص و مقادیر آنها در دات نت هستند.
- و آرایهها به صورت [array]
همچنین ترکیبی از این سه عنصر یاد شده نیز همواره میسر است. برای مثال، یک key مشخص میتواند دارای مقداری حاوی یک آرایه یا شیء نیز باشد.
JSON: JavaScript Object Notation document :{ key: "Value", another_key: { name: "embedded object" }, some_date: new Date(), some_number: 12 } C# anonymous object var Document = new { Key= "Value", AnotherKey= new { Name = "embedded object" }, SomeDate = DateTime.Now(), SomeNumber = 12 };
یک نکته مهم
اگر پیشنیازهای بحث را مطالعه کرده باشید، حتما بارها با این جمله که دنیای NoSQL از تراکنشها پشتیبانی نمیکند، برخورد داشتهاید. این مطلب در مورد RavenDB صادق نیست و این بانک اطلاعاتی NoSQL خاص، از تراکنشها پشتیبانی میکند. RavenDB در Document store خود ACID عملکرده و از تراکنشها پشتیبانی میکند. اما تهیه ایندکسهای آن بر مبنای مفهوم عاقبت یک دست شدن عمل میکند.
مجوز استفاده از RavenDB
هرچند مجموعه سرور و کلاینت RavenDB سورس باز هستند، اما این مورد به معنای رایگان بودن آن نیست. مجوز استفاده از RavenDB نوع خاصی به نام AGPL است. به این معنا که یا کل کار مشتق شده خود را باید به صورت رایگان و سورس باز ارائه دهید و یا اینکه مجوز استفاده از آنرا برای کارهای تجاری بسته خود خریداری نمائید. نسخه استاندارد آن نزدیک به هزار دلار است و نسخه سازمانی آن نزدیک به 2800 دلار به ازای هر سرور.
شروع به نوشتن اولین برنامه کار با RavenDB
ابتدا یک پروژه کنسول ساده را آغاز کنید. سپس کلاسهای مدل زیر را به آن اضافه نمائید:
using System.Collections.Generic; namespace RavenDBSample01.Models { public class Question { public string By { set; get; } public string Title { set; get; } public string Content { set; get; } public List<Comment> Comments { set; get; } public List<Answer> Answers { set; get; } public Question() { Comments = new List<Comment>(); Answers = new List<Answer>(); } } } namespace RavenDBSample01.Models { public class Comment { public string By { set; get; } public string Content { set; get; } } } namespace RavenDBSample01.Models { public class Answer { public string By { set; get; } public string Content { set; get; } } }
PM> Install-Package RavenDB.Client PM> Install-Package RavenDB.Server
اکنون به پوشه packages\RavenDB.Server.2.5.2700\tools مراجعه کرده و برنامه Raven.Server.exe را اجرا کنید تا سرور RavenDB شروع به کار کند. این سرور به صورت پیش فرض بر روی پورت 8080 اجرا میشود. از این جهت که در RavenDB نیز همانند سایر Document Stores مطرح، امکان دسترسی به اسناد از طریق REST API و Urlها وجود دارد.
البته لازم به ذکر است که RavenDB در 4 حالت برنامه کنسول (همین سرور فوق)، نصب به عنوان یک سرویس ویندوز NT، هاست شدن در IIS و حالت مدفون شده یا Embedded قابل استفاده است.
خوب؛ همین اندازه برای برپایی اولیه RavenDB کفایت میکند.
using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { session.Store(new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }); session.SaveChanges(); } } } } }
کار با ایجاد یک DocumentStore که به آدرس سرور اشاره میکند و کار مدیریت اتصالات را برعهده دارد، شروع خواهد شد. اگر نمیخواهید Url را درون کدهای برنامه مقدار دهی کنید، میتوان از فایل کانفیگ برنامه نیز برای این منظور کمک گرفت:
<connectionStrings> <add name="ravenDB" connectionString="Url=http://localhost:8080"/> </connectionStrings>
سپس با ایجاد Session در حقیقت یک Unit of work آغاز میشود که درون آن میتوان انواع و اقسام دستورات را صادر نمود و سپس در پایان کار، با فراخوانی SaveChanges، این اعمال ذخیره میگردند. در RavenDB یک سشن باید طول عمری کوتاه داشته باشد و اگر تعداد عملیاتی که در آن صادر کردهاید، زیاد است با خطای زیر متوقف خواهید شد:
The maximum number of requests (30) allowed for this session has been reached.
در یک برنامه واقعی، ایجاد DocumentStore یکبار در آغاز کار برنامه باید انجام گردد. اما هر سشن یا هر واحد کاری آن، به ازای تراکنشهای مختلفی که باید صورت گیرند، بر روی این DocumentStore، ایجاد شده و سپس بسته خواهند شد. برای مثال در یک برنامه ASP.NET، در فایل Global.asax در زمان آغاز برنامه، کار ایجاد DocumentStore انجام شده و سپس به ازای هر درخواست رسیده، یک سشن RavenDB ایجاد و در پایان درخواست، این سشن آزاد خواهد شد.
برنامه را اجرا کنید، سپس به کنسول سرور RavenDB که پیشتر آنرا اجرا نمودیم مراجعه نمائید تا نمایی از عملیات انجام شده را بتوان مشاهده کرد:
Raven is ready to process requests. Build 2700, Version 2.5.0 / 6dce79a Server started in 14,438 ms Data directory: D:\Prog\RavenDBSample01\packages\RavenDB.Server.2.5.2700\tools\Database\System HostName: <any> Port: 8080, Storage: Esent Server Url: http://localhost:8080/ Available commands: cls, reset, gc, q Request # 1: GET - 514 ms - <system> - 404 - /docs/Raven/Replication/Destinations Request # 2: GET - 763 ms - <system> - 200 - /queries/?&id=Raven%2FHilo%2Fquestions&id=Raven%2FServerPrefixForHilo Request # 3: PUT - 185 ms - <system> - 201 - /docs/Raven/Hilo/questions Request # 4: POST - 103 ms - <system> - 200 - /bulk_docs PUT questions/1
عملیات PUT حتما نیاز به یک Id از پیش مشخص دارد و این Id، پیشتر در سطری که Hilo در آن ذکر شده (یکی از الگوریتمهای محاسبه Id در RavenDB)، محاسبه گردیده است. برای نمونه در اینجا الگوریتم Hilo مقدار "questions/1" را به عنوان Id محاسبه شده بازگشت داده است.
در سطری که عملیات Post به آدرس bulk_docs سرور ارسال گردیده است، کار ارسال یکباره چندین شیء JSON برای کاهش رفت و برگشتها به سرور انجام میشود.
و برای کوئری گرفتن مقدماتی از اطلاعات ثبت شده میتوان نوشت:
using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); }
نگاهی به بانک اطلاعاتی ایجاد شده
در همین حال که سرور RavenDB در حال اجرا است، مرورگر دلخواه خود را گشوده و سپس آدرس http://localhost:8080 را وارد نمائید. بلافاصله، کنسول مدیریتی تحت وب این بانک اطلاعاتی که با سیلورلایت نوشته شده است، ظاهر خواهد شد:
و اگر بر روی هر سطر اطلاعات دوبار کلیک کنید، به معادل JSON آن نیز خواهید رسید:
اینبار برنامه را به صورت زیر تغییر دهید تا روابط بین کلاسها را نیز پیاده سازی کند:
using System; using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { var question = new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }; question.Answers.Add(new Answer { By = "users/Farid", Content = "بررسی میشود" }); session.Store(question); session.SaveChanges(); } using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); } } } } }
اگر برنامه فوق را اجرا کرده و به ساختار اطلاعات ذخیره شده نگاهی بیندازیم به شکل زیر خواهیم رسید:
نکته جالبی که در اینجا وجود دارد، عدم نیاز به join نویسی برای دریافت اطلاعات وابسته به یک شیء است. اگر سؤالی وجود دارد، پاسخهای به آن و یا سایر نظرات، یکجا داخل همان سؤال ذخیره میشوند و به این ترتیب سرعت دسترسی نهایی به اطلاعات بیشتر شده و همچنین قفل گذاری روی سایر اسناد کمتر. این مساله نیز به ذات NoSQL و یا غیر رابطهای RavenDB بر میگردد. در بانکهای اطلاعاتی NoSQL، مفاهیمی مانند کلیدهای خارجی، JOIN بین جداول و امثال آن وجود خارجی ندارند. برای نمونه اگر به کلاسهای مدلهای برنامه دقت کرده باشید، خبری از وجود Id در آنها نیست. RavenDB یک Document store است و نه یک Relation store. در اینجا کل درخت تو در توی خواص یک شیء دریافت و به صورت یک سند ذخیره میشود. به حاصل این نوع عملیات در دنیای بانکهای اطلاعاتی رابطهای، Denormalized data هم گفته میشود.
البته میتوان به کلاسهای تعریف شده خاصیت رشتهای Id را نیز اضافه کرد. در این حالت برای مثال در حالت فراخوانی متد Load، این خاصیت رشتهای، با Id تولید شده توسط RavenDB مانند "questions/1" مقدار دهی میشود. اما از این Id برای تعریف ارجاعات به سؤالات و پاسخهای متناظر استفاده نخواهد شد؛ چون تمام آنها جزو یک سند بوده و داخل آن قرار میگیرند.
IIS Hosting:
if (request.Properties.ContainsKey["MS_HttpContext"]) { var ctx = request.Properties["MS_HttpContext"] as HttpContextWrapper; if (ctx != null) { var ip = ctx.Request.UserHostAddress; } }
public static class HttpRequestMessageExtensions { private const string HttpContext = "MS_HttpContext"; public static string GetClientIpAddress(this HttpRequestMessage request) { if (request.Properties.ContainsKey(HttpContext)) { dynamic ctx = request.Properties[HttpContext]; if (ctx != null) { return ctx.Request.UserHostAddress; } } return null; } }
Self Hosting:
if (request.Properties.ContainsKey[RemoteEndpointMessageProperty.Name]) { var remote = request.Properties[RemoteEndpointMessageProperty.Name] as RemoteEndpointMessageProperty; if (remote != null) { var ip = remote.Address; } }
ترکیب حالتهای قبلی:
»در این صورت دیگر نیازی به اضافه کردن اسمبلی System.ServiceModel نیست.
public static class HttpRequestMessageExtensions { private const string HttpContext = "MS_HttpContext"; private const string RemoteEndpointMessage = "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; public static string GetClientIpAddress(this HttpRequestMessage request) { if (request.Properties.ContainsKey(HttpContext)) { dynamic ctx = request.Properties[HttpContext]; if (ctx != null) { return ctx.Request.UserHostAddress; } } if (request.Properties.ContainsKey(RemoteEndpointMessage)) { dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; if (remoteEndpoint != null) { return remoteEndpoint.Address; } } return null; } }
public class MyHandler : DelegatingHandler { private readonly HashSet<string> deniedIps; protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if (deniedIps.Contains(request.GetClientIpAddress())) { return Task.FromResult( new HttpResponseMessage( HttpStatusCode.Unauthorized ) ); } return base.SendAsync(request, cancellationToken); } }
Owin :
زمانی که از Owin برای هاست سرویسهای Web Api خود استفاده میکنید کمی روال انجام کار متفاوت خواهد شد. در این مورد نیز میتوانید از DelegatingHandlerها استفاده کنید. معرفی DelegatingHandler طراحی شده به Asp.Net PipeLine به صورت زیر خواهد بود:
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var config = new HttpConfiguration(); var routeHandler = HttpClientFactory.CreatePipeline( new HttpControllerDispatcher( config ), new DelegatingHandler[] { new MyHandler(), } ); config.Routes.MapHttpRoute( name: "Default", routeTemplate: "{controller}/{action}", defaults: null, constraints: null, handler: routeHandler ); config.EnsureInitialized(); appBuilder.UseWebApi( config ); } }
public class IpMiddleware : OwinMiddleware { private readonly HashSet<string> _deniedIps; public IpMiddleware(OwinMiddleware next, HashSet<string> deniedIps) : base(next) { _deniedIps = deniedIps; } public override async Task Invoke(OwinRequest request, OwinResponse response) { var ipAddress = (string)request.Environment["server.RemoteIpAddress"]; if (_deniedIps.Contains(ipAddress)) { response.StatusCode = 403; return; } await Next.Invoke(request, response); } }
در نهایت برای معرفی این Middleware طراحی شده به Application، مراحل زیر را انجام دهید.
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var config = new HttpConfiguration(); var deniedIps = new HashSet<string> {"192.168.0.100", "192.168.0.101"};
app.Use(typeof(IpMiddleware), deniedIps); appBuilder.UseWebApi( config ); } }