هدایت خودکار کاربر به صفحه لاگین در حین اعمال Ajax ایی
Angular Interceptors
ابتدا مشکل و هدف را بیان میکنیم:
مشکل: کاربر در صفحهای حضور دارد که نیاز به اعتبارسنجی داشته و مدت اعتبار کاربر نیز تمام شده است، ولی هنوز در صفحهای که نباید حضور داشته باشد، حضور دارد و بدتر از آن این است که میتواند درخواستهای بی نتیجهای را نیز ارسال کند.
هدف: کاربر را سریعا به صفحهای که به آن تعلق دارد هدایت کنیم ( یعنی صفحهی ورود به سیستم ).
و حالا از ابتدا پروسه را دنبال میکنیم. یک Controller سمت سرور داریم به این صورت :
[Authorize(Roles = AuthorizeRole.SuperAdministrator)] public partial class HomeController : Controller { [HttpPost] [AngularValidateAntiForgeryToken] public virtual JsonResult GetUserInfo() { var userInfoViewModel = _applicationUserManager.GetUserInfoById(User.Identity.GetUserId()); return Json(userInfoViewModel); } }
یعنی با کدی همانند کد زیر:
$scope.getUserInfo = function() { $http({ method: 'POST', url: 'Home/GetUserInfo', headers: $scope.getHeaders() }). success(function(data, status, headers, config) { $scope.userInfo = data; }). error(function(data, status, headers, config) { }).then(function(res) { }); }
و نتیجهی کدهای بالا به صورت زیر درخواهد آمد :
همانطور که میبینید دادههای اولیه کاربر پس از ورود به سیستم، بدون هیچ مشکلی دریافت میشوند.
نکته : زمانیکه status برابر با 200 هست، یعنی درخواست OK میباشد. ( در پیوست ، لیست تمامی کدها قرار داده شده است )
حالا فرض کنید کاربر در صفحه حضور دارد و به هر دلیلی اعتبار حضور کاربر منقضی شده است و حالا پس از مدتی کاربر درخواستی را به سرور ارسال میکند و میخواهد اطلاعات خودش را مشاهده کند.
درخواست کاربر با همان کدهای اولیه ارسال میشود و خروجی اینبار به صورت زیر در خواهد آمد :
همانطور که میبینید وضعیت اینبار نیز OK میباشد، ولی هیچ دادهای از سرور دریافت نشده است. کاربر قطعا در اینجا دچار سردرگمی میشود. چون هیچ چیزی را مشاهده نمیکند و به هیچ مسیر دیگری نیز هدایت نمیشود و هرکار دیگری نیز انجام دهد، پاسخی مشاهده نمیکند.
راه حل چیست ؟
ابتدا باید برای درخواستهای Ajax ایی اعتبارسنجی را اعمال کنیم. برای این کار باید یک Attribute جدید بسازیم. یعنی به این صورت :
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] public class MyCustomAuthorize : AuthorizeAttribute { protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext) { if (filterContext.HttpContext.Request.IsAjaxRequest()) { // یعنی اعتبارسنجی نشده است filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized; filterContext.HttpContext.Response.End(); } base.HandleUnauthorizedRequest(filterContext); } }
توجه : کد کاملتر همراه با توضیحات در بخش پیش نیازها آمده است.
حالا در سمت کلاینت نیز به این صورت عمل میکنیم :
$http({ method: 'POST', url: 'Home/GetUserInfo', headers: $scope.getHeaders() }). success(function(data, status, headers, config) { $scope.userInfo = data; }). error(function(data, status, headers, config) { if (status === 401) { // you are not authorized } }).then(function(res) { });
حالا دیگر متوجه خواهیم شد که کاربر اعتبارسنجی نشده است، یا اعتبار آن منقضی شده است و میتوانیم کاربر را به مسیر "ورود به سیستم" هدایت کنیم.
در console مرورگر نیز خطای زیر رخ میدهد :
POST http://localhost:000000/Administrator/Home/GetUserInfo 401 (Unauthorized)
حالا باید از یک Interceptor استفاده و درخواستهای HTTP خودمان را مدیریت کنیم. برای این منظور ما یک Interceptor جدید را همانند کدهای زیر مینویسیم:
factory('AuthorizedInterceptor', function ($q) { return { response: function(response) { return response || $q.when(response); }, responseError: function(rejection) { if (rejection.status === 401) { // you are not authorized } return $q.reject(rejection); } }; })
همانطور که مشاهده میکنید کدهای خطای درخواست http$ را در Interceptor قرار دادیم. حالا کاربر درخواستی را ارسال میکند و ما با توجه به این Interceptor متوجه خواهیم شد که آیا اعتبار آن منقضی شده است یا خیر و در صورت منقضی شدن، کاربر را به صفحهی ورود هدایت خواهیم کرد. یعنی بدین صورت :
در خروجی بالا میبینید که کاربر، اعتبارسنجی نشده است و آن را به مسیر ورود هدایت خواهیم کرد.
با Interceptor بالا دیگر نیازی نیست برای هر درخواستی این وضعیت را چک کنیم و به صورت خودکار این چک کردن رخ خواهد داد.
پیوست : http_status_codes.rar
کلاسهای موجودیتهای مثال جاری
برای توضیح قابلیت جدید مقدار دهی اولیهی بانک اطلاعاتی در +EF Core 2.1، از کلاسهای موجودیتهای ذیل استفاده خواهیم کرد:
public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public string Publisher { get; set; } public List<Article> Articles { get; set; } } public class Article { public int ArticleId { get; set; } public string Title { get; set; } public DateTime PublishDate { get; set; } public int MagazineId { get; set; } public Author Author { get; set; } public int? AuthorId { get; set; } } public class Author { public int AuthorId { get; set; } public string Name { get; set; } public List<Article> Articles { get; set; } }
روش مقدار دهی اولیهی تک موجودیتها
اکنون فرض کنید قصد داریم جدول مجلات را مقدار دهی اولیه کنیم. برای اینکار خواهیم داشت:
protected override void OnModelCreating (ModelBuilder modelBuilder) { modelBuilder.Entity<Magazine>().HasData(new Magazine { MagazineId = 1, Name = "DNT Magazine" }); }
- ذکر صریح مقدار Id یک رکورد (هرچند نوع Id آن auto-increment است).
- عدم ذکر مقدار Publisher.
اکنون اگر توسط دستورات Migrations مانند dotnet ef migrations add init، کار تولید کدهای متناظر به روز رسانی بانک اطلاعاتی را بر اساس این کدها تولید کنیم، در قسمتی از آن، یک چنین خروجی را دریافت خواهیم کرد:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Magazine", null });
set IDENTITY_INSERT ON INSERT INTO "Magazines" ("MagazineId", "Name", "Publisher") VALUES (1, 'DNT Magazine', NULL);
توسط متد HasData امکان درج چندین رکورد با هم نیز وجود دارد:
modelBuilder.Entity<Magazine>() .HasData(new Magazine{ MagazineId=2, Name="This Mag" }, new Magazine{ MagazineId=3, Name="That Mag" } );
البته باید دقت داشت که متد HasData، برای کار با یک تک موجودیت، طراحی شدهاست و توسط آن نمیتوان در چندین جدول بانک اطلاعاتی، مقادیری را درج کرد.
در مورد دادههای نالنپذیر چطور؟
در مثال فوق اگر تنظیمات خاصیت Publisherای را که نال وارد کردیم، نالنپذیر تعریف کنیم:
modelBuilder.Entity<Magazine>().Property(m=>m.Publisher).IsRequired();
"The seed entity for entity type 'Magazine' cannot be added because there was no value provided for the required property 'Publisher'."
امکان استفادهی از Anonymous Types در متد HasData
فرض کنید برای کلاس موجودیت خود یک سازنده را نیز تعریف کردهاید:
public Magazine(string name, string publisher) { Name=name; Publisher=publisher; }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Magazine", "1105 Media"));
modelBuilder.Entity<Magazine>().HasData(new {MagazineId=1, Name="DNT Mag", Publisher="1105 Media"});
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher" }, values: new object[] { 1, "DNT Mag", "1105 Media" });
حالت دیگر استفادهی از این قابلیت، کار با خواصی هستند که private set میباشند. فرض کنید کلاس موجودیت Magazine را به صورت زیر تغییر دادهاید:
public class Magazine { public Magazine(string name, string publisher) { Name=name; Publisher=publisher; MagazineId=Guid.NewGuid(); } public Guid MagazineId { get; private set; } public string Name { get; private set; } public string Publisher { get; private set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData(new Magazine("DNT Mag", "1105 Media");
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی اطلاعات به هم مرتبط
همانطور که پیشتر نیز ذکر شد، متد HasData تنها با یک تک موجودیت کار میکند و روش کار آن همانند کار با DbSetها نیست. به همین جهت نمیتوان اشیاء به هم مرتبط را توسط آن در بانک اطلاعاتی درج کرد. بنابراین برای درج اطلاعات یک مجله و مقالات مرتبط با آن، ابتدا باید مجله را ثبت کرد و سپس بر اساس Id آن مجله، کلید خارجی مقالات را به صورت جداگانهای مقدار دهی نمود:
modelBuilder.Entity<Article>().HasData(new Article { ArticleId = 1, MagazineId = 1, Title = "EF Core 2.1 Query Types"});
var mag1=new {MagazineId= new Guid("0483b59c-f7f8-4b21-b1df-5149fb57984e"), Name="DNT Mag", Publisher="1105 Media"}; modelBuilder.Entity<Magazine>().HasData(mag1);
مقدار دهی اولیهی Owned Entities
complex types در EF 6x با مفهوم دیگری به نام owned types در EF Core جایگزین شدهاند:
public class Publisher { public string Name { get; set; } public int YearFounded { get; set; } } public class Magazine { public int MagazineId { get; set; } public string Name { get; set; } public Publisher Publisher { get; set; } public List<Article> Articles { get; set; } }
modelBuilder.Entity<Magazine>().HasData (new Magazine { MagazineId = 1, Name = "DNT Magazine" }); modelBuilder.Entity<Magazine>().OwnsOne (m => m.Publisher) .HasData (new { Name = "1105 Media", YearFounded = 2006, MagazineId=1 });
این دو دستور، خروجی Migrations زیر را تولید میکنند:
migrationBuilder.InsertData( table: "Magazines", columns: new[] { "MagazineId", "Name", "Publisher_Name", "Publisher_YearFounded" }, values: new object[] { 1, "DNT Magazine", "1105 Media", 2006 });
محل صحیح اجرای Migrations در برنامههای ASP.NET Core 2x
زمانیکه متد ()context.Database.Migrate را اجرا میکنید، تمام مهاجرتهای اعمال نشده را به بانک اطلاعاتی اعمال میکند که این مورد شامل اجرای دستورات HasData نیز هست. روش فراخوانی این متد در ASP.NET Core 1x به صورت زیر در متد Configure کلاس Startup بود (و البته هنوز هم کار میکند):
namespace EFCoreMultipleDb.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { applyPendingMigrations(app); // ... } private static void applyPendingMigrations(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetService<IUnitOfWork>(); uow.Migrate(); } } } }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContext : DbContext, IUnitOfWork { // ... public void Migrate() { this.Database.Migrate(); } } }
public static void Main(string[] args) { var host = BuildWebHost(args); using (var scope = host.Services.CreateScope()) { var context = scope.ServiceProvider.GetRequiredService<yourDBContext>(); context.Database.Migrate(); } host.Run(); }
دانای اطلاعات ( Information Expert )
بر طبق این اصل میتوان برای واگذاری هر مسئولیت، کلاسی را انتخاب کرد که بیشترین اطلاعات را در مورد انجام آن در اختیار دارد و لذا نیاز کمتری به ایجاد ارتباط با دیگر مولفهها خواهد داشت.
در مثال زیر مشاهده میکنید که کلاس User، اطلاعات کاملی را از عملیات اضافه کردن آیتمی را به لیست خرید و تسویه حساب، ندارد و پیاده سازی این عملیات در این کلاس، نیاز به ایجاد وابستگیهای پیچیدهای دارد.
public class User { public ShoppingCart ShoppingCart { get; set; } public void AddItem(string name) { // User class must know how to create OrderItem var item = new OrderItem() { Name = name }; // User class must know how to add item to shopping cart ShoppingCart.Items.Add(item); } public void CheckOut() { // User class must know logic behind cost and discount calculations: // check for discount // check shipping method // check promotions // calculate total cost of items } } public class OrderItem { public int Id { get; set; } public string Name { get; set; } } public class ShoppingCart { public int Id { get; set; } public List<OrderItem> Items { get; set; } }
بنابراین به جای این طراحی، مسئولیتها را به ShoppingCart منتقل میکنیم:
public class User { public ShoppingCart ShoppingCart { get; set; } } public class OrderItem { public int Id { get; set; } public string Name { get; set; } } public class ShoppingCart { public int Id { get; set; } public List<OrderItem> Items { get; set; } public void AddItem(string name) { // ShoppingCart class know how to create OrderItem var item = new OrderItem() { Name = name }; // ShoppingCart class already know how to add item Items.Add(item); } public void CheckOut() { // ShoppingCart class know logic behind cost and discount calculations: // check for discount // check shipping method // check promotions // calculate total cost of items } }
اتصال ضعیف ( Low Coupling )
با اتصال ضعیف نیز که از ویژگیهای یک طراحی خوب است آشنا هستیم. هر چه تعداد و نوع اتصال بین مولفهها کمتر و ضعیفتر باشد، اعمال تغییرات راحتتر صورت خواهد گرفت. طراحی با اتصال مناسب سه ویژگی را دارد:
- وابستگی بین کلاسها کم است.
- تغییرات در یک کلاس، اثر کمی بر دیگر کلاسها دارد.
- پتانسیل استفادهی مجدد از مؤلفهها بالا است.
چنانچه قبلا هم اشاره کردم، نوشتن نرم افزاری بدون اتصال، ممکن نیست و باید مؤلفهها با هم همکاری کرده و وظایف را انجام دهند. با این حال میتوان نوع اتصالات و تعداد آنرا بهبود بخشید.
چند ریختی ( Polymorphism )
چند ریختی که از ویژگیهای اساسی برنامه نویسی و زبانهای شیء گراست، به منظور بالا بردن قابلیت استفادهی مجدد، استفاده میشود. بر طبق این اصل، مسئولیت تعریف رفتارهای وابسته به نوع کلاس (زیرنوعها در روابط ارث بری) باید به کلاسی واگذار شود که تغییر رفتار در آن اتفاق میافتد. به عبارت دیگر باید به صورت خودکار رفتار را بر اساس نوع کلاس تصحیح کنیم. این روش در مقابل بررسی نوع دادهای برای انجام رفتار مناسب میباشد.
به عنوان مثال اگر کلاسهای چهار ضلعی، مربع، مستطیل و ذوزنقه را داشته باشیم، برای پیاده سازی مساحت در کلاس چهار ضلعی، طول را در عرض ضرب میکنیم. با این حال نوع رفتار مساحت ذوزنقه متفاوت از دیگران است. طبق این اصل، برای اعمال کردن این تغییر، فقط خود کلاس ذوزنقه باید رفتار مربوطه را پیاده سازی کند و هیچ منطق و کدی نباید برای چک کردن نوع کلاس استفاده گردد.
public class ShapeWithoutPolymorphism { public double X { get; set; } public double Y { get; set; } public double Z { get; set; } public double Area(string shapeType) { switch (shapeType) { case "square": return X * Y; case "rectangle": return X * Y; case "trapze": return (X + Z) * Y / 2; default: return 0; } } }
با استفاده از چندریختی، طراحی به این صورت در خواهد آمد:
public abstract class Shape { public double X { get; set; } public double Y { get; set; } public virtual double Area() { return X * Y; } } public class Rectangle : Shape { // No need to override } public class Square : Shape { // No need to override } public class Trapze : Shape { public double Z { get; set; } public override double Area() { return (X + Z) * Y / 2; } }
مصنوع خالص ( Pure Fabrication )
مصنوع خالص کلاسی است که در دامنه مساله وجود ندارد و به منظور کاهش اتصال، افزایش انسجام و افزایش امکان استفاده مجدد کد ایجاد میشود. سرویسها را میتوان از این دسته نامید. کلاسهایی که دقیقا برای کاهش اتصال، افزایش انسجام و افزایش امکان استفاده مجدد کد استفاده میگردند. سرویسها عملیاتی تکراری هستند که توسط مولفههای دیگر استفاده میشوند. اگر سرویسها وجود نداشتند هر مولفهای میبایست عملیات را در درون خود پیاده سازی میکرد که این هم باعث افزایش حجم کد و هم باعث کابوس شدن اعمال تغییرات میشد.
برای تشخیص زمان استفاده از این اصل میتوان گفت زمانیکه رفتاری را نمیدانیم به کدام کلاس واگذار کنیم، کلاس جدیدی را ایجاد میکنیم. در اینجا بجای آنکه به زور مسئولیتی را به کلاس نامربوطی بچسبانیم، آنرا به کلاس جدیدی که فقط رفتاری را دارد، منتقل میکنیم. با اینکار انسجام کلاسها را حفظ کردهایم و هیچ اشکالی ندارد که کلاسی بدون داده بوده و فقط متد داشته باشد. اگر به یاد داشته باشید، در اصل واسطه گری (Indirection ) کلاس جدیدی برای ایجاد ارتباط ساختیم. در حقیقت مسئولیت برقراری ارتباط بین مؤلفهها را به کلاس دیگری واگذار کردیم که چنانچه میبینید، بدون آنکه بدانیم، برای حل مشکل از اصل مصنوع خالص استفاده کردیم.
در مثال زیر این مساله مشهود است:
public class User { public int Id { get; set; } public string UserName { get; set; } public string Password { get; set; } } public class LibraryManagement { public User CurrentUser { get; set; } public void AddBookToLibrary(int bookId) { // check for CurrentUser authority: // not user's responsibility nor LibraryManagement } public void RearrangeBook(int bookId, int shelfId) { // check for CurrentUser authority // not user's responsibility nor LibraryManagement } } public class UserManagement { public User CurrentUser { get; set; } public void AddUser(string name) { // check for CurrentUser authority: // not user's responsibility nor UserManagement } public void ChangeUserRole(int userId, int roleId) { // check for CurrentUser authority // not user's responsibility nor UserManagement } } public class AuthorizationService { public bool IsAuthorized(int userId, int roleId) { // get user roles from data base // return true if user has the authority } }
عملیات بررسی مجوزها باید در کلاس جدیدی به نام AuthorizationService ارائه شود. بدین صورت تمام قسمتها، از این کد بدون وابستگی اضافی میتوانند استفاده کنند.
حفاظت از تاثیر تغییرات ( Protected Variations )
این اصل میگوید که کلاسها باید از تغییرات یکدیگر مصون بمانند. در واقع این اصل غایت یک طراحی خوب است. تمام اصولی را که تا به حال بررسی کردهایم، به منظور دستیابی به چنین رفتاری از طراحی بودهاست. بدین منظور باید از اصول Open/Closed برای واسطها، چند ریختی در توارث و ... استفاده کرد تا از تاثیرات زنجیرهای تغییرات در امان بمانیم.
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
مدل و منبع داده برنامه
namespace jQueryMvcSample06.Models { public class BlogPost { public int Id { set; get; } public string Title { set; get; } public string Body { set; get; } } }
using System.Collections.Generic; using jQueryMvcSample06.Models; namespace jQueryMvcSample06.DataSource { /// <summary> /// منبع داده فرضی جهت سهولت دموی برنامه /// </summary> public static class BlogPostDataSource { private static IList<BlogPost> _cachedItems; static BlogPostDataSource() { _cachedItems = createBlogPostsInMemoryDataSource(); } /// <summary> /// هدف صرفا تهیه یک منبع داده آزمایشی ساده تشکیل شده در حافظه است /// </summary> private static IList<BlogPost> createBlogPostsInMemoryDataSource() { var results = new List<BlogPost>(); for (int i = 1; i < 30; i++) { results.Add(new BlogPost { Id = i, Title = "عنوان " + i, Body = "متن ... متن ... متن " + i}); } return results; } public static IList<BlogPost> LatestBlogPosts { get { return _cachedItems; } } } }
کنترلر برنامه
using System.Web.Mvc; using System.Web.UI; using jQueryMvcSample06.DataSource; using jQueryMvcSample06.Security; namespace jQueryMvcSample06.Controllers { public class HomeController : Controller { [HttpGet] public ActionResult Index() { var postsList = BlogPostDataSource.LatestBlogPosts; return View(postsList); } [AjaxOnly] [HttpPost] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public ActionResult DeleteRow(int? postId) { if (postId == null) return Content(null); //todo: delete post from db return Content("ok"); } } }
data: JSON.stringify({ postId: postId }),
View برنامه
@model IEnumerable<jQueryMvcSample06.Models.BlogPost> @{ ViewBag.Title = "Index"; var postUrl = Url.Action(actionName: "DeleteRow", controllerName: "Home"); } <h2> حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن</h2> <table> <tr> <th> عملیات </th> <th> عنوان </th> </tr> @foreach (var item in Model) { <tr> <td> <span id="row-@item.Id">حذف</span> </td> <td> @item.Title </td> </tr> } </table> @section JavaScript { <script type="text/javascript"> $(function () { $('span[id^="row"]').click(function () { var span = $(this); var postId = span.attr('id').replace('row-', ''); var tableRow = span.parent().parent(); $.ajax({ type: "POST", url: '@postUrl', data: JSON.stringify({ postId: postId }), contentType: "application/json; charset=utf-8", dataType: "json", complete: function (xhr, status) { var data = xhr.responseText; if (xhr.status == 403) { window.location = "/login"; } else if (status === 'error' || !data || data == "nok") { alert('خطایی رخ داده است'); } else { $(tableRow).fadeTo(600, 0, function () { $(tableRow).remove(); }); } } }); }); }); </script> }
در کدهای اسکریپتی صفحه، ابتدا کلیک بر روی کلیه spanهایی که id آنها با row شروع میشود را مونیتور خواهیم کرد:
$('span[id^="row"]').click(function () {
var span = $(this); var postId = span.attr('id').replace('row-', ''); var tableRow = span.parent().parent();
دریافت کدها و پروژه کامل این قسمت
jQueryMvcSample06.zip
انقیاد در لیست List Binding
public static ObservableCollection<Employee> GetEmployees() { var employees = new ObservableCollection<Employee>(); employees.Add(new Employee() { Name = "Mahdi", Title = "Manager" }); employees.Add(new Employee() { Name = "Nima", Title = "Teacher" }); employees.Add(new Employee() { Name = "Rahim", Title = "Assistant" }); employees.Add(new Employee() { Name = "Saeed", Title = "Administrator" }); return employees; }
<ComboBox Name="President" ItemsSource="{Binding}" FontSize="30" Height="50" Width="550"> <ComboBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}"/> <TextBlock Text="{Binding Title}" Margin="5,0,0,0"/> </StackPanel> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox>
private ObservableCollection<Employee> employees; public MainWindow() { InitializeComponent(); employees = Employee.GetEmployees(); DataContext = employees; }
<Grid> <StackPanel Orientation="Horizontal"> <Slider Name="mySlider" Minimum="0" Maximum="100" Width="300"/> <TextBlock Margin="5" Text="{Binding Value,ElementName=mySlider}"/> </StackPanel> </Grid>
public DateTime BornDate { get { return _bornDate; } set { _bornDate = value; OnPropertyChanged(); } }
public static ObservableCollection<Employee> GetEmployees() { var employees = new ObservableCollection<Employee>(); employees.Add(new Employee() { Name = "Mahdi", Title = "Manager", BornDate = DateTime.Parse("2008/8/8") }); employees.Add(new Employee() { Name = "Nima", Title = "Teacher", BornDate = DateTime.Parse("2012/3/14") }); employees.Add(new Employee() { Name = "Rahim", Title = "Assistant", BornDate = DateTime.Parse("2009/11/18") }); employees.Add(new Employee() { Name = "Saeed", Title = "Administrator", BornDate = DateTime.Parse("2014/7/28") }); return employees; }
<ListBox ItemsSource="{Binding}" BorderThickness="1" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="100"/> <TextBlock Text="{Binding Title}" Width="100" Margin="5,0,0,0"/> <TextBlock Text="{Binding BornDate}" Margin="5,0,0,0"/> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> </ListBox>
در زمانیکه در عملیات Data Binding نوع دادهی خصوصیت ما در Source (منبع داده) با نوع دادهی خصوصیت ما در target (کنترل یا View) متفاوت است، به یک مبدل در حین Binding نیاز داریم. این کار را از طریق یک کلاس که اینترفیس IValueConvertor را پیاده سازی کرده است، انجام میدهیم.
public class DateConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { DateTime date = (DateTime)value; PersianCalendar pc = new PersianCalendar(); var persianDate = string.Format ($"{pc.GetYear(date)}/{pc.GetMonth(date)}/{pc.GetDayOfMonth(date)}"); return persianDate; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
xmlns:local="clr-namespace:DataConversion"
<Window.Resources> <local:DateConverter x:Key="MyConverter"/> </Window.Resources>
<TextBlock Text="{Binding BornDate,Converter={StaticResource MyConverter}}" Margin="5,0,0,0"/>
سازماندهی برنامههای Angular توسط ماژولها
اگر به CoreModule تعریف شده دقت کنید، یک چنین تعریفی در آن ذکر شدهاست:
providers: [ UserRepositoryService ]
اکنون فرض کنید سرویسی را در ماژول معمولی غیر Lazy loaded تعریف کردهاید. Angular این سرویس را در Root Injector ثبت میکند. به این ترتیب این سرویس در کل برنامه قابل دسترسی میشود. اما اگر سرویسی را در یک ماژول Lazy loaded تعریف کنید، Angular کار ایجاد یک Injector جداگانه را برای آن ماژول و در داخل آن انجام میدهد. این Injector مجزا است از Root Injector قرار گرفتهی در App Module. سپس این سرویس در این Injector جدید ثبت میشود. به این ترتیب، وهلهی تهیه شدهی از این سرویس، تنها درون این ماژول Lazy loaded قابل دسترسی است. حتی اگر این دو سرویس ثبت شدهی در Root Injector و Injector مخصوص ماژول Lazy loaded از یک کلاس تهیه شوند، باز هم دو وهلهی مختلف از آن ارائه میشوند. برای مثال اگر UserRepositoryService را در ماژول معمولی و ماژول Lazy loaded مورد استفاده قرار دهیم، دو وهلهی مجزای از آنها تشکیل خواهد شد که دیگر با هم همگام نبوده و کار ردیابی اطلاعات کاربر جاری سیستم را با مشکل مواجه میکنند.
ایجاد وهلهی دوم از یک سرویس، تنها منحصر است به ماژولهای Lazy loaded. اما اگر این سرویس در ماژولهای معمولی مختلفی مورد استفاده قرار گیرد، Angular تنها یک وهله از آنرا ایجاد خواهد کرد. به همین جهت است که در اینجا CoreModule را تعریف کردیم. این ماژول مکانی است که قرار است سرویسهای اشتراکی در کل برنامه در آن قرار گیرند. چون این ماژول هیچگاه Lazy loaded نخواهد شد، هرگاه سرویسی را توسط آن ارائه دهیم، در Root Injector مربوط به App Module ثبت میشود. به این ترتیب تبدیل به یک Singleton Service قابل دسترسی در کل برنامه میشود.
باید دقت داشت که از لحاظ فنی، تفاوتی بین Core Module و یک ماژول معمولی غیر Lazy loaded نیست و بیشتر هدف از آن نظم بخشیدن به تعریف سرویسهایی است که قرار است در طول عمر برنامه و در تمام انواع ماژولهای آن، توسط یک وهله قابل دسترسی شوند.
بنابراین تفاوتی نمیکند که یک سرویس را درون Core Module تعریف کنید و یا یک ماژول معمولی. این سرویس همواره در Root Injecror ثبت خواهد شد؛ مگر اینکه آن ماژول Lazy loaded باشد. در این حالت اگر سرویسی تنها قرار است در یک ماژول خاص استفاده شود، بهتر است آنرا جهت مدیریت بهتر برنامه، درون همان پوشه تعریف کرد. هرچند از لحاظ فنی، این سرویسها نهایتا در Root Injector مربوط به App Module ثبت و ارائه میشوند (از این لحاظ فرقی بین یک Core Module و Feature Modules نیست).
SQL Server CE برای اولین بار جهت استفاده در SmartPhones طراحی شد؛ جزو خانوادهی Embedded databases قرار میگیرد و این مزایا را دارد:
- نیازی به نصب ندارد و از چند DLL تشکیل شده است (برای مثال جهت استفاده در کارهای تک کاربرهی قابل حمل ایدهآل است).
- رایگان است (جهت استفاده در کارهای تجاری و غیرتجاری).
- حجم کمی دارد (جمعا کمتر از دو مگابایت).
- پروایدر ADO.NET آن موجود است (توسط فضای نام System.Data.SqlServerCe که به کمک اسمبلی System.Data.SqlServerCe.dll قرار گرفته در مسیر C:\Program Files\Microsoft SQL Server Compact Edition\v3.5\Desktop ارائه میشود).
- با کمک ORM هایی مانند Entity framework و یا NHibernate نیز میتوان با آن کار کرد.
- نسخهی 4 نهایی آن که قرار است در زمان ارائهی SP1 مربوط به VS.NET 2010 ارائه شود، جهت استفاده در برنامههای ASP.NET (برنامههای چند کاربره) ایی که تعداد کاربر کمی دارند، بهینه سازی شده و این مورد یک مزیت مهم نسبت به SQLite است که اساسا با تردهای همزمان جهت کار با بانک اطلاعاتی مشکل دارد.
- امکان گذاشتن کلمهی عبور بر روی بانک اطلاعاتی آن وجود دارد که سبب رمزنگاری خودکار آن نیز خواهد شد (این مورد به صورت پیش فرض در SQLite پیش بینی نشده و جزو مواردی که است که باید برای آن هزینه کرد). الگوریتم رمزنگاری آن به صورت رسمی معرفی نشده، ولی به احتمال زیاد AES میباشد.
- از ADO.NET Sync Framework پشتیبانی میکند.
ملاحظات:
- به آن میتوان به صورت نسخهی تعدیل شدهی SQL Server 2000 با تواناییهای کاهش یافته نگاه کرد. در آن خبری از رویههای ذخیره شده، View ها ، Full text search ، CLR Procs، CLR Triggers و غیره نیست (سطح توقع را باید در حد همان 2 مگابایت پایین نگه داشت!). لیست کامل : (+)
- Management studio مربوط به SQL Server 2005 به هیچ عنوان از آن پشتیبانی نمیکند و تنها نسخهی 2008 است که نگارش 3 و نیم آنرا پشتیبانی میکند آن هم نه با تواناییهایی که جهت کار با SQL Server اصلی وجود دارد. مثلا امکان rename یک فیلد را ندارد و باید برای اینکار کوئری نوشت. خوشبختانه یک سری پروژهی رایگان در سایت CodePlex این نقایص را پوشش دادهاند؛ برای مثال : ExportSqlCe
- از آنجائیکه DLL های SQL CE از نوع Native هستند، باید دقت داشت که حین استفاده از آنها در دات نت فریم ورک اگر platform target قسمت build برنامه بر روی ALL CPU تنظیم شده باشد، برنامه به احتمال زیاد در سیستمهای 64 بیتی کرش خواهد کرد (اگر در حین توسعه برنامه از DLLهای بومی 32 بیتی آن استفاده شده باشد). بنابراین نیاز است DLL های 64 بیتی را به صورت جداگانه جهت سیستمهای 64 بیتی ارائه داد. اطلاعات بیشتر: (+) و (+) و (+)
- Entity framework یک سری از قابلیتهای این بانک اطلاعاتی را پشتیبانی نمیکند. برای مثال اگر یک primary key از نوع identity را تعریف کردید، برنامه کار نخواهد کرد! لیست مواردی را که پشتیبانی نمیشوند، در این آدرس میتوان مشاهده کرد.
و اخبار مرتبط با SQL CE را در این بلاگ میتوانید دنبال کنید.
در مطالب قبلی به اختصار در مورد dynamic management views که از SQL server 2005 به بعد ارائه شدهاند مثالهایی کاربردی ارائه گشتند. یکی دیگر از قابلیتهای فوق العاده مهم این DMV ها، پیشنهاد ایجاد ایندکس بر روی جداول است. این پیشنهادات بر اساس آمارهای جمع آوری شده توسط موتور بهینه ساز اجرای کوئریها در اس کیوال سرور به شما ارائه خواهند شد. برای مثال کوئری زیر را در management studio اجر نمائید:
USE master;
SELECT d.database_id,
d.object_id,
d.index_handle,
d.equality_columns,
d.inequality_columns,
d.included_columns,
d.statement AS fully_qualified_object,
gs.*
FROM sys.dm_db_missing_index_groups g
JOIN sys.dm_db_missing_index_group_stats gs
ON gs.group_handle = g.index_group_handle
JOIN sys.dm_db_missing_index_details d
ON g.index_handle = d.index_handle
خروجی حاصل لیستی است که بر اساس تفاسیر موتور بهینه ساز اجرای کوئریها بدست آمده است. equality_columns بر اساس حالتهایی مانند table.column = constant_value پیش بینی شدهاست. inequality_columns بر اساس حالتهایی مانند table.column > constant_value و included_columns برای حالتهایی است که میخواهیم ایندکس ایجاد شده محدودیت اندازه 900 بایت را نداشته باشد، یا نوع دادهای مورد استفاده برای مثال nvrachar max و امثال آن باشد (text و ntext مجاز نیست) و مواردی از این دست.
fully_qualified_object هم مشخص میکند که این ایندکس دقیقا باید بر روی چه دیتابیس و جدولی ایجاد شود.
تذکر: این آمارهای جمعآوری شده پس از هر بار ریاستارت سرور، صفر خواهند شد.
اکنون این سؤال مطرح میشود که چگونه از این اطلاعات استفاده کنیم؟
دقیقا بر اساس EQUALITY_COLUMNS ، INEQUALITY_COLUMNS و INCLUDED_COLUMNS گزارش فوق، میتوان به صورت زیر عمل کرد:
CREATE NONCLUSTERED INDEX <unique index name>
ON <FULL_TABLE_NAME> (<EQUALITY_COLUMNS>,<INEQUALITY_COLUMNS>) -- exclude INEQUALITY_COLUMNS if NULL
INCLUDE (<INCLUDED_COLUMNS>); -- exclude INCLUDED_COLUMNS if NULL
SELECT mig.index_group_handle,
mid.index_handle,
migs.avg_total_user_cost AS AvgTotalUserCostThatCouldbeReduced,
migs.avg_user_impact AS AvgPercentageBenefit,
'CREATE INDEX missing_index_' + CONVERT (varchar, mig.index_group_handle)
+ '_' + CONVERT (varchar, mid.index_handle)
+ ' ON ' + mid.statement
+ ' (' + ISNULL (mid.equality_columns,'')
+ CASE
WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns
IS NOT NULL THEN ','
ELSE ''
END
+ ISNULL (mid.inequality_columns, '')
+ ')'
+ ISNULL (' INCLUDE (' + mid.included_columns + ')', '') AS
create_index_statement
FROM sys.dm_db_missing_index_groups mig
INNER JOIN sys.dm_db_missing_index_group_stats migs ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
مزایای ایجاد ایندکسهای صحیح بر اساس نیازهای واقعی کاری:
- سریعتر شدن اجرای کوئریهای جستجو در تعداد رکوردهای بالا
- مرتب سازی سریعتر نتایج (sorting)
- کوئریهایی که بر اساس عبارت GROUP BY ایجاد شدهاند، سریعتر اجرا خواهند شد
تراکنشها در RavenDB
ACID چیست؟
ACID از 4 قاعده تشکیل شده است (Atomic, Consistent, Isolated, and Durable) که با کنار هم قرار دادن آنها یک تراکنش مفهوم پیدا میکند:
الف) Atomic: به معنای همه یا هیچ
اگر تراکنشی از چندین تغییر تشکیل میشود، همهی آنها باید با موفقیت انجام شوند، یا اینکه هیچکدام از تغییرات نباید فرصت اعمال نهایی را بیابند.
برای مثال انتقال مبلغ X را از یک حساب، به حسابی دیگر درنظر بگیرید. در این حالت X ریال از حساب شخص کسر و X ریال به حساب شخص دیگری واریز خواهد شد. اگر موجودی حساب شخص، دارای X ریال نباشد، نباید مبلغی از این حساب کسر شود. مرحله اول شکست خورده است؛ بنابراین کل عملیات لغو میشود. همچنین اگر حساب دریافت کننده بسته شده باشد نیز نباید مبلغی از حساب اول کسر گردد و در این حالت نیز کل تراکنش باید برگشت بخورد.
ب) Consistent یا یکپارچه
در اینجا consistency علاوه بر اعمال قیود، به معنای اطلاعاتی است که بلافاصله پس از پایان تراکنشی از سیستم قابل دریافت و خواندن است.
ج) Isolated: محصور شده
اگر چندین تراکنش در یک زمان با هم در حال اجرا باشند، نتیجه نهایی با حالتی که تراکنشها یکی پس از دیگری اجرا میشوند باید یکی باشد.
د) Durable: ماندگار
اگر سیستم پایان تراکنشی را اعلام میکند، این مورد به معنای 100 درصد نوشته شدن اطلاعات در سخت دیسک باید باشد.
مراحل چهارگانه ACID در RavenDB به چه نحوی وجود دارند؟
RavebDB از هر دو نوع تراکنشهای implicit و explicit پشتیبانی میکند. Implicit به این معنا است که در حین استفاده معمول از RavenDB (و بدون انجام تنظیمات خاصی)، به صورت خودکار مفهوم تراکنشها وجود داشته و اعمال میشوند. برای نمونه به متد ذیل توجه نمائید:
public void TransferMoney(string fromAccountNumber, string toAccountNumber, decimal amount) { using(var session = Store.OpenSession()) { session.Advanced.UseOptimisticConcurrency = true; var fromAccount = session.Load<Account>("Accounts/" + fromAccountNumber); var toAccount = session.Load<Account>("Accounts/" + toAccountNumber); fromAccount.Balance -= amount; toAccount.Balance += amount; session.SaveChanges(); } }
- از document store ایی که پیشتر تدارک دیده شده، جهت بازکردن یک سشن استفاده شده است.
- به سشن صراحتا عنوان شده است که از Optimistic Concurrency استفاده کند. در این حالت RavenDB اطمینان حاصل میکند که اکانتهای بارگذاری شده توسط متدهای Load، تا زمان فراخوانی SaveChanges تغییر پیدا نکردهاند (و در غیراینصورت یک استثناء را صادر میکند).
- دو اکانت بر اساس Id آنها از بانک اطلاعاتی واکشی میشوند.
- موجودی یکی تقلیل یافته و موجودی دیگر، افزایش مییابد.
- متد SaveChanges بر روی شیء سشن فراخوانی شده است. تا زمانیکه این متد فراخوانی نشده است، کلیه تغییرات در حافظه نگهداری میشوند و به سرور ارسال نخواهند شد. فراخوانی آن سبب کامل شدن تراکنش و ارسال اطلاعات به سرور میگردد.
بنابراین شیء سشن بیانگر یک atomic transaction ماندگار و محصور شده است (سه جزء ACID تاکنون محقق شدهاند). محصور شده بودن آن به این معنا است که:
الف) هر تغییری که در سشن اعمال میشود، تا پیش از فراخوانی متد SaveChanges از دید سایر تراکنشها مخفی است.
ب) اگر دو تراکنش همزمان رخ دهند، تغییرات هیچکدام بر روی دیگری اثری ندارد.
اما Consistency یا یکپارچگی در RavenDB بستگی دارد به نحوهی خواندن اطلاعات و این مورد با دنیای رابطهای اندکی متفاوت است که در ادامه جزئیات آنرا بیشتر بررسی خواهیم کرد.
عاقبت یک دست شدن یا eventual consistency
درک Consistency مفهوم ACID در RavenDB بسیار مهم است و عدم آشنایی با نحوه عملکرد آن میتواند مشکلساز شود. در دنیای بانکهای اطلاعاتی رابطهای، برنامه نویسها به «immediate consistency» عادت دارند (یکپارچگی آنی). به این معنا که هرگونه تغییری در بانک اطلاعاتی، پس از پایان تراکنش، بلافاصله در اختیار کلیه خوانندگان سیستم قرار میگیرد. در RavenDB و خصوصا دنیای NoSQL، این یکپارچگی آنی دنیای رابطهای، به «eventual consistency» تبدیل میشود (عاقبت یکدست شدن). عاقبت یک دست شدن در RavenDB به این معنا است که اگر تغییری به یک سند اعمال گردیده و ذخیره شود؛ کوئری انجام شده بر روی این اطلاعات تغییر یافته ممکن است «stale data» باز گرداند. واژه stale در RavenDB به این معنا است که هنوز اطلاعاتی در دیتابیس موجود هستند که جهت تکمیل ایندکسها پردازش نشدهاند. به این مورد در قسمت بررسی ایندکسها در RavenDB اشاره شد.
در RavenDB یک سری تردهای پشت صحنه، مدام مشغول به کار هستند و بدون کند کردن عملیات سیستم، کار ایندکس کردن اطلاعات را انجام میدهند. هر زمانیکه اطلاعاتی را ذخیره میکنیم، بلافاصله این تردها تغییرات را تشخیص داده و ایندکسها را به روز رسانی میکنند. همچنین باید درنظر داشت که RavenDB جزو معدود بانکهای اطلاعاتی است که خودش را بر اساس نحوه استفاده شما ایندکس میکند! (نمونهای از آنرا در قسمت ایندکسهای پویای حاصل از کوئریهای LINQ پیشتر مشاهده کردهاید)
نکته مهم
در RavenDB اگر از کوئریهای LINQ استفاده کنیم، ممکن است به علت اینکه هنوز تردهای پشت صحنهی ایندکس سازی اطلاعات، کارشان تمام نشده است، تمام اطلاعات یا آخرین اطلاعات را دریافت نکنیم (که به آن stale data گفته میشود). هر آنچه که ایندکس شده است دریافت میگردد (مفهوم عاقبت یک دست شدن ایندکسها). اما اگر نیاز به یکپارچگی آنی داشتیم، متد Load یک سشن، مستقیما به بانک اطلاعاتی مراجعه میکند و اطلاعات بازگشت داده شده توسط آن هیچگاه احتمال stale بودن را ندارند.
بنابراین برای نمایش اطلاعات یا گزارشگیری، از کوئریهای LINQ استفاده کنید. RavenDB خودش را بر اساس کوئری شما ایندکس خواهد کرد و نهایتا به کوئریهایی فوق العاده سریعی در طول کارکرد سیستم خواهیم رسید. اما در صفحه ویرایش اطلاعات بهتر است از متد Load استفاده گردد تا نیاز به مفهوم immediate consistency یا یکپارچگی آنی برآورده شود.
تنظیمات خاص کار با ایندکس سازها برای انتظار جهت اتمام کار آنها
عنوان شد که اگر ایندکس سازهای پشت صحنه هنوز کارشان تمام نشده است، در حین کوئری گرفتن، هر آنچه که ایندکس شده بازگشت داده میشود.
در اینجا میتوان به RavenDB گفت که تا چه زمانی میتواند یک کوئری را جهت دریافت اطلاعات نهایی به تاخیر بیندازد. برای اینکار باید اندکی کوئریهای LINQ آنرا سفارشی سازی کنیم:
RavenQueryStatistics stats; var results = session.Query<Product>() .Statistics(out stats) .Where(x => x.Price > 10) .ToArray(); if (stats.IsStale) { // Results are known to be stale }
همچنین زمان انتظار تا پایان کار ایندکس ساز را نیز توسط متد Customize به نحو ذیل میتوان تنظیم کرد:
RavenQueryStatistics stats; var results = session.Query<Product>() .Statistics(out stats) .Where(x => x.Price > 10) .Customize(x => x.WaitForNonStaleResults(TimeSpan.FromSeconds(5))) .ToArray();
documentStore.Conventions.DefaultQueryingConsistency = ConsistencyOptions.QueryYourWrites;
while (documentStore.DatabaseCommands.GetStatistics().StaleIndexes.Length != 0) { Thread.Sleep(10); }
مقابله با تداخلات همزمانی
با تنظیم session.Advanced.UseOptimisticConcurrency = true، اگر سندی که در حال ویرایش است، در این حین توسط کاربر دیگری تغییر کرده باشد، استثنای ConcurrencyException صادر خواهد شد. همچنین این استثناء در صورتیکه شخصی قصد بازنویسی سند موجودی را داشته باشد نیز صادر خواهد شد (شخصی بخواهد سندی را با ID سند موجودی ذخیره کند). اگر از optimistic concurrency استفاده نشود، آخرین ترد نویسنده یا به روز کننده اطلاعات، برنده خواهد شد و اطلاعات نهایی موجود در بانک اطلاعاتی متعلق به او و حاصل بازنویسی آن ترد است.
optimistic concurrency به زبان ساده به معنای به خاطر سپردن شماره نگارش یک سند است، زمانیکه آنرا بارگذاری میکنیم و سپس ارسال آن به سرور، زمانیکه قصد ذخیره آنرا داریم. در SQL Server اینکار توسط RowVersion انجام میشود. در بانکهای اطلاعاتی سندگرا چون تمایل به استفاده از HTTP در آنها زیاد است (مانند RavenDB) از مکانیزمی به نام E-Tag برای این منظور کمک گرفته میشود. هر زمانیکه تغییری به یک سند اعمال میشود، E-Tag آن به صورت خودکار افزایش خواهد یافت.
برای مثال فرض کنید کاربری سندی را با E-Tag مساوی 2 بارگذاری کرده است. قبل از اینکه این کاربر در صفحه ویرایش اطلاعات کارش با این سند خاتمه یابد، کاربر دیگری در شبکه، این سند را ویرایش کرده است و اکنون E-Tag آن مثلا مساوی 6 است. در این زمان اگر کاربر یک سعی به ذخیره سازی اطلاعات نماید، چون E-Tag سند او با E-Tag سند موجود در سرور دیگر یکی نیست، با استثنای ConcurrencyException متوقف خواهد شد.
مشکل! در برنامههای بدون حالت وب، چون پس از نمایش صفحه ویرایش اطلاعات، سشن RavenDB نیز بلافاصله Dispose خواهد شد، این E-Tag را از دست خواهیم داد. همچنین باید دقت داشت که سشن RavenDB به هیچ عنوان نباید در طول عمر یک برنامه باز نگهداشته شود و برای طول عمری کوتاه طراحی شده است. راه حلی که برای آن درنظر گرفته شده است، ذخیره سازی این E-Tag در بار اول دریافت آن از سشن میباشد. برای این منظور تنها کافی است خاصیتی را به نام Etag با ویژگی JsonIgnore (که سبب عدم ذخیره سازی آن در بانک اطلاعاتی خواهد شد) تعریف کنیم:
public class Person { public string Id { get; set; } [JsonIgnore] public Guid? Etag { get; set; } public string Name { get; set; } }
public Person Get(string id) { var person = session.Load<Person>(id); person.Etag = session.Advanced.GetEtagFor(person); return person; }
public void Update(Person person) { session.Advanced.UseOptimisticConcurrency = true; session.Store(person, person.Etag, person.Id); session.SaveChanges(); person.Etag = session.Advanced.GetEtagFor(person); }
تراکنشهای صریح
همانطور که عنوان شد، به صورت ضمنی کلیه سشنها، یک واحد کار را تشکیل داده و با پایان آنها، تراکنش خاتمه مییابد. اگر به هر علتی قصد تغییر این رفتار ضمنی پیش فرض را دارید، امکان تعریف صریح تراکنشهای نیز وجود دارد:
using (var transaction = new TransactionScope()) { using (var session1 = store.OpenSession()) { session1.Store(new Account()); session1.SaveChanges(); } using (var session2 = store.OpenSession()) { session2.Store(new Account()); session2.SaveChanges(); } transaction.Complete(); }