الف) فعال سازی ارائهی فایلهای استاتیک
ب) فعال سازی ASP.NET MVC
ج) آشنایی با تغییرات مسیریابی
و مابقی آن صرفا یک سری نکات تکمیلی هستند که در ادامه آنها را بررسی خواهیم کرد.
تعریف مسیریابی کلی کنترلر
در اینجا همانند مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی»، میتوان در صورت نیاز، مسیریابی کلی کنترلر را توسط ویژگی Route بازنویسی کرد و برای مثال درخواستهای آنرا محدود به درخواستهایی کرد که با api/ شروع شوند:
[Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller { private readonly ILogger<TestController> _logger; public TestController(ILogger<TestController> logger) { _logger = logger; }
در مورد سرویس ثبت وقایع نیز در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 17 - بررسی فریم ورک Logging» بحث کردیم و از آن میتوان برای ثبت استثناءهای رخ داده استفاده کرد.
یک کنترلر ، اما با قابلیتهای متعدد
همانطور که ملاحظه میکنید، اینبار کلاس پایهی این کنترلر Test، همان Controller متداول ASP.NET MVC ذکر شدهاست و نه Api Controller سابق. تمام قابلیتهای موجود در ایندو توسط همان Controller ارائه میشوند.
هنوز پیش فرضهای سابق Web API برقرار هستند
در مثال ذیل که به نظر یک کنترلر ASP.NET MVC است،
- هنوز متد Get مربوط به Web API که به صورت پیش فرض به درخواستهای Get ختم شدهی به نام کنترلر پاسخ میدهد، برقرار است (متد IEnumerable<string> Get). برای مثال اگر شخصی در مرورگر، آدرس http://localhost:7742/api/test را درخواست دهد، متد Get اجرا میشود.
- در اینجا میتوان نوع خروجی متد را دقیقا از همان نوع اشیاء مدنظر، تعیین کرد؛ برای نمونه تعریف <IEnumerable<string در مثال زیر.
- مهم نیست که از return Json استفاده کنید و یا خروجی را مستقیما با فرمت <IEnumerable<string ارائه دهید.
- اگر نیاز به کنترل بیشتری بر روی HTTP Response Status بازگشتی داشتید، میتوانید از متدهایی مانند return Ok و یا return BadRequest در صورت بروز مشکلی استفاده نمائید. برای مثال در متد IActionResult GetEpisodes2، استثنای فرضی حاصل، ابتدا توسط سرویس ثبت وقایع ذخیره شده و در آخر یک BadRequest بازگشت داده میشود.
- تمام مسیریابیها را توسط ویژگی Route و یا نوعهای درخواستی مانند HttpGet، میتوان بازنویسی کرد؛ مانند مسیر /api/path1
- امکان محدود ساختن نوع پارامترهای دریافتی همانند متد Get(int page) ذیل، توسط ویژگیهای مسیریابی وجود دارد.
[Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller { private readonly ILogger<TestController> _logger; public TestController(ILogger<TestController> logger) { _logger = logger; } [HttpGet] public IEnumerable<string> Get() // http://localhost:7742/api/test { return new [] { "value1", "value2" }; } [HttpGet("{page:int}")] public IActionResult Get(int page) // http://localhost:7742/api/test/1 { return Json(new[] { "value3", "value4" }); } [HttpGet("/api/path1")] public IActionResult GetEpisodes1() // http://localhost:7742/api/path1 { return Json(new[] { "value5", "value6" }); } [HttpGet("/api/path2")] public IActionResult GetEpisodes2() // http://localhost:7742/api/path2 { try { // get data from the DB ... return Ok(new[] { "value7", "value8" }); } catch (Exception ex) { _logger.LogError("Failed to get data from the API", ex); return BadRequest(); } } }
[Route("api/[controller]")] public class ValuesController : Controller { // GET: api/values [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/values [HttpPost] public void Post([FromBody]string value) { } // PUT api/values/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string value) { } // DELETE api/values/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
یک نکته: اگر میخواهید خروجی Web API شما همواره JSON باشد، میتوانید ویژگی جدید Produces را به شکل ذیل به کلاس کنترلر اعمال کنید:
[Produces("application/json")] [Route("api/[controller]")] // http://localhost:7742/api/test public class TestController : Controller
تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API
فرض کنید مدل زیر را به برنامه اضافه کردهاید:
namespace Core1RtmEmptyTest.Models { public class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } }
using Core1RtmEmptyTest.Models; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class PersonController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult Index(Person person) { return Json(person); } } }
@section scripts { <script type="text/javascript"> $(function () { $.ajax({ type: 'POST', url: '/Person/Index', dataType: 'json', contentType: 'application/json; charset=utf-8', data: JSON.stringify({ FirstName: 'F1', LastName: 'L1', Age: 23 }), success: function (result) { console.log('Data received: '); console.log(result); } }); }); </script> }
همانطور که مشاهده میکنید، اگر در ابتدای این متد یک break-point قرار دهیم، اطلاعاتی را از سمت کاربر دریافت نکردهاست و مقادیر دریافتی نال هستند.
این مورد یکی از مهمترین تغییرات Model binding این نگارش از ASP.NET MVC با نگارشهای قبلی آن است. در اینجا اشیاء پیچیده از request body دریافت و bind نمیشوند و باید به نحو ذیل، محل دریافت و تفسیر آنها را دقیقا مشخص کرد:
public IActionResult Index([FromBody]Person person)
نکتهی مهم: حتی اگر FromBody را ذکر کنید ولی از JSON.stringify در سمت کاربر استفاده نکنید، باز هم نال دریافت خواهید کرد. بنابراین در این نگارش ذکر JSON.stringify نیز الزامی است.
حالتهای دیگر تغییرات Model Binding در ASP.NET Core
تا اینجا مشخص شد که اگر یک درخواست Ajax ایی را به سمت سرور یک برنامهی ASP.NET Core ارسال کنیم، به صورت پیش فرض به اشیاء پیچیدهی سمت سرور bind نمیشود و باید حتما ویژگی FromBody را نیز مشخص کرد تا اطلاعات را از request body واکشی کند (محل دریافت اطلاعات پیش فرض آن نامشخص است).
یک سؤال: اگر به سمت یک چنین اکشن متدی، اطلاعات فرمی را به حالت معمول ارسال کنیم، چه اتفاقی رخ خواهد داد؟
ارسال اطلاعات فرمها به سرور، همواره شامل دو تغییر ذیل است:
var dataType = 'application/x-www-form-urlencoded; charset=utf-8'; var data = $('form').serialize();
[HttpPost] public IActionResult Index([FromForm]Person person)
علت این مساله نیز بالا رفتن میزان امنیت سیستم است. در نگارشهای قبلی، تمام مکانها و حالتهای میسر جستجو میشوند و اگر یکی از آنها قابلیت تطابق با خواص شیء مدنظر را داشته باشد، کار binding به پایان میرسد. اما در اینجا با مشخص شدن محل دقیق منبع اطلاعات، دیگر سایر حالات جستجو نشده و سطح حمله کاهش پیدا میکند.
در اینجا باید مشخص کرد که دقیقا اطلاعاتی که قرار است به یک شیء پیچیده Bind شوند، آیا از یک Form تامین میشوند، یا از Body و یا از هدر، کوئری استرینگ، مسیریابی و یا حتی از یک سرویس.
تمام این حالتها مشخص هستند (برای مثال دریافت اطلاعات از هدر درخواست HTTP و انتساب آنها به خواص متناظری در شیء مشخص شده)، منهای FromService آن که به نحو ذیل عمل میکند:
در این حالت میتوان در سازندهی کلاس مدل خود، سرویسی را تزریق کرد و توسط آن خاصیتی را مقدار دهی نمود:
public class ProductModel { public ProductModel(IProductService prodService) { Value = prodService.Get(productId); } public IProduct Value { get; private set; } }
public async Task<IActionResult> GetProduct([FromServices]ProductModel product) { }
تغییر تنظیمات اولیهی خروجیهای ASP.NET Web API
در اینجا حالت ارائهی خروجی XML به صورت پیش فرض فعال نیست. اگر علاقمند به افزودن آن نیز باشید، نحوهی کار را در متد ConfigureServices کلاس آغازین برنامه در کدهای ذیل مشاهده میکنید:
public void ConfigureServices(IServiceCollection services) { services.AddMvc(options => { options.FormatterMappings.SetMediaTypeMappingForFormat("xml", new MediaTypeHeaderValue("application/xml")); }).AddJsonOptions(options => { options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Include; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; });
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; public partial class tblNews { public int tblNewsId { get; set; } public int tblCategoryId { get; set; } public string Title { get; set; } public string Description { get; set; } public System.DateTime RegDate { get; set; } public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
using System.Runtime.Serialization; namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; [DataContract] public partial class tblNews { [DataMember] public int tblNewsId { get; set; } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Description { get; set; } [DataMember] public System.DateTime RegDate { get; set; } [DataMember] public Nullable<bool> IsDeleted { get; set; } public virtual tblCategory tblCategory { get; set; } } }
namespace MyNewsWCFLibrary { using System; using System.Collections.Generic; using System.Runtime.Serialization; [DataContract] public partial class tblCategory { public tblCategory() { this.tblNews = new HashSet<tblNews>(); } [DataMember] public int tblCategoryId { get; set; } [DataMember] public string CatName { get; set; } [DataMember] public bool IsDeleted { get; set; } public virtual ICollection<tblNews> tblNews { get; set; } } }
BEGIN TRANSACTION GO ALTER TABLE dbo.tblNews DROP CONSTRAINT FK_tblNews_tblCategory GO ALTER TABLE dbo.tblCategory SET (LOCK_ESCALATION = TABLE) GO COMMIT BEGIN TRANSACTION GO CREATE TABLE dbo.Tmp_tblNews ( tblNewsId int NOT NULL IDENTITY (1, 1), tblCategoryId int NOT NULL, Title nvarchar(50) NOT NULL, Description nvarchar(MAX) NOT NULL, RegDate datetime NOT NULL, IsDeleted bit NOT NULL ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] GO ALTER TABLE dbo.Tmp_tblNews SET (LOCK_ESCALATION = TABLE) GO ALTER TABLE dbo.Tmp_tblNews ADD CONSTRAINT DF_tblNews_IsDeleted DEFAULT 0 FOR IsDeleted GO SET IDENTITY_INSERT dbo.Tmp_tblNews ON GO IF EXISTS(SELECT * FROM dbo.tblNews) EXEC('INSERT INTO dbo.Tmp_tblNews (tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted) SELECT tblNewsId, tblCategoryId, Title, Description, RegDate, IsDeleted FROM dbo.tblNews WITH (HOLDLOCK TABLOCKX)') GO SET IDENTITY_INSERT dbo.Tmp_tblNews OFF GO DROP TABLE dbo.tblNews GO EXECUTE sp_rename N'dbo.Tmp_tblNews', N'tblNews', 'OBJECT' GO ALTER TABLE dbo.tblNews ADD CONSTRAINT PK_tblNews PRIMARY KEY CLUSTERED ( tblNewsId ) WITH( STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] GO ALTER TABLE dbo.tblNews ADD CONSTRAINT FK_tblNews_tblCategory FOREIGN KEY ( tblCategoryId ) REFERENCES dbo.tblCategory ( tblCategoryId ) ON UPDATE NO ACTION ON DELETE NO ACTION GO COMMIT
- شماره تلفن، به صورت رشته کاراکتری
- آدرس، به عنوان رشته کاراکتری
- نام مسئول رسیدگی به تیکت، به صورت رشته کاراکتری
اما بعد از سپری شدن مدتی از توسعه محصول ممکن است اقلام اطلاعاتی خاصی بر روی هر یک از آیتمهای بالا نیاز شود. به طور مثال برای آدرس نیاز باشد اطلاعات استان و شهر جداگانه قابل ذخیره سازی و گزارش گیری باشند و یا در کنار نام مسئول رسیدگی به تیکت، شماره تلفن او نیز وجود داشته باشد.
در چنین شرایطی، یک اقدام ممکن، افزودن اقلام اطلاعاتی مورد نیاز در همان مکان آیتم قبلی است؛ به طور مثال اگر نام مسئول بر روی موجودیت تیکت باشد، شماره تلفن مسئول نیز در همان موجودیت تیکت اضافه شود.
راه حل مناسبتر برای حل این نوع مشکلات ایجاد کلاس خاص آیتم اطلاعاتی و استفاده از شیء آن بهجای مقدار مربوطه است. به طور مثال به طراحی زیر دقت نمایید. در طراحی زیر کلاس دیگری به نام Agent ایجاد و در کلاس تیکت از آن استفاده کردهایم.
این بازسازی کد دو مزیت کلی دارد:
- راه را برای توسعه آینده آیتمهای دادهای باز میکند
- از تکرار آیتمهای دادهای جلوگیری میکند (به طور مثال زمانیکه از پایگاه دادههای رابطهای جهت ذخیره سازی، استفاده شود)
public boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); }
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
البته توصیه ما نیز استفاده از این کد هست و توصیه میشود که استثناءها یا وضعیت خروجیها را کنترل نمایید. روشهای زیر تنها تحلیل کوچکی برای بررسی وضعیت اینترنت است یا اینکه واقعا به این بررسیهای نیاز داشته باشید. در غیر این صورت برای بسیاری از برنامهها همین کد کفایت میکند. راه حلهای پایین مواردی است که از تجربه خود به دست آوردهام تا شاید راهنمایی برای افرادی باشد که میخواهند این کار را آغاز کنند تا در وقتشان صرفه جویی شود.
مثالهای زیر وضعیتهایی را نشان میدهند که شبکه موجود است ولی اینترنتی در دسترس نیست:
- مودم وای فای را فعال کرده است، ولی اینترنت را در اختیار ندارد که این علل میتواند عدم ثابت شدن چراغ DSL یا راه اندازی مجدد مودم باشد که وای فای زودتر از DSL فعال میشود و یا اینکه اشتراک شما تمام شده است یا در شبکه به مشکل برخورد کردهاید.
- شما از طریق mobile data قصد اتصال دارید. در این حالت یا اعتبار شما پایان یافته است یا شبکه آنان دچار اختلال است.
- شما در یک محیط اداری هستید که به عنوان مثال سیستمشان توسط روترهای میکروتیک هدایت میشود. در این حالت شما میتوانید وارد شبکه بدون کلمه عبور آنان شوید. ولی نیاز دارید که حتما صفحه لاگین را رد نمایید تا اینترنت در اختیار شما قرار بگیرد.
در همه مثالهای بالا اینترنتی وجود ندارد، ولی تکه کد بالا true را برخواهد گرداند.
public boolean IsInternetConnected() { try { Process ipProcess = Runtime.getRuntime().exec("/system/bin/ping -c 1 4.2.2.4"); int exitValue = ipProcess.waitFor(); return (exitValue == 0); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; }
System.Diagnostics.Process.Start("processName");
فرمان یا دستور بالا به شرح زیر است:
در سیستم عامل لینکوس تمام برنامههای سیستمی مورد نیاز، در شاخه bin قرار میگیرند و پینگ، یکی از آن هاست. سوییچ c هم که مخفف count است، به معنی تعداد درخواستهای یک اکو است که در این دستور گفتهایم تنها یک درخواست اکو echo ارسال کن. (ااطلاعات بیشتر در مورد دستور پینگ ).
در خط بعدی از آنجا که این دستور، یک دستور زمان بر است، باید مدتی در این کد توقف شود تا مقدار مورد نظر دریافت شود. در صورتی که مقدار 0 بازگردانده شود، اکو پاسخ داده شده است و یعنی اینکه شما به اینترنت متصلید. (مشاهده کدهای وضعیتی ICMP )
وجود catchهای بالا الزامی است از آنجا که متدهای استفاده شده توسط استثناءهای زیر throw شدهاند، جاوا شما را ملزم به استفاده از catchهای این استثناها خواهد کرد.
public Process exec(String prog) throws java.io.IOException { return exec(prog, null, null); }
public abstract int waitFor() throws InterruptedException;
برای اجرا این تکه کد شما نیاز به مجوز اتصال به اینترنت دارید:
<uses-permission android:name="android.permission.INTERNET"/>
نکته مهم اینکه نگران اجرای این دستور در گوشی کاربر نباشید. این دستور نیاز به مجوز روت ندارد.
تکه کد زیر صفحه گوگل را درخواست میکند و در نهایت وضعیت کد http آن را دریافت میکنیم و اگر این کد وضعیت برابر 200 بود به این معنی است که اینترنت متصل میباشد. ولی در یک سیستم میکروتیک که هنوز وارد سیستم آن نشده باشید، به صفحه لاگین هدایت میشوید و وضعیت دیگری را دریافت خواهید مانند آدرس درخواستی شما redirect شده است یا اینکه باز هم کد 200 را دریافت میکنید که در بیشتر حالات هم به همین شکل است. برای رفع این مسئله بهتر است url فعلی را با url درخواستی مطابقت دهیم. برای این قضیه گوگل در بخش Handling Network Sign-On این صفحه چنین کدی را پیشنهاد داده است:
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { InputStream in = new BufferedInputStream(urlConnection.getInputStream()); if (!url.getHost().equals(urlConnection.getURL().getHost())) { // we were redirected! Kick the user out to the browser to sign on? ... } finally { urlConnection.disconnect(); } }
ولی با توجه به تحقیقات و مشاهداتی که کردهام، در بعضی از گوشیها این کد کارکرد مناسبی ندارد و برای خودم هم پاسخی دریافت نکردم. اگر واقعا باز هم مصر هستید که این وضعیت را بررسی کنید، میتواند بررسی یک url با محتوای خاص باشد و بعد از دریافت این صفحه محتوای آن را بررسی کنید.
ولی با این همه بسیاری از برنامهها از همان تکه کد بالا استفاده میکنند و با مدیریت استثناءها سعی در جلوگیری از خطا دارند. در غیر این صورت شما باید مدام در حال بررسی وضعیت اینترنت به شکل بالا باشید که بسیار زمان بر خواهد شد.
در صورتی که شما روش بهتری را برای بررسی وضعیت اینترنت دارید یا راه حل خاصی به نظرتان میرسد بسیار عالی خواهد بود که آن را با ما به اشتراک بگذارید.
خروجی پیش فرش وب سرویس WCF Data Services ساختار Xml دارد پس میبایست وب سرویس را متوجه سازیم که ما با خروجی Json نیاز داریم. از نسخه 5 به بعد اگر MaxProtocolVersion را بر روی V3 قرار دهیم دیگر با Accept Header برابر application/json کار نخواهد کرد و میبایست از application/json;odata=verbose استفاده نمود یا نسخه پروتکل را بر روی V2 یا پایینتر تنطیم کنید. علاوه بر آن کتابخانههای و قطعه کدهای تهیه شده است که با پارامتر format$ این کار را برای ما انجام میدهد در زیر آدرس دو نمونه آورده شده است.
[JSONPSupportBehavior] public class Northwind : DataService<NorthwindEntities>
http://localhost:8358/Northwind.svc/Products?$format=json
public class Northwind : ODataService<NorthwindEntities>
$.getJSON("Northwind.svc/Products?$format=json", function (data) { $.each(data.d, function (i, item) { $("<p/>").html(item.Product_Name + " " + item.Unit_Price).appendTo("#products"); }); });
$.ajax('Northwind.svc/Products', { dataType: "json", beforeSend: function (xhr) { xhr.setRequestHeader("Accept", "application/json;odata=verbose"); xhr.setRequestHeader("MaxDataServiceVersion", "3.0"); }, success: function (data) { $.each(data.d, function (i, item) { $("<p/>").html(item.Product_Name + " " + item.Unit_Price).appendTo("#products"); }); } });
برای یک مسئله میتوان کوئریهای متنوعی نوشت که همگی به یک جواب میرسند؛ ولی زمان اجرا و میزان حافظهی مصرفی متفاوتی دارند. یک سناریوی رایج در نوشتن کوئریهای LINQ، ترکیب اطلاعات جداول مختلف و محاسبهی یک عدد معنی دار از ترکیب آن هاست.
برای نمونه دو Entity زیر را در مدل EF خود داریم:
public class User { public int ID { get; set; } public string Name { get; set; } public int Age { get; set; } } public class Login { public int ID { get; set; } public DateTime Date { get; set; } public int UserID { get; set; } public User User { get; set; } }
برای تولید اطلاعات تصادفی میتوان از کد زیر در LINQPad استفاده کرد:
int usersCount = 1200; Random rnd = new Random(); for(int i=0; i<usersCount; i++) { Users.Add(new User() { Name = $"User {i + 1}", Age = rnd.Next(10, i + 10) / 10 }); } SaveChanges(); $"Users: {Users.Count()}".Dump(); var usersID = Users.Select(x => x.ID).ToArray(); int loginsCount = 20000; for(int i=0; i<loginsCount; i++) { Logins.Add(new Login() { UserID = usersID[rnd.Next(0, usersID.Length - 1)], Date = DateTime.Now.AddDays(rnd.Next(0, i)) }); if(i % 1000 == 0) { SaveChanges(); $"Save {i + 1}".Dump(); } } SaveChanges(); $"Logins: {Logins.Count()}".Dump();
$"Users: {Users.Count()}".Dump(); $"Logins: {Logins.Count()}".Dump(); Users: 1200 Logins: 21000
مسئله: نمایش اطلاعات پروفایل هر کاربر، به همراه تاریخ آخرین لوگین و تعداد کل لوگینهای فرد
در سناریوهای این سبکی، باید خیلی با دقت عمل کرد و از تمام اطلاعات موجود استفاده کرد. اطلاعاتی که در اینجا برای ما مفید است، تعداد نسبی رکوردهای جداول دیتابیس است. مثلا در حال حاضر تعداد رکوردهای Logins تقریبا 17 برابر Users است و در آینده هم رشد Logins چند برابر Users خواهد بود. از طرفی در صورت مسئله، اطلاعات هر کاربر را میخواهیم، که به سادگی یک SELECT است. ولی بخش سنگینتر کوئری، محاسبهی تعداد لوگینها و تاریخ آخرین لوگینهای هر فرد است که باز هم به جدول Logins بر میگردد.
روش اول:
راه حل اولی که به ذهن میرسد، JOIN کردن این دو جدول و محاسبه موارد لازم از ترکیب این دو جدول است:
var data = ( from u in Users join x in Logins on u.ID equals x.UserID into g from x in g.DefaultIfEmpty() select new { UserID = u.ID, Name = u.Name, Age = u.Age, Date = x.Date } ); var result = ( from d in data group d by d.UserID into g select new { UserID = g.Key, Name = g.FirstOrDefault().Name, LoginsCount = g.Count(x => x.Date != null), LastLogin = g.Max(x => (DateTime?) x.Date) ?? null } );
SELECT [Project7].[ID] AS [ID], [Project7].[C2] AS [C1], [Project7].[C3] AS [C2], [Project7].[C1] AS [C3] FROM ( SELECT [Project6].[ID] AS [ID], CASE WHEN ([Project6].[C3] IS NULL) THEN CAST(NULL AS datetime2) ELSE [Project6].[C4] END AS [C1], [Project6].[C1] AS [C2], [Project6].[C2] AS [C3] FROM ( SELECT [Project5].[ID] AS [ID], [Project5].[C1] AS [C1], [Project5].[C2] AS [C2], [Project5].[C3] AS [C3], (SELECT MAX( CAST( [Extent9].[Date] AS datetime2)) AS [A1] FROM [dbo].[Users] AS [Extent8] LEFT OUTER JOIN [dbo].[Logins] AS [Extent9] ON [Extent8].[ID] = [Extent9].[UserID] WHERE [Project5].[ID] = [Extent8].[ID]) AS [C4] FROM ( SELECT [Project4].[ID] AS [ID], [Project4].[C1] AS [C1], [Project4].[C2] AS [C2], (SELECT MAX( CAST( [Extent7].[Date] AS datetime2)) AS [A1] FROM [dbo].[Users] AS [Extent6] LEFT OUTER JOIN [dbo].[Logins] AS [Extent7] ON [Extent6].[ID] = [Extent7].[UserID] WHERE [Project4].[ID] = [Extent6].[ID]) AS [C3] FROM ( SELECT [Project3].[ID] AS [ID], [Project3].[C1] AS [C1], (SELECT COUNT(1) AS [A1] FROM [dbo].[Logins] AS [Extent5] WHERE [Project3].[ID] = [Extent5].[UserID]) AS [C2] FROM ( SELECT [Distinct1].[ID] AS [ID], (SELECT TOP (1) [Extent3].[Name] AS [Name] FROM [dbo].[Users] AS [Extent3] LEFT OUTER JOIN [dbo].[Logins] AS [Extent4] ON [Extent3].[ID] = [Extent4].[UserID] WHERE [Distinct1].[ID] = [Extent3].[ID]) AS [C1] FROM ( SELECT DISTINCT [Extent1].[ID] AS [ID] FROM [dbo].[Users] AS [Extent1] LEFT OUTER JOIN [dbo].[Logins] AS [Extent2] ON [Extent1].[ID] = [Extent2].[UserID] ) AS [Distinct1] ) AS [Project3] ) AS [Project4] ) AS [Project5] ) AS [Project6] ) AS [Project7] ORDER BY [Project7].[C3] ASC, [Project7].[ID] ASC
روش دوم:
روش دوم اینست که دادههای سنگینتر (اطلاعات Logins) را ابتدا محاسبه کرده و سپس JOIN را انجام دهیم:
var data = ( from x in Logins group x by x.UserID into g orderby g.Key descending select new { UserID = g.Key, LoginsCount = g.Count(), LastLogin = g.Max(d => d.Date) } ); var result = ( from u in Users join d in data on u.ID equals d.UserID into g from d in g.DefaultIfEmpty() select new { UserID = u.ID, LoginsCount = d != null ? d.LoginsCount : 0, LastLogin = d != null ? (DateTime?)d.LastLogin : null } );
اکنون اگر کد SQL روش دوم را بررسی کنیم خواهیم دید که تنها 2 دستور SELECT ، یک LEFT OUTER JOIN به همراه یک GROUP BY تولید شده است که با توجه به ماهیت مسئله و ساختار دیتای ما، این دستورات منطقیترین و بهینهترین دستورات ممکن به نظر میرسد.
SELECT [Project1].[ID] AS [ID], [Project1].[C1] AS [C1], [Project1].[C2] AS [C2] FROM ( SELECT [Extent1].[ID] AS [ID], CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN [GroupBy1].[A1] ELSE 0 END AS [C1], CASE WHEN ([GroupBy1].[K1] IS NOT NULL) THEN CAST( [GroupBy1].[A2] AS datetime2) END AS [C2] FROM [dbo].[Users] AS [Extent1] LEFT OUTER JOIN (SELECT [Extent2].[UserID] AS [K1], COUNT(1) AS [A1], MAX([Extent2].[Date]) AS [A2] FROM [dbo].[Logins] AS [Extent2] GROUP BY [Extent2].[UserID] ) AS [GroupBy1] ON [Extent1].[ID] = [GroupBy1].[K1] ) AS [Project1] ORDER BY [Project1].[C1] ASC, [Project1].[ID] ASC
import 'dart:html'; import 'dart:math' show Random; import 'dart:convert' show JSON;
class PirateName { ... PirateName.fromJSON(String jsonString) { Map storedName = JSON.decode(jsonString); _firstName = storedName['f']; _appellation = storedName['a']; } }
class PirateName { ... String get jsonString => JSON.encode({"f": _firstName, "a": _appellation}); }
final String TREASURE_KEY = 'pirateName'; void main() { ... }
void setBadgeName(PirateName newName) { if (newName == null) { return; } querySelector('#badgeName').text = newName.pirateName; window.localStorage[TREASURE_KEY] = newName.jsonString; }
void setBadgeName(PirateName newName) { ... } PirateName getBadgeNameFromStorage() { String storedName = window.localStorage[TREASURE_KEY]; if (storedName != null) { return new PirateName.fromJSON(storedName); } else { return null; } }
void main() { ... setBadgeName(getBadgeNameFromStorage()); }
{ "names": [ "Anne", "Bette", "Cate", "Dawn", "Elise", "Faye", "Ginger", "Harriot", "Izzy", "Jane", "Kaye", "Liz", "Maria", "Nell", "Olive", "Pat", "Queenie", "Rae", "Sal", "Tam", "Uma", "Violet", "Wilma", "Xana", "Yvonne", "Zelda", "Abe", "Billy", "Caleb", "Davie", "Eb", "Frank", "Gabe", "House", "Icarus", "Jack", "Kurt", "Larry", "Mike", "Nolan", "Oliver", "Pat", "Quib", "Roy", "Sal", "Tom", "Ube", "Val", "Walt", "Xavier", "Yvan", "Zeb"], "appellations": [ "Awesome", "Captain", "Even", "Fighter", "Great", "Hearty", "Jackal", "King", "Lord", "Mighty", "Noble", "Old", "Powerful", "Quick", "Red", "Stalwart", "Tank", "Ultimate", "Vicious", "Wily", "aXe", "Young", "Brave", "Eager", "Kind", "Sandy", "Xeric", "Yellow", "Zesty"]}
... <div> <input type="text" id="inputName" maxlength="15" disabled> </div> <div> <button id="generateButton" disabled>Aye! Gimme a name!</button> </div> ...
import 'dart:html'; import 'dart:math' show Random; import 'dart:convert' show JSON; import 'dart:async' show Future;
class PirateName { ... static List<String> names = []; static List<String> appellations = []; ... }
class PirateName { ... static Future readyThePirates() { var path = 'piratenames.json'; return HttpRequest.getString(path) .then(_parsePirateNamesFromJSON); } static _parsePirateNamesFromJSON(String jsonString) { Map pirateNames = JSON.decode(jsonString); names = pirateNames['names']; appellations = pirateNames['appellations']; } }
SpanElement badgeNameElement; void main() { ... }
void main() { InputElement inputField = querySelector('#inputName'); inputField.onInput.listen(updateBadge); genButton = querySelector('#generateButton'); genButton.onClick.listen(generateBadge); badgeNameElement = querySelector('#badgeName'); ... }
void main() { ... PirateName.readyThePirates() .then((_) { //on success inputField.disabled = false; //enable genButton.disabled = false; //enable setBadgeName(getBadgeNameFromStorage()); }) .catchError((arrr) { print('Error initializing pirate names: $arrr'); badgeNameElement.text = 'Arrr! No names.'; }); }
- https://github.com/loresoft/EntityFramework.Extended دقیقا شبیه به پیاده سازی مثال جاری هست. فقط cache provider اختصاصی ایجاد کرده (بجای استفاده از HttpRuntime.Cache).
- https://efcache.codeplex.com داخل سیستم Data Reader میشود برای کش کردن. (جهت ایده دادن به تیم EF خوب است)
- https://github.com/osjoberg/LinqCache نمونهی توسعه یافته مثال جاری است.
ASP.NET MVC #7
@if (User.Identity.IsAuthenticated && User.IsInRole("Administrator")) { <div id="sidebar"> data </div> }