- هدف نحوهی نمایش این است که تزریق وابستگیهای ASP.NET Identity و امکانات تنظیم شدهی آن در این مثال، در یک کنترلر Web API هم کار میکند.
- اگر هدف صرفا استفاده از Single page applications و Web API است، روش استفادهی JWT متداولتر است.
• Join
• GroupJoin
• Zip
عملگر Join
این عملگر همانند inner join در SQL، دو مجموعه را بر اساس کلیدهای مرتبط که از طریق پارامترها به آن ارسال میشوند، با یکدیگر ترکیب میکند.
در عملیات Join، یک توالی ورودی که به آن توالی خارجی (Outer Sequence) گفته میشود با یک توالی دیگر که به آن توالی داخلی (Inner Sequence) میگوییم، بر اساس کلیدهای مشخص شده، ترکیب شده و یک توالی خروجی تولید میشود.
بررسی پارامترهای عملگر Join:
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector)
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Tinner,Tresult> resultSelector : یک عبارت Lambda است که ظاهر عناصر خروجی را مشخص میکند.
نکته : بطور کلی T در پارامترهای بالا معرف Generic Type Parameter میباشد؛ T==>Type (هر نوع دادهای که ما مشخص کنیم).
نکته : عملگر Join یک امضاء دیگر نیز دارد که اجازه مشخص کردن IEqualityComparer را میدهد.
کد زیر استفاده از عملگر Join را نشان میدهد. توجه داشته باشید که اعلان صریح نوع دادهها در عبارات Lambda نوشته شده، فقط برای روشنتر شدن فرآیند عملیات میباشد.
تعریف دو آرایه از کلاسهای Recipe و Review:
Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes // recipes توالی خارجی .Join(reviews, // reviewsتوالی داخلی (Recipe outerKey) => outerKey.Id, // کلید انخاب شده از توالی خارجی (Review innerKey) => innerKey.RecipeId, // کلید انتخاب شده از توالی داخلی // نحوه قالب بندی خروجی (recipe, review) => recipe.Name + " - " + review.ReviewText); foreach (string item in query) { Console.WriteLine(item); }
Mashed Potato - Tasty! Mashed Potato - Not nice :( Mashed Potato - Pretty good Crispy Duck - Too hard Crispy Duck - Loved it
var query = recipes.Join (reviews, outerKey => outerKey.Id, innerKey => innerKey.RecipeId, (recipe, review) => recipe.Name + " - " + review.ReviewText);
پیاده سازی توسط عبارتهای جستجو
کلمه کلیدی Join، در زمان استفاده از روش عبارتهای پرس و جو، مورد استفاده قرار گرفت. دستور Join در قسمت چهارم از این سری آموزشی بطور کامل بررسی شده است. کد زیر نحوه اجرای دستور Join را به روش عبارتهای پرس و جو، نشان میدهد.
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId select new //انواع بی نام { RecipeName = recipe.Name, RecipeReview = review.ReviewText }; foreach (var item in query) { Console.WriteLine(item.RecipeName + " - " + item.RecipeReview); }
عملگر GroupJoin
نحوه عملکرد عملگر GroupJoin، شبیه عملگر Join میباشد؛ با این تفاوت که خروجی حاصل از دستور GroupJoin، یک ساختار سلسله مراتبی میباشد. توالی خروجی، مجموعهای از گروهها میباشد که هر گروه، تشکیل شدهاست از عناصر توالی درونی.
بررسی پارامترهای عملگر GroupJoin
• <Inner IEnumerable<TInner : نشان دهنده توالی داخلی
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Ienumerable<Tinner>,Tresult> resultSelector : قالب بندی گروههای تولید شده خروجی را مشخص میکند
کد زیر استفاده از عملگر GroupJoin را نشان میدهد :
// outer sequence Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes .GroupJoin( reviews, (Recipe outerKey) => outerKey.Id,//outer key (Review innerKey) => innerKey.RecipeId,//inner key (Recipe recipe, IEnumerable<Review> rev )=>تعریف ساختار گروهها new { RecipeName = recipe.Name, Reviews = rev } ); foreach (var item in query) { Console.WriteLine($"Reviews for {item.RecipeName}"); foreach (var review in item.Reviews) { Console.WriteLine($" - {review.ReviewText}"); } }
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
پیاده سازی توسط عبارتهای جستجو
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId into reviewGroup select new //انواع بی نام { RecipeName = recipe.Name, Reviews = reviewGroup//کلیه بازخوردها مرتبط با یک دستور غذایی };
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
عملگر Zip
عملگر Zip، رفتاری متفاوت نسبت به عملگر GroupJoin و Join دارد و هیچ آیتمی را به عنوان کلید، از دو توالی دریافت نمیکند. عملگر Zip همه عناصر دو توالی را یک به یک، به ترتیب کنار هم قرار میدهد. مثل زیپ در دنیای واقعی که لبههای دو طرف زیپ را به هم میرساند.
public class Ingredient { public string Name { get; set; } public int Calories { get; set; } } string[] names = { "Flour", "Butter", "Sugar" }; int[] calories = { 100, 400, 500 }; IEnumerable<Ingredient> ingredients = names.Zip(calories, (name, calorie) => new Ingredient { Name = name, Calories = calorie }); foreach (var item in ingredients) { Console.WriteLine($"{item.Name} has {item.Calories} calories"); }
Flour has 100 calories Butter has 400 calories Sugar has 500 calories
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Zip، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
1- با استفاده از کلاس ApplicationUser، شیء ClaimsPrincipal را تولید میکند.
2- به این ClaimsPrincipal اطلاعاتی مانند ApplicationUser.Id و SecurityStamp اضافه میشوند.
3- در ادامه، ClaimsPrincipal به OWIN و کلاس CookieAuthenticationHandler آن ارسال میشود.
4- کار کلاس CookieAuthenticationHandler، تولید و تنظیم اطلاعاتی مانند تاریخ صدور کوکی، تاریخ انقضای آن، نوع کوکی، مانند ماندگار بودن یا امن بودن (HTTPS) و امثال آن است. حاصل این مراحله، تولید یک AuthenticationTicket است.
5- در آخر، AuthenticationTicket و ClaimsPrincipal به کلاس SecureDataFormat، برای ابتدا، serialize شدن اشیاء، رمزنگاری و در نهایت تبدیل آنها به فرمت base64، ارسال میشوند.
جزئیات تکمیلی مرحلهی آخر آن نیز به این ترتیب است:
AuthenticationTicket با استفاده از کلاس TicketSerializer سریالایز میشود. پس از آن یک memory stream تشکیل شده و اطلاعات ClaimsIdentity و AuthenticationTicket سریالایز شده به آن ارسال میشوند. این memory stream با استفاده از الگوریتم GZip فشرده شده و برای پردازش بیشتر بازگشت داده میشود. مرحلهی بعد، رمزنگاری اطلاعات فشرده سازی شدهاست. برای این منظور از کلاس DpapiDataProtector دات نت استفاده میکنند. پس از رمزنگاری، استریم نهایی با فرمت base64 برای درج در HTTP Response آماده خواهد شد.
سؤال: چرا کوکیهای یک کاربر معین لاگین شدهی توسط ASP.NET Identity، در مرورگرهای مختلف متفاوت است؟
هرچند اطلاعاتی مانند ApplicationUser.Id و SecurityStamp برای یک کاربر، در مرورگرهای مختلف یکسان هستند، اما در مرحلهی چهارم، ذکر شد که AuthenticationTicket دارای اطلاعات بیشتری مانند زمان تولید کوکی نیز هست. بنابراین اطلاعات نهایی رمزنگاری شدهی در این حالت که در زمانهای مختلفی تولید شدهاند، یکسان نخواهند بود.
سؤال: در ساب دومینهای مختلف دومین مشخصی، چندین برنامهی مختلف نصب شدهاند. چگونه میتوان از یک سیستم لاگین ASP.NET Identity برای تمام آنها استفاده کرد؟
برای این منظور نیاز هست خاصیت CookieDomain را به صورت صریح مقدار دهی کرد. برای اینکار فایل Startup.Auth.cs را گشوده و CookieAuthenticationOptions را تنظیم کنید:
var cookieAuthenticationOptions = new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Account/Login"), CookieDomain = ".mydomain.com" };
برای اینکار به کنسول IIS مراجعه کرده و گزینهی machine key آنرا بیابید. در این قسمت بر روی generate keys کلیک کرده و اطلاعات تولیدی را باید به تمام web.configهای موجود کپی کنید:
<machineKey validationKey="DAD9E2B0F9..." decryptionKey="ADD1C39C02..." validation="SHA1" decryption="AES" />
سؤال: برنامههای مختلفی بر روی یک دومین نصب هستند، اما قصد نداریم از سیستم اعتبارسنجی یکپارچهای برای تمام آنها استفاده کنیم. اما اگر در یکی لاگین کنیم، بلافاصله لاگین در برنامهی دوم منقضی میشود، چرا؟
شبیه به همین مساله با Forms Authentication هم وجود دارد. برای رفع آن باید نام کوکیهای هر برنامه را منحصربفرد کنید و از نام پیش فرض کوکیها استفاده نکنید تا بر روی یکدیگر بازنویسی نشوند. برای اینکار خاصیت CookieName شیء CookieAuthenticationOptions را جداگانه مقدار دهی کنید:
CookieName = "my-very-own-cookie-name"
سؤال: لاگین انجام شدهی در برنامهای که از ASP.NET Identity استفاده میکند، زود منقضی میشود؛ چرا؟
برای تنظیم صریح زمان انقضای کوکی ASP.NET Identity نیاز است خاصیت ExpireTimeSpan آنرا مقدار دهی کنید:
app.UseCookieAuthentication(new CookieAuthenticationOptions { ExpireTimeSpan = TimeSpan.FromHours(24.0), });
سؤال: کاربر سیستم ASP.NET Identity از سیستم خارج شدهاست (log off کرده) ولی هنوز میتوان از کوکی پیشین او برای اعتبارسنجی مجدد استفاده کرد. چطور میتوان این نقیصهی امنیتی را برطرف کرد؟
مشکل از اینجا است:
public ActionResult LogOff() { AuthenticationManager.SignOut(); return RedirectToAction("Index", "Home"); }
[HttpPost] [ValidateAntiForgeryToken] public async Task<ActionResult> LogOff() { var user = await UserManager.FindByNameAsync(User.Identity.Name); AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); await UserManager.UpdateSecurityStampAsync(user.Id); return RedirectToAction("Login", "Account"); }
همچنین بهتر است مقدار validateInterval مربوط به SecurityStampValidator.OnValidateIdentity که به صورت پیش فرض 30 دقیقه است را به مقدار کمتری مانند 5 دقیقه تغییر دهید (تنظیمات OnValidateIdentity مربوط به CookieAuthenticationOptions فایل آغارین برنامه). کار این تنظیم، بررسی اعتبار کوکی، در بازههای زمانی مشخص شدهاست.
پنل مدیریت ng2-admin-rtl - راستچین و فارسی سازی از تیم وبلی
- Webpack 2
- Angular 2
- Typescript
- Bootstrap 4
- ES 6
در قسمت قبلی بروز رسانی موجودیتهای منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه میتوان تغییرات موجودیتها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.
تشخیص تغییرات با Web API
فرض کنید میخواهیم از سرویسهای Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر میخواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت دادهها هم با مدل Code-First پیاده سازی میشود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی میکند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.
مدل زیر را در نظر بگیرید.
همانطور که میبینید مدل ما آژانسهای مسافرتی و رزرواسیون آنها را ارائه میکند. میخواهیم مدل و کد دسترسی دادهها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
- کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
- دو کلاس جدید با نامهای TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent { public TravelAgent() { this.Bookings = new HashSet<Booking>(); } public int AgentId { get; set; } public string Name { get; set; } public virtual ICollection<Booking> Bookings { get; set; } } public class Booking { public int BookingId { get; set; } public int AgentId { get; set; } public string Customer { get; set; } public DateTime BookingDate { get; set; } public bool Paid { get; set; } public virtual TravelAgent TravelAgent { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext { public Recipe3Context() : base("Recipe3ConnectionString") { } public DbSet<TravelAgent> TravelAgents { get; set; } public DbSet<Booking> Bookings { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId); modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents"); modelBuilder.Entity<Booking>().ToTable("Bookings"); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings> <add name="Recipe3ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال میکند. همچنین به JSON serializer میگوییم که self-referencing loop خاصیتهای پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیتها بوجود میآید.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); // The bidirectional navigation properties between related entities // create a self-referencing loop that breaks Web API's effort to // serialize the objects as JSON. By default, Json.NET is configured // to error when a reference loop is detected. To resolve problem, // simply configure JSON serializer to ignore self-referencing loops. GlobalConfiguration.Configuration.Formatters.JsonFormatter .SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore; ... }
- فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "ActionMethodSave", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional }); }
- در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController { // GET api/travelagent [HttpGet] public IEnumerable<TravelAgent> Retrieve() { using (var context = new Recipe3Context()) { return context.TravelAgents.Include(x => x.Bookings).ToList(); } } /// <summary> /// Update changes to TravelAgent, implementing Action-Based Routing in Web API /// </summary> public HttpResponseMessage Update(TravelAgent travelAgent) { using (var context = new Recipe3Context()) { var newParentEntity = true; // adding the object graph makes the context aware of entire // object graph (parent and child entities) and assigns a state // of added to each entity. context.TravelAgents.Add(travelAgent); if (travelAgent.AgentId > 0) { // as the Id property has a value greater than 0, we assume // that travel agent already exists and set entity state to // be updated. context.Entry(travelAgent).State = EntityState.Modified; newParentEntity = false; } // iterate through child entities, assigning correct state. foreach (var booking in travelAgent.Bookings) { if (booking.BookingId > 0) // assume booking already exists if ID is greater than zero. // set entity to be updated. context.Entry(booking).State = EntityState.Modified; } context.SaveChanges(); HttpResponseMessage response; // set Http Status code based on operation type response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent); return response; } } [HttpDelete] public HttpResponseMessage Cleanup() { using (var context = new Recipe3Context()) { context.Database.ExecuteSqlCommand("delete from [bookings]"); context.Database.ExecuteSqlCommand("delete from [travelagents]"); } return Request.CreateResponse(HttpStatusCode.OK); } }
- در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
- فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program { private HttpClient _client; private TravelAgent _agent1, _agent2; private Booking _booking1, _booking2, _booking3; private HttpResponseMessage _response; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { var program = new Program(); program.ServiceSetup(); // do not proceed until clean-up is completed await program.CleanupAsync(); program.CreateFirstAgent(); // do not proceed until agent is created await program.AddAgentAsync(); program.CreateSecondAgent(); // do not proceed until agent is created await program.AddSecondAgentAsync(); program.ModifyAgent(); // do not proceed until agent is updated await program.UpdateAgentAsync(); // do not proceed until agents are fetched await program.FetchAgentsAsync(); } private void ServiceSetup() { // set up infrastructure for Web API call _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")}; // add Accept Header to request Web API content negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } private async Task CleanupAsync() { // call cleanup method in service _response = await _client.DeleteAsync("api/travelagent/cleanup/"); } private void CreateFirstAgent() { // create new Travel Agent and booking _agent1 = new TravelAgent {Name = "John Tate"}; _booking1 = new Booking { Customer = "Karen Stevens", Paid = false, BookingDate = DateTime.Parse("2/2/2010") }; _booking2 = new Booking { Customer = "Dolly Parton", Paid = true, BookingDate = DateTime.Parse("3/10/2010") }; _agent1.Bookings.Add(_booking1); _agent1.Bookings.Add(_booking2); } private async Task AddAgentAsync() { // call generic update method in Web API service to add agent and bookings _response = await _client.PostAsync("api/travelagent/update/", _agent1, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include // database-generated Ids for each entity _agent1 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens"); _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void CreateSecondAgent() { // add new agent and booking _agent2 = new TravelAgent {Name = "Perry Como"}; _booking3 = new Booking { Customer = "Loretta Lynn", Paid = true, BookingDate = DateTime.Parse("3/15/2010")}; _agent2.Bookings.Add(_booking3); } private async Task AddSecondAgentAsync() { // call generic update method in Web API service to add agent and booking _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service _agent2 = await _response.Content.ReadAsAsync<TravelAgent>(); _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn"); Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)", _agent2.Name, _agent2.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private void ModifyAgent() { // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1 _agent2.Name = "Perry Como, Jr."; _agent2.Bookings.Add(_booking1); } private async Task UpdateAgentAsync() { // call generic update method in Web API service to update agent 2 _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter()); if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result; Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count); } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } private async Task FetchAgentsAsync() { // call Get method on service to fetch all Travel Agents and Bookings _response = _client.GetAsync("api/travelagent/retrieve").Result; if (_response.IsSuccessStatusCode) { // capture newly created travel agent from service, which will include Ids var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>(); foreach (var agent in agents) { Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count()); } } else Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase); } }
- در آخر کلاسهای TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژهها به اشتراک گذاشته شوند.
اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.
(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر MVC Web Controller دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله سایت در حال اجرا است و سرویسها قابل دسترسی هستند.
سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت میکنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست میکنیم که اطلاعات را با فرمت JSON بازگرداند.
بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی میکنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام دادههای پیشین را حذف میکند.
در قدم بعدی سه آبجکت جدید میسازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکتها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال میکنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت میکند و آن را به موجودیت TravelAgents در Context جاری اضافه مینماید. این کار آبجکت آژانس مسافرتی و تمام آبجکتهای فرزند آن را در حالت Added اضافه میکند و باعث میشود که context جاری شروع به ردیابی (tracking) آنها کند.
نکته: قابل ذکر است که اگر موجودیتهای متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکتهای خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر میکند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.
بعد از آن بر اساس مقدار خاصیت Id مشخص میکنیم که موجودیتها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر میدهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها میکنیم.
سپس تمام آبجکتهای فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال میکنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر میدهیم. در نهایت متد SaveChanges را فراخوانی میکنیم. در این مرحله برای موجودیتهای جدید اسکریپتهای Insert و برای موجودیتهای تغییر کرده اسکریپتهای Update تولید میشود. سپس کد وضعیت مناسب را به کلاینت بر میگردانیم. برای موجودیتهای اضافه شده کد وضعیت 201 (Created) و برای موجودیتهای بروز رسانی شده کد وضعیت 200 (OK) باز میگردد. کد 201 به کلاینت اطلاع میدهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر میدهد. هنگام تولید سرویسهای REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.
پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی میسازیم و آنها را به سرویس ارسال میکنیم. سپس نام آژانس مسافرتی دوم را تغییر میدهیم، و یکی از رزرواسیونها را از آژانس اولی به آژانس دومی منتقل میکنیم. اینبار هنگام فراخوانی متد Update تمام موجودیتها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر میدهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.
در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی میکند. این فراخوانی با کمک متد GetAsync انجام میشود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانسهای مسافرتی بهمراه رزرواسیونهای متناظرشان را دریافت میکند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری میکنیم.
دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز میگرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.
نکته دیگر آنکه در مثال جاری از قراردادهای توکار Web API برای نگاشت درخواستهای HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواستهای POST به متدهایی نگاشت میشوند که نام آنها با "Post" شروع میشود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشنهای واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویسهای REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواستهای متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.
تبدیلگر ایران سیستم به یونیکد
با این کدها:
// from http://www.microsoft.com/en-us/download/details.aspx?id=14839 var connectionString = "Provider=VFPOLEDB.1;Data Source=" + @"D:\path\JVJ100.DBF" + ";Password=;Collating Sequence=MACHINE"; using (var dbConnection = new OleDbConnection(connectionString)) { using (var dataAdapter = new OleDbDataAdapter("select FAMILY from JVJ100.DBF", dbConnection)) { using (var dataset = new DataSet()) { dataAdapter.Fill(dataset, "table1"); foreach (DataRow dataRow in dataset.Tables[0].Rows) { var familyIranSystem = dataRow[0] as string; var familyIranUnicode = ConvertTo.Unicode(familyIranSystem, 1256); if (!string.IsNullOrWhiteSpace(familyIranUnicode)) { } } } } }
دو نکته در اینجا مهم است:
الف) استفاده از درایور فاکس پرو
ب) code page استفاده شده 1256 است که باید در IranSystemConvertor تنظیم شود.
ایجاد یک پروژهی جدید Blazor WASM
برای تکمیل پیاده سازی قسمت سمت کلاینت پروژهی این سری، نیاز به یک پروژهی جدید Blazor WASM را داریم که میتوان آنرا با اجرای دستور dotnet new blazorwasm در یک پوشهی خالی، ایجاد کرد. کدهای این پروژه را میتوانید در پوشهی HotelManagement\BlazorWasm\BlazorWasm.Client فایل پیوستی انتهای بحث مشاهده کنید.
افزودن فایلهای جاوااسکریپتی مورد نیاز
شبیه به کاری که در مطلب «Blazor 5x - قسمت یازدهم - مبانی Blazor - بخش 8 - کار با جاوا اسکریپت» انجام دادیم، در اینجا هم قصد افزودن یکسری کتابخانهی جاوااسکریپتی و CSS ای را داریم که توسط LibMan آنها را مدیریت خواهیم کرد.
- بنابراین در ابتدا به پوشهی BlazorWasm.Client\wwwroot\css وارد شده و پوشههای پیشفرض bootstrap و open-iconic آنرا حذف میکنیم؛ چون تحت مدیریت هیچ package manager ای نیستند و در این حالت، مدیریت به روز رسانی و یا بازیابی آنها به صورت خودکار میسر نیست.
- سپس فایل wwwroot\css\app.css را هم ویرایش کرده و سطر زیر را از ابتدای آن حذف میکنیم:
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
dotnet tool update -g Microsoft.Web.LibraryManager.Cli libman init libman install bootstrap --provider unpkg --destination wwwroot/lib/bootstrap libman install open-iconic --provider unpkg --destination wwwroot/lib/open-iconic libman install jquery --provider unpkg --destination wwwroot/lib/jquery libman install toastr --provider unpkg --destination wwwroot/lib/toastr
- بعد از نصب بستههای ذکر شده، فایل wwwroot\index.html را به صورت زیر به روز رسانی میکنیم تا به مسیرهای جدید بستههای CSS و JS نصب شده، اشاره کند:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>BlazorWasm.Client</title> <base href="/" /> <link href="lib/toastr/build/toastr.min.css" rel="stylesheet" /> <link href="lib/open-iconic/font/css/open-iconic-bootstrap.min.css" rel="stylesheet" /> <link href="lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> <link href="BlazorWasm.Client.styles.css" rel="stylesheet" /> </head> <body> <div id="app">Loading...</div> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="lib/jquery/dist/jquery.min.js"></script> <script src="lib/toastr/build/toastr.min.js"></script> <script src="js/common.js"></script> <script src="_framework/blazor.webassembly.js"></script> </body> </html>
- محتویات فایل wwwroot\css\app.css را هم به صورت زیر تغییر میدهیم تا یک spinner و شیوه نامههای نمایش تصاویر، به آن اضافه شوند:
.valid.modified:not([type="checkbox"]) { outline: 1px solid #26b050; } .invalid { outline: 1px solid red; } .validation-message { color: red; } #blazor-error-ui { background: lightyellow; bottom: 0; box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); display: none; left: 0; padding: 0.6rem 1.25rem 0.7rem 1.25rem; position: fixed; width: 100%; z-index: 1000; } #blazor-error-ui .dismiss { cursor: pointer; position: absolute; right: 0.75rem; top: 0.5rem; } .spinner { border: 16px solid silver !important; border-top: 16px solid #337ab7 !important; border-radius: 50% !important; width: 80px !important; height: 80px !important; animation: spin 700ms linear infinite !important; top: 50% !important; left: 50% !important; transform: translate(-50%, -50%); position: absolute !important; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .room-image { display: block; width: 100%; height: 150px; background-size: cover !important; border: 3px solid green; position: relative; } .room-image-title { position: absolute; top: 0; right: 0; background-color: green; color: white; padding: 0px 6px; display: inline-block; }
window.ShowToastr = (type, message) => { if (type === "success") { toastr.success(message, "Operation Successful", { timeOut: 10000 }); } if (type === "error") { toastr.error(message, "Operation Failed", { timeOut: 10000 }); } };
- در قسمت 11، در بخش «کاهش کدهای تکراری فراخوانی متدهای جاوا اسکریپتی با تعریف متدهای الحاقی» آن، کلاس JSRuntimeExtensions را تعریف کردیم که سبب کاهش تکرار کدهای استفاده از تابع ShowToastr میشود. این فایلرا در پروژهی BlazorServer.App\Utils\JSRuntimeExtensions.cs این سری نیز استفاده کردیم. یا میتوان مجددا آنرا به پروژهی جاری کپی کرد؛ یا آنرا در یک پروژهی اشتراکی قرار داد. برای مثال اگر آنرا به پوشهی BlazorWasm.Client\Utils کپی کردیم، نیاز است فضای نام آنرا اصلاح کرده و سپس آنرا به انتهای فایل BlazorWasm.Client\_Imports.razor نیز اضافه کنیم تا در تمام کامپوننتهای برنامه قابل استفاده شود:
@using BlazorWasm.Client.Utils
تغییر و ساده سازی منوی برنامهی کلاینت
در برنامهی کلاینت جاری دیگر نمیخواهیم منوی پیشفرض سمت چپ صفحه را شاهد باشیم. به همین جهت ابتدا فایل Shared\MainLayout.razor را به صورت زیر ساده میکنیم:
@inherits LayoutComponentBase <NavMenu /> <div> @Body </div>
<nav class="navbar navbar-expand-sm navbar-dark bg-dark p-0"> <a class="navbar-brand mx-4" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse pr-2" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"></ul> <ul class="my-0 navbar-nav"> <li class="nav-item p-0"> <NavLink class="nav-link" href="registration"> <span class="p-2"> Register </span> </NavLink> </li> <li class="nav-item p-0"> <NavLink class="nav-link" href="login"> <span class="p-2"> Login </span> </NavLink> </li> </ul> </div> </nav>
تغییر محتوای صفحهی آغازین برنامه
صفحهی ابتدایی برنامه، یعنی کامپوننت Pages\Index.razor را نیز به صورت زیر تغییر میدهیم:
@page "/" <form> <div class="row p-0 mx-0 mt-4"> <div class="col-12 col-md-5 offset-md-1 pl-2 pr-2 pr-md-0"> <div class="form-group"> <label>Check In Date</label> <input type="text" class="form-control" /> </div> </div> <div class="col-8 col-md-3 pl-2 pr-2"> <div class="form-group"> <label>No. of nights</label> <select class="form-control"> @for (var i = 1; i <= 10; i++) { <option value="@i">@i</option> } </select> </div> </div> <div class="col-4 col-md-2 p-0 pr-2"> <div class="form-group"> <label> </label> <input type="submit" value="Go" class="btn btn-success btn-block" /> </div> </div> </div> </form>
تعریف View Model رابط کاربری Pages\Index.razor
پس از تعریف محتوای ثابت برنامه، اکنون نوبت به پویا سازی آن است. به همین جهت نیاز است مدلی را برای صفحهی آغازین برنامه تعریف کرد تا بتوان فرم آنرا به این مدل متصل کرد. این مدل چون مختص به برنامهی کلاینت است، آنرا در پوشهی جدید Models\ViewModels ایجاد میکنیم:
using System; namespace BlazorWasm.Client.Models.ViewModels { public class HomeVM { public DateTime StartDate { get; set; } = DateTime.Now; public DateTime EndDate { get; set; } public int NoOfNights { get; set; } = 1; } }
پس از این تعریف، بهتر است فضای نام آنرا نیز به فایل BlazorWasm.Client\_Imports.razor افزود، تا کار با آن در کامپوننتهای برنامه، سادهتر شود:
using BlazorWasm.Client.Models.ViewModels
- ابتدا فیلدی که ارائه کنندهی شیء ViewModel فرم است را تعریف میکنیم:
@code{ HomeVM HomeModel = new HomeVM(); }
<EditForm Model="HomeModel"> // ... </EditForm>
<InputDate min="@DateTime.Now.ToString("yyyy-MM-dd")" @bind-Value="HomeModel.StartDate" type="text" class="form-control" />
<select @bind="HomeModel.NoOfNights">
تعریف Local Storage سمت کلاینت
در ادامه میخواهیم اگر کاربری زمان شروع رزرو اتاقی را به همراه تعداد شب مدنظر، انتخاب کرد، با کلیک بر روی دکمهی Go، به یک صفحهی مشاهدهی جزئیات منتقل شود. بنابراین نیاز داریم تا اطلاعات انتخابی کاربر را به نحوی ذخیره سازی کنیم. برای یک چنین سناریوی سمت کلاینتی، میتوان از local storage استاندارد مرورگرها استفاده کرد که امکان کار آفلاین با برنامه را نیز فراهم میکند.
برای این منظور کتابخانهای به نام Blazored.LocalStorage طراحی شدهاست که پس از نصب آن توسط دستور زیر:
dotnet add package Blazored.LocalStorage
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddBlazoredLocalStorage(); // ... } } }
@using Blazored.LocalStorage
@page "/" @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime <EditForm Model="HomeModel" OnValidSubmit="SaveInitialData">
@code{ HomeVM HomeModel = new HomeVM(); private async Task SaveInitialData() { try { HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights); await LocalStorage.SetItemAsync("InitialRoomBookingInfo", HomeModel); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } }
برای مثال اگر تاریخ و عددی را انتخاب کنیم، نتیجهی حاصل از کلیک بر روی دکمهی Go را میتوان در قسمت Local storage مرورگر جاری مشاهده کرد:
البته با توجه به اینکه میخواهیم از کلید InitialRoomBookingInfo در سایر کامپوننتهای برنامه نیز استفاده کنیم، بهتر است آنرا به یک پروژهی مشترک مانند BlazorServer.Common که پیشتر نام نقشهایی مانند Admin را در آن تعریف کردیم، منتقل کنیم:
namespace BlazorServer.Common { public static class ConstantKeys { public const string LocalInitialBooking = "InitialRoomBookingInfo"; } }
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <ProjectReference Include="..\..\BlazorServer\BlazorServer.Common\BlazorServer.Common.csproj" /> </ItemGroup> </Project>
@using BlazorServer.Common
await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel);
در آخر قصد داریم با کلیک بر روی Go، به یک صفحهی جدید مانند نمایش لیست اتاقها هدایت شویم. به همین جهت کامپوننت جدید Pages\HotelRooms\HotelRooms.razor را ایجاد میکنیم:
@page "/hotel/rooms" <h3>HotelRooms</h3> @code { }
@page "/" @inject ILocalStorageService LocalStorage @inject IJSRuntime JsRuntime @inject NavigationManager NavigationManager @code{ HomeVM HomeModel = new HomeVM(); private async Task SaveInitialData() { try { HomeModel.EndDate = HomeModel.StartDate.AddDays(HomeModel.NoOfNights); await LocalStorage.SetItemAsync(ConstantKeys.LocalInitialBooking, HomeModel); NavigationManager.NavigateTo("hotel/rooms"); } catch (Exception e) { await JsRuntime.ToastrError(e.Message); } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-26.zip
حلقه های تکرار
انواع حلقههای تکرار
- حلقه تکرار for in
- حلقه تکرار for to
- حلقه تکرار while do
در ادامه به بررسی و پیاده سازی مثال برای هر سه حلقه میپردازیم
#1 حلقه for سادهlet list1 = [ 1; 5; 100; 450; 788 ] for i in list1 do printfn "%d" i
1 5 100 450 788
for i in 1 .. 2 .. 10 do printf "%d " i
1 3 5 7 9
for c in 'a' .. 'z' do printf "%c " c
a b c d e f g h i j k l m n o p q r s t u v w x y z
for i in 10 .. -1 .. 1 do printf "%d " i
10 9 8 7 6 5 4 3 2 1
let beginning x y = x - 2*y let ending x y = x + 2*y for i in (beginning x y) .. (ending x y) do printf "%d " i
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
for i = 1 to 10 do printf "%d " i
1 2 3 4 5 6 7 8 9 10
for i = 10 downto 1 do printf "%d " i
10 9 8 7 6 5 4 3 2 1
for i = (beginning x y) to (ending x y) do printf "%d " i
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#9 حلقه while do
ساختار کلی آن به صورت زیر است.
while test-expression do body-expression
open System let main() = let password = "monkey"//شناسه برای مقدار رمز عبور let mutable guess = String.Empty// شناسه برای حدس رمز عبور let mutable attempts = 0//تعداد دفعات تست while password <> guess && attempts < 3 do// تا زمانی که رمز عبور با حدس آن برابر نیست و تعداد دفعات تکرار کمتر از سه است Console.Write("What's the password? ")//چاپ پیغام در خروجی attempts <- attempts + 1//مقدار دفعات یکی افزایش مییابد guess <- Console.ReadLine()// حدس رمز عبور از ورودی دریافت میشود if password = guess then// اگر رمز عبور با حدس آن یکی بود Console.WriteLine("You got the password right!")// پیغام موفقیت else Console.WriteLine("You didn't guess the password")//پیغام عدم موفقیت Console.ReadKey(true) |> ignore//منتظر ورودی برای خروج از برنامه