توضیح LINQ با تصاویر
استفاده GitHub از Web Componentها
We’re using Web Components in a big way at GitHub. We have over a dozen open-source Web Components and with dozens more that are closed source.
تازههای ASP.NET Core 3 Preview 6
We would love to hear about your experience with building client applications in .NET. Your feedback will greatly help us to improve the .NET tooling and ensure our roadmap focuses on your needs. Participate in shaping the future of the .NET client development by taking this short survey (5 minutes to complete).
مهارتهای تزریق وابستگیها در برنامههای NET Core. - قسمت سوم - رهاسازی منابع سرویسهای IDisposable
رفتار IoC Container توکار ASP.NET Core با سرویسهای IDisposable
ASP.NET Core به همراه یک IoC Container توکار ارائه میشود و اگر سرویسی با طول عمرTransient و یا Scoped به آن معرفی شود و همچنین این سرویس اینترفیس IDisposable را نیز پیاده سازی کند، کار dispose خودکار آن در پایان درخواست جاری صورت میگیرد و نیازی به تنظیمات اضافهتری ندارد. در اینجا سرویسهایی با طول عمر Singleton نیز در پایان کار برنامه، زمانیکه خود ServiceProvider به پایان کارش میرسد، dispose خواهند شد.
البته این مورد یک شرط را نیز به همراه دارد: کار وهله سازی سرویسهای درخواستی باید توسط خود این IoC Container مدیریت شود تا در پایان کار بداند چگونه آنها را Dispose کند.
یک مثال: بررسی Dispose شدن خودکار یک سرویس IDisposable
namespace CoreIocServices { public interface IMyDisposableService { void Run(); } public class MyDisposableService : IMyDisposableService, IDisposable { private readonly ILogger<MyDisposableService> _logger; public MyDisposableService(ILogger<MyDisposableService> logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger.LogInformation("+ {0} was created", this.GetType().Name); } public void Run() { _logger.LogInformation("Running MyDisposableService!"); } public void Dispose() { _logger.LogInformation("- {0} was disposed!", this.GetType().Name); } } }
اگر این سرویس را به یک برنامهی ASP.NET Core معرفی کنیم:
namespace CoreIocSample02 { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient<IMyDisposableService, MyDisposableService>();
namespace CoreIocSample02.Controllers { public class HomeController : Controller { private readonly IMyDisposableService _myDisposableService; public HomeController(IMyDisposableService myDisposableService) { _myDisposableService = myDisposableService; } public IActionResult Index() { _myDisposableService.Run(); return View(); }
در ادامه با اجرای برنامه، اگر به لاگهای آن دقت کنیم، این خروجی قابل مشاهده خواهد بود:
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1] Route matched with {action = "Index", controller = "Home"}. Executing action CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02) info: CoreIocServices.MyDisposableService[0] + MyDisposableService was created . . . info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1] Executed endpoint 'CoreIocSample02.Controllers.HomeController.Index (CoreIocSample02)' info: CoreIocServices.MyDisposableService[0] - MyDisposableService was disposed! info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2] Request finished in 1316.4719ms 200 text/html; charset=utf-8
بنابراین IoC Container، به صورت خودکار، کار Dispose این سرویس IDisposable را نیز انجام دادهاست.
Dispose خودکار وهلههایی که توسط IoC Container ایجاد نشدهاند
اگر ایجاد اشیاء از نوع IDisposable را خودتان و خارج از دید IoC Container توکار ASP.NET Core انجام میدهید، از مزیت پاکسازی خودکار منابع توسط آنها در پایان درخواست محروم خواهید شد، اما ... برای رفع این مشکل نیز متد context.Response.RegisterForDispose پیش بینی شدهاست. اگر شیءای از نوع IDisposable را توسط این متد به ASP.NET Core معرفی کنید، در پایان درخواست به صورت خودکار Dispose خواهد شد.
یک مثال: فرض کنید یک StreamWriter را داخل یک میانافزار ایجاد کردهاید، اما آنرا Dispose نکردهاید:
namespace CoreIocSample02 { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.Use(async (context, next) => { var writer = File.CreateText(Path.GetTempFileName()); context.Response.RegisterForDispose(writer); context.Items["filewriter"] = writer; await writer.WriteLineAsync("some important information"); await writer.FlushAsync(); await next(); });
اکنون در ادامه، در اکشن متد WriteLog یک کنترلر دلخواه، کار ثبت وقایع با دریافت این writer از HttpContext.Items قابل انجام است؛ چون هنوز طول عمر درخواست جاری پایان نیافته و شیء writer به صورت خودکار Dispose نشدهاست:
namespace CoreIocSample02.Controllers { public class HomeController : Controller { public async Task<IActionResult> WriteLog() { var writer = HttpContext.Items["filewriter"] as StreamWriter; if (writer != null) { await writer.WriteLineAsync("more important information"); await writer.FlushAsync(); } return View(); }
زمانیکه به صورت متداولی از سیستم تزریق وابستگیهای ASP.NET Core استفاده میکنیم، به ازای هر درخواست HTTP رسیده، یک Scope از نوع IServiceScopeFactory ایجاد میشود و با پایان درخواست، این Scope نیز Dispose خواهد شد. به این ترتیب هر سرویس ایجاد شدهی درون این Scope نیز Dispose میشود؛ کاری شبیه به عملیات زیر:
using(var scope = serviceProvider.CreateScope()) { var provider = scope.ServiceProvider; var resolvedService = provider.GetRequiredService(someType); // Use resolvedService... }
مشکل! اگر از سرویس فرضی IOperationScoped با طول عمر Scoped در متدهای مختلف کلاس آغازین برنامه استفاده کنیم (مانند DbContext برنامه)، طول عمری را که دریافت خواهیم کرد singleton خواهد بود و نه Scoped؛ چون درون یک scopeFactory.CreateScope ایجاد شدهی به صورت خودکار توسط یک درخواست قرار نداریم. بنابراین هر درخواست وهلهای از سرویس IOperationScoped با طول عمر Scoped، تنها همان وهلهی ابتدایی آنرا باز میگرداند و singleton رفتار میکند؛ چون scope ایی ایجاد و تخریب نشدهاست.
در یک چنین مواردی، برای اطمینان حاصل کردن از dispose شدن سرویس در پایان کار، نیاز است مراحل ایجاد scope و dispose آنرا به صورت دستی به نحو ذیل مدیریت کنیم:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, IServiceScopeFactory scopeFactory) { using (var scope = scopeFactory.CreateScope()) { var initializer = scope.ServiceProvider.GetService<IOperationScoped>(); initializer.SeedAsync().Wait(); } }
Dispose کردن سرویسهای IDisposable در برنامههای Console
اگر همین سرویس IMyDisposableService را در مثال برنامهی کنسول قسمت اول استفاده کنیم:
var myDisposableService = serviceProvider.GetService<IMyDisposableService>(); myDisposableService.Run();
در برنامهی کنسول، کار ایجاد serviceProvider را خودمان انجام دادیم:
var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); var serviceProvider = serviceCollection.BuildServiceProvider();
namespace Microsoft.Extensions.DependencyInjection { public sealed class ServiceProvider : IServiceProvider, IDisposable, IServiceProviderEngineCallback { public void Dispose(); public object GetService(Type serviceType); } }
namespace CoreIocSample01 { class Program { static void Main(string[] args) { var serviceCollection = new ServiceCollection(); ConfigureServices(serviceCollection); using (var serviceProvider = serviceCollection.BuildServiceProvider()) { var myDisposableService = serviceProvider.GetService<IMyDisposableService>(); myDisposableService.Run(); var testService = serviceProvider.GetService<ITestService>(); testService.Run(); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: CoreDependencyInjectionSamples-02.zip
بروز رسانی موجودیتهای منفصل توسط WCF
سناریویی را در نظر بگیرید که در آن عملیات CRUD توسط WCF پیاده سازی شده اند و دسترسی دادهها با مدل Code-First انجام میشود. فرض کنید مدل اپلیکیشن مانند تصویر زیر است.
همانطور که میبینید مدل ما متشکل از پستها و نظرات کاربران است. برای ساده نگاه داشتن مثال جاری، اکثر فیلدها حذف شده اند. مثلا متن پست ها، نویسنده، تاریخ و زمان انتشار و غیره. میخواهیم تمام کد دسترسی دادهها را در یک سرویس WCF پیاده سازی کنیم تا کلاینتها بتوانند عملیات CRUD را توسط آن انجام دهند. برای ساختن این سرویس مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع Class Library بسازید و نام آن را به Recipe2 تغییر دهید.
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- سه کلاس با نامهای Post, Comment و Recipe2Context به پروژه اضافه کنید. کلاسهای Post و Comment موجودیتهای مدل ما هستند که به جداول متناظرشان نگاشت میشوند. کلاس Recipe2Context آبجکت DbContext ما خواهد بود که بعنوان درگاه عملیاتی EF عمل میکند. دقت کنید که خاصیتهای لازم WCF یعنی DataContract و DataMember در کلاسهای موجودیتها بدرستی استفاده میشوند. لیست زیر کد این کلاسها را نشان میدهد.
[DataContract(IsReference = true)] public class Post { public Post() { comments = new HashSet<Comments>(); } [DataMember] public int PostId { get; set; } [DataMember] public string Title { get; set; } [DataMember] public virtual ICollection<Comment> Comments { get; set; } } [DataContract(IsReference=true)] public class Comment { [DataMember] public int CommentId { get; set; } [DataMember] public int PostId { get; set; } [DataMember] public string CommentText { get; set; } [DataMember] public virtual Post Post { get; set; } } public class EFRecipesEntities : DbContext { public EFRecipesEntities() : base("name=EFRecipesEntities") {} public DbSet<Post> posts; public DbSet<Comment> comments; }
- یک فایل App.config به پروژه اضافه کنید و رشته اتصال زیر را به آن اضافه نمایید.
<connectionStrings> <add name="Recipe2ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- حال یک پروژه WCF به Solution جاری اضافه کنید. برای ساده نگاه داشتن مثال جاری، نام پیش فرض Service1 را بپذیرید. فایل IService1.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید.
[ServiceContract] public interface IService1 { [OperationContract] void Cleanup(); [OperationContract] Post GetPostByTitle(string title); [OperationContract] Post SubmitPost(Post post); [OperationContract] Comment SubmitComment(Comment comment); [OperationContract] void DeleteComment(Comment comment); }
- فایل Service1.svc.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. بیاد داشته باشید که پروژه Recipe2 را ارجاع کنید و فضای نام آن را وارد نمایید. همچنین کتابخانه EF 6 را باید به پروژه اضافه کنید.
public class Service1 : IService { public void Cleanup() { using (var context = new EFRecipesEntities()) { context.Database.ExecuteSqlCommand("delete from [comments]"); context. Database.ExecuteSqlCommand ("delete from [posts]"); } } public Post GetPostByTitle(string title) { using (var context = new EFRecipesEntities()) { context.Configuration.ProxyCreationEnabled = false; var post = context.Posts.Include(p => p.Comments).Single(p => p.Title == title); return post; } } public Post SubmitPost(Post post) { context.Entry(post).State = // if Id equal to 0, must be insert; otherwise, it's an update post.PostId == 0 ? EntityState.Added : EntityState.Modified; context.SaveChanges(); return post; } public Comment SubmitComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Comments.Attach(comment); if (comment.CommentId == 0) { // this is an insert context.Entry(comment).State = EntityState.Added); } else { // set single property to modified, which sets state of entity to modified, but // only updates the single property – not the entire entity context.entry(comment).Property(x => x.CommentText).IsModified = true; } context.SaveChanges(); return comment; } } public void DeleteComment(Comment comment) { using (var context = new EFRecipesEntities()) { context.Entry(comment).State = EntityState.Deleted; context.SaveChanges(); } } }
- در آخر پروژه جدیدی از نوع Windows Console Application به Solution جاری اضافه کنید. از این اپلیکیشن بعنوان کلاینتی برای تست سرویس WCF استفاده خواهیم کرد. فایل program.cs را باز کنید و کد زیر را با محتوای آن جایگزین نمایید. روی نام پروژه کلیک راست کرده و گزینه Add Service Reference را انتخاب کنید، سپس ارجاعی به سرویس Service1 اضافه کنید. رفرنسی هم به کتابخانه کلاسها که در ابتدای مراحل ساختید باید اضافه کنید.
class Program { static void Main(string[] args) { using (var client = new ServiceReference2.Service1Client()) { // cleanup previous data client.Cleanup(); // insert a post var post = new Post { Title = "POCO Proxies" }; post = client.SubmitPost(post); // update the post post.Title = "Change Tracking Proxies"; client.SubmitPost(post); // add a comment var comment1 = new Comment { CommentText = "Virtual Properties are cool!", PostId = post.PostId }; var comment2 = new Comment { CommentText = "I use ICollection<T> all the time", PostId = post.PostId }; comment1 = client.SubmitComment(comment1); comment2 = client.SubmitComment(comment2); // update a comment comment1.CommentText = "How do I use ICollection<T>?"; client.SubmitComment(comment1); // delete comment 1 client.DeleteComment(comment1); // get posts with comments var p = client.GetPostByTitle("Change Tracking Proxies"); Console.WriteLine("Comments for post: {0}", p.Title); foreach (var comment in p.Comments) { Console.WriteLine("\tComment: {0}", comment.CommentText); } } } }
Comment: I use ICollection<T> all the time
شرح مثال جاری
ابتدا با اپلیکیشن کنسول شروع میکنیم، که کلاینت سرویس ما است. نخست در یک بلاک {} using وهله ای از کلاینت سرویس مان ایجاد میکنیم. درست همانطور که وهله ای از یک EF Context میسازیم. استفاده از بلوکهای using توصیه میشود چرا که متد Dispose بصورت خودکار فراخوانی خواهد شد، چه بصورت عادی چه هنگام بروز خطا. پس از آنکه وهله ای از کلاینت سرویس را در اختیار داشتیم، متد Cleanup را صدا میزنیم. با فراخوانی این متد تمام دادههای تست پیشین را حذف میکنیم. در چند خط بعدی، متد SubmitPost را روی سرویس فراخوانی میکنیم. در پیاده سازی فعلی شناسه پست را بررسی میکنیم. اگر مقدار شناسه صفر باشد، خاصیت State موجودیت را به Added تغییر میدهید تا رکورد جدیدی ثبت کنیم. در غیر اینصورت فرض بر این است که چنین موجودیتی وجود دارد و قصد ویرایش آن را داریم، بنابراین خاصیت State را به Modified تغییر میدهیم. از آنجا که مقدار متغیرهای int بصورت پیش فرض صفر است، با این روش میتوانیم وضعیت پستها را مشخص کنیم. یعنی تعیین کنیم رکورد جدیدی باید ثبت شود یا رکوردی موجود بروز رسانی گردد. رویکردی بهتر آن است که پارامتری اضافی به متد پاس دهیم، یا متدی مجزا برای ثبت رکوردهای جدید تعریف کنیم. مثلا رکوردی با نام InsertPost. در هر حال، بهترین روش بستگی به ساختار اپلیکیشن شما دارد.
اگر پست جدیدی ثبت شود، خاصیت PostId با مقدار مناسب جدید بروز رسانی میشود و وهله پست را باز میگردانیم. ایجاد و بروز رسانی نظرات کاربران مشابه ایجاد و بروز رسانی پستها است، اما با یک تفاوت اساسی: بعنوان یک قانون، هنگام بروز رسانی نظرات کاربران تنها فیلد متن نظر باید بروز رسانی شود. بنابراین با فیلدهای دیگری مانند تاریخ انتشار و غیره اصلا کاری نخواهیم داشت. بدین منظور تنها خاصیت CommentText را بعنوان Modified علامت گذاری میکنیم. این امر منجر میشود که Entity Framework عبارتی برای بروز رسانی تولید کند که تنها این فیلد را در بر میگیرد. توجه داشته باشید که این روش تنها در صورتی کار میکند که بخواهید یک فیلد واحد را بروز رسانی کنید. اگر میخواستیم فیلدهای بیشتری را در موجودیت Comment بروز رسانی کنیم، باید مکانیزمی برای ردیابی تغییرات در سمت کلاینت در نظر میگرفتیم. در مواقعی که خاصیتهای متعددی میتوانند تغییر کنند، معمولا بهتر است کل موجودیت بروز رسانی شود تا اینکه مکانیزمی پیچیده برای ردیابی تغییرات در سمت کلاینت پیاده گردد. بروز رسانی کل موجودیت بهینهتر خواهد بود.
برای حذف یک دیدگاه، متد Entry را روی آبجکت DbContext فراخوانی میکنیم و موجودیت مورد نظر را بعنوان آرگومان پاس میدهیم. این امر سبب میشود که موجودیت مورد نظر بعنوان Deleted علامت گذاری شود، که هنگام فراخوانی متد SaveChanges اسکریپت لازم برای حذف رکورد را تولید خواهد کرد.
در آخر متد GetPostByTitle یک پست را بر اساس عنوان پیدا کرده و تمام نظرات کاربران مربوط به آن را هم بارگذاری میکند. از آنجا که ما کلاسهای POCO را پیاده سازی کرده ایم، Entity Framework آبجکتی را بر میگرداند که Dynamic Proxy نامیده میشود. این آبجکت پست و نظرات مربوط به آن را در بر خواهد گرفت. متاسفانه WCF نمیتواند آبجکتهای پروکسی را مرتب سازی (serialize) کند. اما با غیرفعال کردن قابلیت ایجاد پروکسیها (ProxyCreationEnabled=false) ما به Entity Framework میگوییم که خود آبجکتهای اصلی را بازگرداند. اگر سعی کنید آبجکت پروکسی را سریال کنید با پیغام خطای زیر مواجه خواهید شد:
The underlying connection was closed: The connection was closed unexpectedly
می توانیم غیرفعال کردن تولید پروکسی را به متد سازنده کلاس سرویس منتقل کنیم تا روی تمام متدهای سرویس اعمال شود.
در این قسمت دیدیم چگونه میتوانیم از آبجکتهای POCO برای مدیریت عملیات CRUD توسط WCF استفاده کنیم. از آنجا که هیچ اطلاعاتی درباره وضعیت موجودیتها روی کلاینت ذخیره نمیشود، متدهایی مجزا برای عملیات CRUD ساختیم. در قسمتهای بعدی خواهیم دید چگونه میتوان تعداد متدهایی که سرویس مان باید پیاده سازی کند را کاهش داد و چگونه ارتباطات بین کلاینت و سرور را سادهتر کنیم.
اجرای کوئریهای خام SQL بر روی بانک اطلاعاتی، توسط EF Core
گاهی از اوقات نیاز به استفادهی قابلیت خاصی از بانک اطلاعاتی مدنظر وجود دارد که توسط LINQ پشتیبانی نمیشود و یا کوئری SQL حاصل از LINQ to Entities آنچنان بهینه نیست. در یک چنین حالاتی راهی بجز نوشتن کوئریهای خام SQL وجود ندارد. امکان اجرای یک چنین کوئریهایی توسط EF Core پیش بینی شدهاست؛ اما با این محدودیتها:
- خروجی کوئری SQL، تنها باید معادل یکی از کلاسهای موجودیتهای شما باشد. قرار است این محدودیت در نگارش 1.1 برطرف شود.
- کوئری SQL نوشته شده باید تمام خواص موجودیتی را که قرار است به آن نگاشت شود، بازگشت دهد.
- نام ستونهای بازگشت داده شدهی توسط کوئری SQL باید با نام خواص موجودیت در حال کار، یکی باشند و برخلاف EF 6.x، از یک چنین عدم تطابقهایی صرفنظر نخواهد شد.
- کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد.
در اینجا برای نوشتن کوئریهای خام SQL میتوان از متد FromSql مرتبط با یکی از DbSetهای برنامه استفاده کرد:
var blogs = context.Blogs .FromSql("SELECT * FROM dbo.Blogs") .ToList();
var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogs") .ToList();
بنابراین رفتار EF Core اندکی متفاوت است با EF 6.x. در اینجا اگر میخواهید از عبارت SQL خود خروجی بگیرید، باید از یکی از DbSetهای خود شروع کنید و متد FromSql را بر روی آن فراخوانی نمائید. همچنین کوئری نوشته شده باید اولا تمام ستونهای آن DbSet رابازگشت دهد و به علاوه این ستونها دقیقا با نامهای خواص آن کلاس، تطابق داشته باشند.
علت این مسایل نیز به این دلیل است که بتوان نتیجهی کوئری را به صورت خودکار وارد سیستم change tracking کرد و همچنین کوئریهای ترکیبی LINQ را نیز در اینجا فعال کرد.
ارسال پارامترها به کوئریهای خام SQL
تنها حالتی در EF Core که مستعد به حملات تزریق SQL است، دقیقا همین مورد دور شدن از LINQ و نوشتن عبارات مستقیم SQL است. در اینجا برای نوشتن کوئریهای پارامتری دو حالت پیش بینی شدهاست:
الف) روش parameter place holders
در اینجا متد FromSql، بسیار شبیه به متد String.Format است، اما در عمل اینطور نیست و تمام place holders آن به صورت خودکار تبدیل به پارامتر میشوند:
var user = "johndoe"; var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser {0}", user) .ToList();
اگر میخواهید از پارامترهای نام دار استفاده کنید، با وهلهای از SqlParameter شروع کرده و سپس آنرا به متد FromSql ارسال کنید:
var user = new SqlParameter("user", "johndoe"); var blogs = context.Blogs .FromSql("EXECUTE dbo.GetMostPopularBlogsForUser @user", user) .ToList();
var results = _context.Contacts.FromSql( @"SELECT Id, Name Address, City, State, Zip FROM Contacts WHERE Name IN (@p0, @p1)", name1, name2);
مزیت کار کردن با SqlParameter این است که میتوان برای مثال Direction و SqlDbType را نیز صریحا ذکر کرد (بسته به نوع پارامترهای رویهی ذخیره شده):
var nameParameter = new SqlParameter { ParameterName = "@name", Value = "doc", Direction = ParameterDirection.Input, SqlDbType = SqlDbType.NVarChar };
امکان ترکیب کوئریهای SQL و LINQ نیز پیش بینی شدهاست
در کوئری ذیل، قسمت select از جدولی به صورت SQL و قسمت where و order by آن توسط LINQ تهیه شدهاند که در نهایت به یک کوئری ترجمه شده و بر روی بانک اطلاعاتی اجرا میشوند.
یک مثال جالب آن، امکان کوئری گرفتن از Table Value Functionها و سپس ترکیب آنها با LINQ است (این ترکیب، تنها یک کوئری SQL نهایی را تولید میکند):
var posts = context.Posts .FromSql("SELECT * FROM dbo.GetMatchingPostByTitle({0})", searchTerm) .Where(p => p.BlogId == 1) .OrderByDescending(p => p.CreateDate) .ToList();
واکشی ارتباطات یک موجودیت توسط SQL و LINQ
در ابتدای بحث در قسمت محدودیتهای کوئریهای SQL نوشته شده، ذکر شد «کوئری SQL نوشته شده نباید به همراه اطلاعات ارتباطات موجودیتها باشد». برای رفع این محدودیت میتوان از ترکیب SQL و LINQ به صورت ذیل استفاده کرد:
var searchTerm = ".NET"; var blogs = context.Blogs .FromSql("SELECT * FROM dbo.SearchBlogs {0}", searchTerm) .Include(b => b.Posts) .ToList();
اجرای عبارات SQL، بدون بازگشت مقداری
تا اینجا در مورد عبارات SQL از نوع Select و یا اجرای رویههای ذخیره شده، بحث شد. برای اجرای عبارات SQL ایی مانند update و delete میتوان از متد ExecuteSqlCommand مربوط به context.Database استفاده کرد:
context.Database.ExecuteSqlCommand("UPDATE dbo.People SET FirstName = 'Jane' WHERE PersonId = 30");
context.Database.ExecuteSqlCommand("usp_CreateShipper @p0, @p1", parameters: new[] { "hello", "world" });
اجرای عبارات SQL و دریافت خروجیهایی به غیر از موجودیتهای برنامه
در ابتدا بحث عنوان شد که محدودیت فعلی کوئریهای FromSQL که میتوانند خروجی را نیز ارائه دهند، مقید بودن آنها به DbSet در حال استفاده است و محدود بودن آنها به خواص کلاس متناظر تعریف شده. در این حالت اگر بخواهیم یک محاسبهی عددی را بازگشت دهیم چه باید کرد؟
متد ExecuteSqlCommand تنها وضعیت نهایی اجرای عملیات را بازگشت میدهد و FromSQL مقید است به DbSet متناظر. برای رفع این محدودیتها میتوان مستقیما به DbConnection دسترسی یافت و سپس کوئری گرفت؛ به نحو ذیل:
using (var connection = context.Database.GetDbConnection()) { connection.Open(); using (var command = connection.CreateCommand()) { command.CommandText = "SELECT COUNT(*) FROM Contacts"; var result = command.ExecuteScalar().ToString(); } }