using System.Linq.Expressions; public class ContextWithExtensionExample { public void DoSomeContextWork(DbContext context) { var uni = new Unicorn(); context.Set<Unicorn>().AddIfNotExists(uni , x => x.Name == "James"); } } public static class DbSetExtensions { public static T AddIfNotExists<T>(this DbSet<T> dbSet, T entity, Expression<Func<T, bool>> predicate = null) where T : class, new() { var exists = predicate != null ? dbSet.Any(predicate) : dbSet.Any(); return !exists ? dbSet.Add(entity) : null; } }
enable-migrations add-migration Initial -IgnoreChanges update-database
سؤال: چگونه میتوان همین عملیات را با کدنویسی انجام داد؟
متد UpdateDatabase کلاس ذیل، دقیقا معادل است با اجرای سه دستور فوق :
using System.Data.Entity.Migrations; using System.Data.Entity.Migrations.Design; namespace EFTests { /// <summary> /// Using Entity Framework Code First with an existing database. /// </summary> public static class CreateMigrationHistory { /// <summary> /// Creates a new '__MigrationHistory' table. /// Enables migrations using Entity Framework Code First on an existing database. /// </summary> public static void UpdateDatabase(DbMigrationsConfiguration configuration) { var scaffolder = new MigrationScaffolder(configuration); // Creates an empty migration, so that the future migrations will start from the current state of your database. var scaffoldedMigration = scaffolder.Scaffold("IgnoreChanges", ignoreChanges: true); // enable-migrations // add-migration Initial -IgnoreChanges configuration.MigrationsAssembly = new MigrationCompiler(ProgrammingLanguage.CSharp.ToString()) .Compile(configuration.MigrationsNamespace, scaffoldedMigration); // update-database var dbMigrator = new DbMigrator(configuration); dbMigrator.Update(); } } }
MigrationScaffolder کار تولید خودکار کلاسهای cs مهاجرتهای EF را انجام میدهد. زمانیکه به متد Scaffold آن پارامتر ignoreChanges: true ارسال شود، کلاس مهاجرتی را ایجاد میکند که خالی است (متدهای up و down آن خالی تشکیل میشوند). سپس این کلاسها کامپایل شده و در حین اجرای برنامه مورد استفاده قرار میگیرند.
برای استفاده از آن، نیاز به کلاس MigrationCompiler خواهید داشت. این کلاس در مجموعه آزمونهای واحد EF به عنوان یک کلاس کمکی وجود دارد: MigrationCompiler.cs
صرفا جهت تکمیل بحث و همچنین سهولت ارجاعات آتی، کدهای آن در ذیل نیز ذکر خواهد شد:
using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.ComponentModel; using System.Data.Common; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Data.Entity.Migrations.Design; using System.Data.Entity.Spatial; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Resources; using System.Text; namespace EF_General.Models.Ex22 { public enum ProgrammingLanguage { CSharp, VB } public class MigrationCompiler { private readonly CodeDomProvider _codeProvider; public MigrationCompiler(string language) { _codeProvider = CodeDomProvider.CreateProvider(language); } public Assembly Compile(string @namespace, params ScaffoldedMigration[] scaffoldedMigrations) { var options = new CompilerParameters { GenerateExecutable = false, GenerateInMemory = true }; options.ReferencedAssemblies.Add(typeof(string).Assembly.Location); options.ReferencedAssemblies.Add(typeof(Expression).Assembly.Location); options.ReferencedAssemblies.Add(typeof(DbMigrator).Assembly.Location); options.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location); options.ReferencedAssemblies.Add(typeof(DbConnection).Assembly.Location); options.ReferencedAssemblies.Add(typeof(Component).Assembly.Location); options.ReferencedAssemblies.Add(typeof(MigrationCompiler).Assembly.Location); options.ReferencedAssemblies.Add(typeof(DbGeography).Assembly.Location); var embededResources = GenerateEmbeddedResources(scaffoldedMigrations, @namespace); foreach (var resource in embededResources) options.EmbeddedResources.Add(resource); var sources = scaffoldedMigrations.SelectMany(g => new[] { g.UserCode, g.DesignerCode }); var compilerResults = _codeProvider.CompileAssemblyFromSource(options, sources.ToArray()); foreach (var resource in embededResources) File.Delete(resource); if (compilerResults.Errors.Count > 0) { throw new InvalidOperationException(BuildCompileErrorMessage(compilerResults.Errors)); } return compilerResults.CompiledAssembly; } private static string BuildCompileErrorMessage(CompilerErrorCollection errors) { var stringBuilder = new StringBuilder(); foreach (CompilerError error in errors) { stringBuilder.AppendLine(error.ToString()); } return stringBuilder.ToString(); } private static IEnumerable<string> GenerateEmbeddedResources(IEnumerable<ScaffoldedMigration> scaffoldedMigrations, string @namespace) { foreach (var scaffoldedMigration in scaffoldedMigrations) { var className = GetClassName(scaffoldedMigration.MigrationId); var embededResource = Path.Combine( Path.GetTempPath(), @namespace + "." + className + ".resources"); using (var writer = new ResourceWriter(embededResource)) { foreach (var resource in scaffoldedMigration.Resources) writer.AddResource(resource.Key, resource.Value); } yield return embededResource; } } private static string GetClassName(string migrationId) { return migrationId .Split(new[] { '_' }, 2) .Last() .Replace(" ", string.Empty); } } }
استفاده از این کلاسها نیز بسیار ساده است. یکبار دستور ذیل را در ابتدای کار برنامه فراخوانی کنید تا جدول MigrationHistory دوباره ساخته شود:
CreateMigrationHistory.UpdateDatabase(new Configuration());
public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } }
معماری N-Tier چالشهای بخصوصی را برای قابلیتهای change-tracking در EF اضافه میکند. در ابتدا دادهها توسط یک آبجکت EF Context بارگذاری میشوند اما این آبجکت پس از ارسال دادهها به کلاینت از بین میرود. تغییراتی که در سمت کلاینت روی دادهها اعمال میشوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیتها نمیداند.
در نسخههای قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities میتوانستند تغییرات موجودیتها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی میشود، آبجکت DbContext از آن پشتیبانی نمیکند.
در این سری از مقالات روی عملیات پایه CRUD تمرکز میکنیم که در اکثر اپلیکیشنهای n-Tier استفاده میشوند. همچنین خواهیم دید چگونه میتوان تغییرات موجودیتها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیتهای منفصل (disconnected) توسط سرویسهای Web API نگاهی خواهیم داشت.
بروز رسانی موجودیتهای منفصل با Web API
سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده میشود. همچنین مدیریت دادهها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی میکند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژهها برای شبیه سازی یک محیط n-Tier انجام شده است.
فرض کنید مدلی مانند تصویر زیر داریم.
همانطور که میبینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی میکند. میخواهیم مدل و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده میکند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
- کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
- کلاس جدیدی با نام Order در پوشه مدلها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order { public int OrderId { get; set; } public string Product { get; set; } public int Quantity { get; set; } public string Status { get; set; } public byte[] TimeStamp { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext { public Recipe1Context() : base("Recipe1ConnectionString") { } public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>().ToTable("Orders"); // Following configuration enables timestamp to be concurrency token modelBuilder.Entity<Order>().Property(x => x.TimeStamp) .IsConcurrencyToken() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe1ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال میکند.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); ... }
- در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController { // GET api/order public IEnumerable<Order> Get() { using (var context = new Recipe1Context()) { return context.Orders.ToList(); } } // GET api/order/5 public Order Get(int id) { using (var context = new Recipe1Context()) { return context.Orders.FirstOrDefault(x => x.OrderId == id); } } // POST api/order public HttpResponseMessage Post(Order order) { // Cleanup data from previous requests Cleanup(); using (var context = new Recipe1Context()) { context.Orders.Add(order); context.SaveChanges(); // create HttpResponseMessage to wrap result, assigning Http Status code of 201, // which informs client that resource created successfully var response = Request.CreateResponse(HttpStatusCode.Created, order); // add location of newly-created resource to response header response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = order.OrderId })); return response; } } // PUT api/order/5 public HttpResponseMessage Put(Order order) { using (var context = new Recipe1Context()) { context.Entry(order).State = EntityState.Modified; context.SaveChanges(); // return Http Status code of 200, informing client that resouce updated successfully return Request.CreateResponse(HttpStatusCode.OK, order); } } // DELETE api/order/5 public HttpResponseMessage Delete(int id) { using (var context = new Recipe1Context()) { var order = context.Orders.FirstOrDefault(x => x.OrderId == id); context.Orders.Remove(order); context.SaveChanges(); // Return Http Status code of 200, informing client that resouce removed successfully return Request.CreateResponse(HttpStatusCode.OK); } } private void Cleanup() { using (var context = new Recipe1Context()) { context.Database.ExecuteSqlCommand("delete from [orders]"); } } }
در قدم بعدی اپلیکیشن کلاینت را میسازیم که از سرویس Web API استفاده میکند.
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
- کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.
نکته: قسمت هایی از اپلیکیشن که باید در لایههای مختلف مورد استفاده قرار گیرند - مانند کلاسهای موجودیتها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا میتوانید پروژه ای از نوع Class Library بسازید و تمام موجودیتها را در آن تعریف کنید. سپس لایههای مختلف این پروژه را ارجاع خواهند کرد.
فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.
private HttpClient _client; private Order _order; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { // create instance of the program class var program = new Program(); program.ServiceSetup(); program.CreateOrder(); // do not proceed until order is added await program.PostOrderAsync(); program.ChangeOrder(); // do not proceed until order is changed await program.PutOrderAsync(); // do not proceed until order is removed await program.RemoveOrderAsync(); } private void ServiceSetup() { // map URL for Web API cal _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") }; // add Accept Header to request Web API content // negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept. Add(new MediaTypeWithQualityHeaderValue("application/json")); } private void CreateOrder() { // Create new order _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" }; } private async Task PostOrderAsync() { // leverage Web API client side API to call service var response = await _client.PostAsJsonAsync("api/order", _order); Uri newOrderUri; if (response.IsSuccessStatusCode) { // Capture Uri of new resource newOrderUri = response.Headers.Location; // capture newly-created order returned from service, // which will now include the database-generated Id value _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully created order. Here is URL to new resource: {0}", newOrderUri); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private void ChangeOrder() { // update order _order.Quantity = 10; } private async Task PutOrderAsync() { // construct call to generate HttpPut verb and dispatch // to corresponding Put method in the Web API Service var response = await _client.PutAsJsonAsync("api/order", _order); if (response.IsSuccessStatusCode) { // capture updated order returned from service, which will include new quanity _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully updated order: {0}", response.StatusCode); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private async Task RemoveOrderAsync() { // remove order var uri = "api/order/" + _order.OrderId; var response = await _client.DeleteAsync(uri); if (response.IsSuccessStatusCode) Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode); else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); }
Successfully updated order: OK
Sucessfully deleted order: OK
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر Web API دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله اپلیکیشن در حال اجرا است و سرویسهای ما قابل دسترسی هستند.
حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی میکنیم. با این کار از سرویس مورد نظر درخواست میکنیم که دادهها را با فرمت JSON بازگرداند. سپس یک آبجکت Order میسازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال میکنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت میکند و آن را به لیست موجودیتها در Context جاری اضافه مینماید. این عمل باعث میشود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات میکند. در آخر با فراخوانی متد SaveChanges دادهها را ذخیره میکنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار میدهیم و به کلاینت ارسال میکنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینتها درخواستهای ایجاد رکورد جدید را بصورت POST ارسال میکنند. درخواستهای HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت میشوند.
در مرحله بعد عملیات بعدی را اجرا میکنیم، تعداد سفارش را تغییر میدهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال میکنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت میشود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر میدهیم، که این کار موجودیت را به Context جاری میچسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی میکنیم. در شمارههای بعدی این سری از مقالات، خواهیم دید چگونه میتوان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام میرسانیم.
در مرحله بعد، عملیات نهایی را اجرا میکنیم که موجودیت Order را از منبع داده حذف میکند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه میکنیم و متد DeleteAsync را فراخوانی میکنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی میکنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری میکند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد میشود.
در یک اپلیکیشن واقعی بهتر است کد دسترسی دادهها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.
Stored Procedure چیست ؟
Stored Procedure یا SP یا به زبان فارسی «رویههای ذخیره شده» اشیایی اجرا پذیر در بانک اطلاعاتی SQL Server هستند که شامل یک یا چندین دستور SQL میشوند. این رویهها میتوانند پارامترهای ورودی و خروجی داشته باشند؛ همچنین میتوانند لیستی از موجودیتها را نیز برگردانند و یا میتوان داخل این رویهها به زبان T-SQL برنامه نویسی کرد.
مهمترین کاربر این رویهها، ذخیره کردن دستورات Select , Insert , Update , Delete هست و یا ترکیبی از اینها .
اشکال راه حلهای پیش فرض مبتنی بر Context
برای استفاده از راه حلهای پیش فرض مبتنی بر Context، همانطور که در مقاله «استفاده از امکانات بومی بانکهای اطلاعاتی» به آن پرداخته شده، سه روش کلی برای استفاده از Stored Procedure پیشنهاد شدهاست:
- روش اول استفاده از متد fromsql است. اشکال این متد، محدودیت استفاده برای یک موجودیت برنامه است و به زبان ساده نمیتوان در کوئری پایگاه داده از join استفاده کرد.
- روش دوم استفاده از متد ExecuteSqlCommand موجود در context برنامه است . اشکال این متد void بودن آن است که باعث میشود بازگشتی از پایگاه داده حاصل نشود.
- روش سوم استفاده از متد ExecuteScalar موجود در Context برنامه است. اشکالی که به این متد گرفته میشود، Scalar بودن مقدار بازگشتی از آن است که باعث میشود نتوانیم لیستی از موجودیتها را به ViewModel مورد نظر نگاشت کنیم.
راه حل این مشکل
برای حل این مشکلات که بسیار هم مهم هستند، اول باید قطعه کد زیر را به Context برنامه اضافه نمود:
public void OpenConnection() { Database.OpenConnection(); } public DbCommand Command() { DbCommand cmd = Database.GetDbConnection().CreateCommand(); return cmd; }
void OpenConnection(); DbCommand Command();
public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<ISpReader, SpReader>(); }
public List<ViewModel> GetFromSp <ViewModel>(string[,] Parametr, string NameSp) where ViewModel : new()
{ _uow.OpenConnection(); DbCommand cmd = _uow.Command(); cmd.CommandText = NameSp; cmd.CommandType = CommandType.StoredProcedure; var countParametr = Parametr.GetLength(0); for (int i = 0; i < countParame tr; i++) { cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] }); } List<ViewModel> list = new List<ViewModel >(); using (var reader = cmd.ExecuteReader()) { if (reader != null && reader.HasRows) { var entity = typeof(ViewModel); var propDict = new Dictionary<string, PropertyInfo>(); var props = entity.GetProperties (BindingFlags.Instance | BindingFlags.Public); propDict = props.ToDictionary(p => p.Name.ToUpper(), p => p); while (reader.Read()) { ViewModel newobject = new ViewModel (); for (int index = 0; index < reader.FieldCount; index++) { if (propDict.ContainsKey(reader.GetName(index).ToUpper())) { var info = propDict[reader.GetName(index).ToUpper()]; if ((info != null) && info.CanWrite) { var val = reader.GetValue(index); info.SetValue(newobject, (val == DBNull.Value) ? null : val, null); } } } list.Add(newobject); } } return list; }
همچنین میتوان برای استفاده این متد برای رویههای بدون پارامتر ورودی، از OverLoad این متد، با حذف قطعات کد زیر:
var countParametr = Parametr.GetLength(0); for (int i = 0; i < countParametr; i++) { cmd.Parameters.Add(new SqlParameter { ParameterName = Parametr[i, 0], Value = Parametr[i, 1] }); }
روش استفاده از این متد
برای استفاده از این متد، لازم است چند نکته رعایت شوند:
1- خروجی Stored Procedure دقیقا منطبق بر ViewModel ارسالی به متد جهت تشکیل لیست باشد.
2- لیست پارامترها باید بصورت آرایه دوبعدی باشد که اندازه بعد اول، تعداد پارامترها و اندازه بعد دوم 2 باشد.
3- در ماتریسی که از این پارامترها ساخته میشود، ستون اول نام پارامتر و ستون دوم مقدار پارامتر ست میشود.
بطور مثال Stored Procedure زیر حاوی سه پارامتر است :
CREATE PROCEDURE [dbo].[isRelation]( @TableName as varchar(50), @FieldOfRelation as varchar(70), @ValueOfField as int)
public class EntityServices : IEntityService { private ISpreader _Reader; public EntityServices( ISpreader reader) { _Reader = reader; } public List<StoreProcedureResultViewModel> IsRelation(string tableName , int keyValue, string keyFieldName) { List<StoreProcedureResultViewModel> IsContact; try { string[,] Parametr = new string[3, 2]; Parametr[0, 0] = "@TableName"; Parametr[0, 1] = tableName ; Parametr[1, 0] = "@ValueOfField"; Parametr[1, 1] = keyValue.ToString().Trim(); Parametr[2, 0] = "@FieldOfRelation"; Parametr[2, 1] = keyFieldName.Trim(); IsContact = _Reader.GetSp<StoreProcedureResultViewModel>(Parametr, "IsRelation"); return IsContact; } catch (Exception ex) { } } }
پایان پشتیبانی از Java 7
PM> Install-Package LightInject
PM> Install-Package LightInject.Source
public class PersonModel { public int Id { get; set; } public string Name { get; set; } public string Family { get; set; } public DateTime Birth { get; set; } } public interface IRepository<T> where T:class { void Insert(T entity); IEnumerable<T> FindAll(); } public interface IPersonRepository:IRepository<PersonModel> { } public class PersonRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public interface IPersonService { void Insert(PersonModel entity); IEnumerable<PersonModel> FindAll(); } public class PersonService:IPersonService { private readonly IPersonRepository _personRepository; public PersonService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { _personRepository.Insert(entity); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
public partial class Form1 : Form { private readonly IPersonService _personService; public Form1(IPersonService personService) { _personService = personService; InitializeComponent(); } }
static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonRepository, PersonRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>())); }
public class WorkerModel:PersonModel { public ManagerModel Manager { get; set; } } public class ManagerModel:PersonModel { public IEnumerable<WorkerModel> Workers { get; set; } } public class WorkerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class ManagerRepository:IPersonRepository { public void Insert(PersonModel entity) { throw new NotImplementedException(); } public IEnumerable<PersonModel> FindAll() { throw new NotImplementedException(); } } public class WorkerService:IPersonService { private readonly IPersonRepository _personRepository; public WorkerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var worker = entity as WorkerModel; _personRepository.Insert(worker); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } } public class ManagerService:IPersonService { private readonly IPersonRepository _personRepository; public ManagerService(IPersonRepository personRepository) { _personRepository = personRepository; } public void Insert(PersonModel entity) { var manager = entity as ManagerModel; _personRepository.Insert(manager); } public IEnumerable<PersonModel> FindAll() { return _personRepository.FindAll(); } }
... var container = new ServiceContainer(); container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>()));
... container.Register<IPersonService, PersonService>("PersonService"); container.Register<IPersonService, WorkerService>(); container.Register<IPersonRepository, PersonRepository>(); container.Register<IPersonRepository, WorkerRepository>(); Application.Run(new Form1(container.GetInstance<IPersonService>("PersonService")));
container.Register<IPersonService, PersonService>("PersonService"); Application.Run(new Form1(container.GetInstance<IPersonService>()));
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetInstance<IEnumerable<IPersonService>>();
container.Register<IPersonService, PersonService>(); container.Register<IPersonService, WorkerService>("WorkerService"); var personList = container.GetAllInstances<IPersonService>();
- Array
- ICollection<T>
- IList<T>
- IReadOnlyCollection<T>
- IReadOnlyList<T>
container.RegisterInstance<string>("SomeValue"); var value = container.GetInstance<string>();
container.RegisterInstance<string>("SomeValue","String1"); container.RegisterInstance<string>("OtherValue","String2"); var value = container.GetInstance<string>("String2");
نیاز به یک Dispatcher برای تعامل با بیش از یک مخزن حالت
در اینجا برای نمونه دو مخزن حالت تعریف شدهاند؛ اما روش تعامل با این مخازن حالت، دیگر مانند قبل نیست. برای نمونه در اثر تعامل یک کاربر با View ای خاص، رخدادی صادر شده و اینبار مدیریت این رخداد توسط یک Action (که عموما یک پیام رشتهای است)، به Dispatcher مرکزی ارسال میشود (و نه مستقیما به مخزن حالت خاصی). اکنون این Dispatcher، اکشن رسیده را به مخازن کد مشترک به آن ارسال میکند تا عمل متناسب با آن اکشن درخواستی را انجام دهند. مابقی آن همانند قبل است که پس از تغییر حالت در هر کدام از مخازن حالت، کار به روز رسانی UI، در کامپوننتهای مشترک صورت خواهد گرفت. بدیهی است در اینجا مخازن حالت، مجاز به صرفنظر کردن از یک اکشن خاص هستند و الزامی به پیاده سازی آن ندارند. هدف اصلی این است که اگر اکشنی قرار بود در تمام مخازن حالت پیاده سازی شود و حالتهای آنها را تغییر دهد، روشی را برای مدیریت آن داشته باشیم.
بنابراین اگر به این الگوی جدید دقت کنید، چیزی نیست بجز یک الگوی Observer دو سطحی:
الف) Dispatcher ای (Subject) که مشترکهایی را مانند مخازن حالت دارد (Observers).
ب) مخازن حالتی (Subjects) که مشترکهایی را مانند کامپوننتها دارند (Observers).
اگر پیشتر با React کار کرده باشید، این الگو را تحت عناوینی مانند Flux و یا Redux میشناسید و در اینجا میخواهیم پیاده سازی #C آنرا بررسی کنیم:
در الگوی Flux، در اثر تعامل یک کاربر با کامپوننتی، اکشنی به سمت یک Dispatcher ارسال میشود. سپس Dispatcher این اکشن را به مخزن حالتی جهت مدیریت آن ارسال میکند که در نهایت سبب تغییر حالت آن شده و به روز رسانی UI را در پی خواهد داشت.
پیاده سازی یک Dispatcher برای تعامل با بیش از یک مخزن حالت
پیش از هر کاری نیاز است قالب اکشنهای ارسالی را که قرار است توسط مخازن حالت مورد پردازش قرار گیرند، مشخص کنیم:
namespace BlazorStateManagement.Stores { public interface IAction { public string Name { get; } } }
namespace BlazorStateManagement.Stores.CounterStore { public class IncrementAction : IAction { public const string Increment = nameof(Increment); public string Name { get; } = Increment; } public class DecrementAction : IAction { public const string Decrement = nameof(Decrement); public string Name { get; } = Decrement; } }
پس از تعریف ساختار یک اکشن، اکنون نوبت به پیاده سازی راه حلی برای ارسال آن به تمام مخازن حالت برنامه است:
using System; namespace BlazorStateManagement.Stores { public interface IActionDispatcher { void Dispatch(IAction action); void Subscribe(Action<IAction> actionHandler); void Unsubscribe(Action<IAction> actionHandler); } public class ActionDispatcher : IActionDispatcher { private Action<IAction> _actionHandlers; public void Subscribe(Action<IAction> actionHandler) => _actionHandlers += actionHandler; public void Unsubscribe(Action<IAction> actionHandler) => _actionHandlers -= actionHandler; public void Dispatch(IAction action) => _actionHandlers?.Invoke(action); } }
این سرویس را نیز با طول عمر Scoped به سیستم تزریق وابستگیهای برنامه معرفی میکنیم که سبب میشود تا پایان عمر برنامه (بسته شدن مرورگر یا ریفرش کامل صفحهی جاری)، در حافظه باقی مانده و وهله سازی مجدد نشود. به همین جهت تزریق آن در مخازن حالت مختلف برنامه، دقیقا حالت یک Dispatcher اشتراکی را پیدا خواهد کرد.
namespace BlazorStateManagement.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IActionDispatcher, ActionDispatcher>(); // ... } } }
استفاده از IActionDispatcher در مخازن حالت برنامه
در ادامه میخواهیم مخازن حالت برنامه را تحت کنترل سرویس IActionDispatcher قرار دهیم تا کاربر بتواند اکشنی را به Dispatcher ارسال کند و سپس Dispatcher این درخواست را به تمام مخازن حالت موجود، جهت بروز واکنشی (در صورت نیاز)، اطلاعات رسانی نماید.
برای این منظور سرویس ICounterStore قسمت قبل ، به صورت زیر تغییر میکند که اینترفیس IDisposable را پیاده سازی کرده و همچنین دیگر به همراه متدهای عمومی افزایش و یا کاهش مقدار نیست:
using System; namespace BlazorStateManagement.Stores.CounterStore { public interface ICounterStore : IDisposable { CounterState State { get; } void AddStateChangeListener(Action listener); void BroadcastStateChange(); void RemoveStateChangeListener(Action listener); } }
using System; namespace BlazorStateManagement.Stores.CounterStore { public class CounterStore : ICounterStore { private readonly CounterState _state = new(); private bool _isDisposed; private Action _listeners; private readonly IActionDispatcher _actionDispatcher; public CounterStore(IActionDispatcher actionDispatcher) { _actionDispatcher = actionDispatcher ?? throw new ArgumentNullException(nameof(actionDispatcher)); _actionDispatcher.Subscribe(HandleActions); } private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } } public CounterState State => _state; private void IncrementCount() { _state.Count++; BroadcastStateChange(); } private void DecrementCount() { _state.Count--; BroadcastStateChange(); } public void AddStateChangeListener(Action listener) => _listeners += listener; public void RemoveStateChangeListener(Action listener) => _listeners -= listener; public void BroadcastStateChange() => _listeners.Invoke(); public void Dispose() { Dispose(disposing: true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_isDisposed) { try { if (disposing) { _actionDispatcher.Unsubscribe(HandleActions); } } finally { _isDisposed = true; } } } } }
- با توجه به اینکه CounterStore یک سرویس ثبت شدهی در سیستم است، میتواند از مزیت تزریق سایر سرویسها در سازندهی خودش بهرهمند شود؛ مانند تزریق سرویس جدید IActionDispatcher.
- پس از تزریق سرویس جدید IActionDispatcher، متدهای Subscribe آنرا در سازندهی کلاس و Unsubscribe آنرا در حین Dispose سرویس، فراخوانی میکنیم. البته فراخوانی و یا پیاده سازی Unsubscribe و Dispose در اینجا غیرضروری است؛ چون طول عمر این کلاس با طول عمر برنامه یکی است.
- بر اساس این الگوی جدید، هر اکشنی که به سمت Dispatcher مرکزی ارسال میشود، در نهایت به متد HandleActions یکی از مخازن حالت تعریف شده، خواهد رسید:
private void HandleActions(IAction action) { switch (action) { case IncrementAction: IncrementCount(); break; case DecrementAction: DecrementCount(); break; } }
@inject ICounterStore CounterStore @code { private void IncrementCount() { CounterStore.IncrementCount(); }
- ابتدا در انتهای فایل Client\_Imports.razor، فضای نام سرویس جدید IActionDispatcher را اضافه میکنیم:
@using BlazorStateManagement.Stores
// ... @inject IActionDispatcher ActionDispatcher @code { private void IncrementCount() { ActionDispatcher.Dispatch(new IncrementAction()); }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: BlazorStateManagement-Part-2.zip
public static void SetRowVersionOnInsert(this IUnitOfWork uow, string table) { uow.ExecuteSqlCommand( $@" CREATE TRIGGER Set{table}RowVersion AFTER INSERT ON {table} BEGIN UPDATE {table} SET RowVersion = randomblob(8) WHERE Id = NEW.Id; END "); }
public static void SetRowVersionOnUpdate(this IUnitOfWork uow, string table) { uow.ExecuteSqlCommand( $@" CREATE TRIGGER Set{table}RowVersion AFTER UPDATE ON {table} BEGIN UPDATE {table} SET RowVersion = randomblob(8) WHERE Id = NEW.Id; END "); }
_serviceProvider.RunScopedService<IUnitOfWork>(uow => { uow.SetRowVersionOnInsert(nameof(MeasurementUnit)); uow.Set<MeasurementUnit>().Add(measurementUnit1); uow.Set<MeasurementUnit>().Add(measurementUnit2); uow.Set<MeasurementUnit>().Add(measurementUnit3); uow.SaveChanges(); });
MVC Scaffolding #1
کل سری ASP.NET MVC
به همراه کل سری EF Code First
MVC Scaffolding چیست؟
MVC Scaffolding ابزاری است برای تولید خودکار کدهای «اولیه» برنامه، جهت بالا بردن سرعت تولید برنامههای ASP.NET MVC مبتنی بر EF Code First.
بررسی مقدماتی MVC Scaffolding
امکان اجرای ابزار MVC Scaffolding از دو طریق دستورات خط فرمان Powershell و یا صفحه دیالوگ افزودن یک کنترلر در پروژههای ASP.NET MVC وجود دارد. در ابتدا حالت ساده و ابتدایی استفاده از صفحه دیالوگ افزودن یک کنترلر را بررسی خواهیم کرد تا با کلیات این فرآیند آشنا شویم. سپس در ادامه به خط فرمان Powershell که اصل توانمندیها و قابلیتهای سفارشی MVC Scaffolding در آن قرار دارد، خواهیم پرداخت.
برای این منظور یک پروژه جدید MVC را آغاز کنید؛ ابزارهای مقدماتی MVC Scaffolding از اولین به روز رسانی ASP.NET MVC3 به بعد با VS.NET یکپارچه هستند.
ابتدا کلاس زیر را به پوشه مدلهای برنامه اضافه کنید:
using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class Task { public int Id { set; get; } [Required] public string Name { set; get; } [DisplayName("Due Date")] public DateTime? DueDate { set; get; } [DisplayName("Is Complete")] public bool IsComplete { set; get; } [StringLength(450)] public string Description { set; get; } } }
همانطور که ملاحظه میکنید در قسمت قالبها، تولید کنترلرهایی با اکشن متدهای ثبت و نمایش اطلاعات مبتنی بر EF Code First انتخاب شده است. کلاس مدل نیز به کلاس Task فوق تنظیم گردیده و در زمان انتخاب DbContext مرتبط، گزینه new data context را انتخاب کرده و نام پیش فرض آنرا پذیرفتهایم. زمانیکه بر روی دکمه Add کلیک کنیم، اتفاقات ذیل رخ خواهند داد:
الف) کنترلر جدید TasksController.cs به همراه تمام کدهای Insert/Update/Delete/Display مرتبط تولید خواهد شد.
ب) کلاس DbContext خودکاری به نام MvcApplication1Context.cs در پوشه مدلهای برنامه ایجاد میگردد تا کلاس Task را در معرض دید EF Code first قرار دهد. (همانطور که عنوان شد یکی از پیشنیازهای بحث Scaffolding آشنایی با EF Code first است)
ج) در پوشه Views\Tasks، پنج View جدید را جهت مدیریت فرآیندهای نمایش صفحات Insert، حذف، ویرایش، نمایش و غیره تهیه میکند.
د) فایل وب کانفیگ برنامه جهت درج رشته اتصالی به بانک اطلاعاتی تغییر کرده است. حالت پیش فرض آن استفاده از SQL CE است و برای استفاده از آن نیاز است قسمت 15 سری EF سایت جاری را پیشتر مطالعه کرده باشید (به چه اسمبلیهای دیگری مانند System.Data.SqlServerCe.dll برای اجرا نیاز است و چطور باید اتصال به بانک اطلاعاتی را تنظیم کرد)
معایب:
کیفیت کد تولیدی پیش فرض قابل قبول نیست:
- DbContext در سطح یک کنترلر وهله سازی شده و الگوی Context Per Request در اینجا بکارگرفته نشده است. واقعیت یک برنامه ASP.NET MVC کامل، داشتن چندین Partial View تغدیه شونده از کنترلرهای مختلف در یک صفحه واحد است. اگر قرار باشد به ازای هر کدام یکبار DbContext وهله سازی شود یعنی به ازای هر صفحه چندین بار اتصال به بانک اطلاعاتی باید برقرار شود که سربار زیادی را به همراه دارد. (قسمت 12 سری EF سایت جاری)
- اکشن متدها حاوی منطق پیاده سازی اعمال CRUD یا همان Create/Update/Delete هستند. به عبارتی از یک لایه سرویس برای خلوت کردن اکشن متدها استفاده نشده است.
- از ViewModel تعریف شدهای به نام Task هم به عنوان Domain model و هم ViewModel استفاده شده است. یک کلاس متناظر با جداول بانک اطلاعاتی میتواند شامل فیلدهای بیشتری باشد و نباید آنرا مستقیما در معرض دید یک View قرار داد (خصوصا از لحاظ مسایل امنیتی).
مزیتها:
قسمت عمدهای از کارهای «اولیه» تهیه یک کنترلر و همچنین Viewهای مرتبط به صورت خودکار انجام شدهاند. کارهای اولیهای که با هر روش و الگوی شناخته شدهای قصد پیاده سازی آنها را داشته باشید، وقت زیادی را به خود اختصاص داده و نهایتا آنچنان تفاوت عمدهای هم با کدهای تولیدی در اینجا نخواهند داشت. حداکثر فرمهای آنرا بخواهید با jQuery Ajax پیاده سازی کنید یا کنترلهای پیش فرض را با افزونههای jQuery غنی سازی نمائید. اما شروع کار و کدهای اولیه چیزی بیشتر از این نیست.
نصب بسته اصلی MVC Scaffolding توسط NuGet
بسته اصلی MVC Scaffolding را با استفاده از دستور خط فرمان Powershell ذیل، از طریق منوی Tools، گزینه Library package manager و انتخاب Package manager console میتوان به پروژه خود اضافه کرد:
Install-Package MvcScaffolding
Attempting to resolve dependency 'T4Scaffolding'. Attempting to resolve dependency 'T4Scaffolding.Core'. Attempting to resolve dependency 'EntityFramework'. Successfully installed 'T4Scaffolding.Core 1.0.0'. Successfully installed 'T4Scaffolding 1.0.8'. Successfully installed 'MvcScaffolding 1.0.9'. Successfully added 'T4Scaffolding.Core 1.0.0' to MvcApplication1. Successfully added 'T4Scaffolding 1.0.8' to MvcApplication1. Successfully added 'MvcScaffolding 1.0.9' to MvcApplication1.
پس از اینکه بسته MvcScaffolding به پروژه جاری اضافه شد، همان مراحل قبل را که توسط صفحه دیالوگ افزودن یک کنترلر انجام دادیم، اینبار به کمک دستور ذیل نیز میتوان پیاده سازی کرد:
Scaffold Controller Task
نکته و مزیت مهم دیگری که در اینجا در دسترس میباشد، سوئیچهای خط فرمانی است که به همراه صفحه دیالوگ افزودن یک کنترلر وجود ندارند. برای مثال دستور Scaffold Controller را تایپ کرده و سپس یک خط تیره را اضافه کنید. اکنون دکمه tab را مجددا بفشارید. منویی ظاهر خواهد شد که بیانگر سوئیچهای قابل استفاده است.
برای مثال اگر بخواهیم دستور Scaffold Controller Task را با جزئیات اولیه کاملتری ذکر کنیم، مانند تعیین نام دقیق کلاس مدل و کنترلر تولیدی به همراه نام دیگری برای DbContext مرتبط، خواهیم داشت:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext
بهبود مقدماتی کیفیت کد تولیدی MVC Scaffolding
در همان کنسول پاروشل NuGet، کلید up arrow را فشار دهید تا مجددا دستور قبلی اجرا شده ظاهر شود. اینبار دستور قبلی را با سوئیچ جدید Repository (استفاده از الگوی مخزن) اجرا کنید:
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository
Scaffold Controller -ModelType Task -ControllerName TasksController -DbContextType TasksDbContext -Repository -Force
public class TasksController : Controller { private readonly ITaskRepository taskRepository; // If you are using Dependency Injection, you can delete the following constructor public TasksController() : this(new TaskRepository()) { } public TasksController(ITaskRepository taskRepository) { this.taskRepository = taskRepository; }
public interface ITaskRepository : IDisposable { IQueryable<Task> All { get; } IQueryable<Task> AllIncluding(params Expression<Func<Task, object>>[] includeProperties); Task Find(int id); void InsertOrUpdate(Task task); void Delete(int id); void Save(); }
پیاده سازی این اینترفیس در حالت متد Save آن شامل فراخوانی context.SaveChanges است. این مورد باید به الگوی واحد کار (که در اینجا تعریف نشده) منتقل شود. زیرا در یک دنیای واقعی حاصل کار بر روی چندین موجودیت باید در یک تراکنش ذخیره شوند و قرارگیری متد Save داخل کلاس مخزن یا سرویس برنامه، مخزنهای تعریف شده را تک موجودیتی میکند.
اما در کل با توجه به اینکه پیاده سازی منطق کار با موجودیتها به کلاسهای مخزن واگذار شدهاند و کنترلرها به این نحو خلوتتر گردیدهاند، یک مرحله پیشرفت محسوب میشود.