@model MvcApplication1.Models.Com <script> var onSuccess = function (result) { $.validator.unobtrusive.parse("#result"); }; </script> <div id="result" > @using (Ajax.BeginForm( "SendCommentAction", "Home", null, new AjaxOptions { OnSuccess = "onSuccess", UpdateTargetId = "result" }, null) ) { @Html.TextBoxFor(x=>x.Contect) <br/> @Html.ValidationMessageFor(x=>x.Contect) <br/> @Html.ValidationSummary() <input type="submit" value="Save" /> } </div>
Delegate در سی شارپ
• Join
• GroupJoin
• Zip
عملگر Join
این عملگر همانند inner join در SQL، دو مجموعه را بر اساس کلیدهای مرتبط که از طریق پارامترها به آن ارسال میشوند، با یکدیگر ترکیب میکند.
در عملیات Join، یک توالی ورودی که به آن توالی خارجی (Outer Sequence) گفته میشود با یک توالی دیگر که به آن توالی داخلی (Inner Sequence) میگوییم، بر اساس کلیدهای مشخص شده، ترکیب شده و یک توالی خروجی تولید میشود.
بررسی پارامترهای عملگر Join:
public static IEnumerable<TResult> Join<TOuter,TInner,TKey,TResult> (this IEnumerable<TOuter> outer, IEnumerable<TInner> inner, Func<TOuter,TKey> outerKeySelector, Func<TInner,TKey> innerKeySelector, Func<TOuter,TInner,TResult> resultSelector)
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Tinner,Tresult> resultSelector : یک عبارت Lambda است که ظاهر عناصر خروجی را مشخص میکند.
نکته : بطور کلی T در پارامترهای بالا معرف Generic Type Parameter میباشد؛ T==>Type (هر نوع دادهای که ما مشخص کنیم).
نکته : عملگر Join یک امضاء دیگر نیز دارد که اجازه مشخص کردن IEqualityComparer را میدهد.
کد زیر استفاده از عملگر Join را نشان میدهد. توجه داشته باشید که اعلان صریح نوع دادهها در عبارات Lambda نوشته شده، فقط برای روشنتر شدن فرآیند عملیات میباشد.
تعریف دو آرایه از کلاسهای Recipe و Review:
Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes // recipes توالی خارجی .Join(reviews, // reviewsتوالی داخلی (Recipe outerKey) => outerKey.Id, // کلید انخاب شده از توالی خارجی (Review innerKey) => innerKey.RecipeId, // کلید انتخاب شده از توالی داخلی // نحوه قالب بندی خروجی (recipe, review) => recipe.Name + " - " + review.ReviewText); foreach (string item in query) { Console.WriteLine(item); }
Mashed Potato - Tasty! Mashed Potato - Not nice :( Mashed Potato - Pretty good Crispy Duck - Too hard Crispy Duck - Loved it
var query = recipes.Join (reviews, outerKey => outerKey.Id, innerKey => innerKey.RecipeId, (recipe, review) => recipe.Name + " - " + review.ReviewText);
پیاده سازی توسط عبارتهای جستجو
کلمه کلیدی Join، در زمان استفاده از روش عبارتهای پرس و جو، مورد استفاده قرار گرفت. دستور Join در قسمت چهارم از این سری آموزشی بطور کامل بررسی شده است. کد زیر نحوه اجرای دستور Join را به روش عبارتهای پرس و جو، نشان میدهد.
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId select new //انواع بی نام { RecipeName = recipe.Name, RecipeReview = review.ReviewText }; foreach (var item in query) { Console.WriteLine(item.RecipeName + " - " + item.RecipeReview); }
عملگر GroupJoin
نحوه عملکرد عملگر GroupJoin، شبیه عملگر Join میباشد؛ با این تفاوت که خروجی حاصل از دستور GroupJoin، یک ساختار سلسله مراتبی میباشد. توالی خروجی، مجموعهای از گروهها میباشد که هر گروه، تشکیل شدهاست از عناصر توالی درونی.
بررسی پارامترهای عملگر GroupJoin
• <Inner IEnumerable<TInner : نشان دهنده توالی داخلی
• Func<Touter,Tkey> outerKeySelector : عنصر کلید، در توالی خارجی
• Func<Tinner,Tkey> innerKeySelector : عنصر کلید، در توالی داخلی
• Func<Touter,Ienumerable<Tinner>,Tresult> resultSelector : قالب بندی گروههای تولید شده خروجی را مشخص میکند
کد زیر استفاده از عملگر GroupJoin را نشان میدهد :
// outer sequence Recipe[] recipes = { new Recipe {Id = 1, Name = "Mashed Potato"}, new Recipe {Id = 2, Name = "Crispy Duck"}, new Recipe {Id = 3, Name = "Sachertorte"} }; // inner sequence Review[] reviews = { new Review {RecipeId = 1, ReviewText = "Tasty!"}, new Review {RecipeId = 1, ReviewText = "Not nice :("}, new Review {RecipeId = 1, ReviewText = "Pretty good"}, new Review {RecipeId = 2, ReviewText = "Too hard"}, new Review {RecipeId = 2, ReviewText = "Loved it"} }; var query = recipes .GroupJoin( reviews, (Recipe outerKey) => outerKey.Id,//outer key (Review innerKey) => innerKey.RecipeId,//inner key (Recipe recipe, IEnumerable<Review> rev )=>تعریف ساختار گروهها new { RecipeName = recipe.Name, Reviews = rev } ); foreach (var item in query) { Console.WriteLine($"Reviews for {item.RecipeName}"); foreach (var review in item.Reviews) { Console.WriteLine($" - {review.ReviewText}"); } }
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
پیاده سازی توسط عبارتهای جستجو
var query = from recipe in recipes join review in reviews on recipe.Id equals review.RecipeId into reviewGroup select new //انواع بی نام { RecipeName = recipe.Name, Reviews = reviewGroup//کلیه بازخوردها مرتبط با یک دستور غذایی };
Reviews for Mashed Potato - Tasty! - Not nice :( - Pretty good Reviews for Crispy Duck - Too hard - Loved it Reviews for Sachertorte
عملگر Zip
عملگر Zip، رفتاری متفاوت نسبت به عملگر GroupJoin و Join دارد و هیچ آیتمی را به عنوان کلید، از دو توالی دریافت نمیکند. عملگر Zip همه عناصر دو توالی را یک به یک، به ترتیب کنار هم قرار میدهد. مثل زیپ در دنیای واقعی که لبههای دو طرف زیپ را به هم میرساند.
public class Ingredient { public string Name { get; set; } public int Calories { get; set; } } string[] names = { "Flour", "Butter", "Sugar" }; int[] calories = { 100, 400, 500 }; IEnumerable<Ingredient> ingredients = names.Zip(calories, (name, calorie) => new Ingredient { Name = name, Calories = calorie }); foreach (var item in ingredients) { Console.WriteLine($"{item.Name} has {item.Calories} calories"); }
Flour has 100 calories Butter has 400 calories Sugar has 500 calories
پیاده سازی توسط عبارتهای جستجو
معادل عملگر Zip، کلمه کلیدی در عبارتهای جستجو وجود ندارد. ترکیب دو روش میتواند خروجی دلخواه را تولید کند.
Install-Package iTextSharp
شروع کار با iTextSharp
معمولا برای کار با iText یک سری روال تکراری از قبیل انتخاب نام فایل نهایی، تعریف فونت، سایز کاغذ، حاشیه بندی و ... را باید طی کنید که کدهای آن را در ذیل مشاهده میکنید:
var fileStream = new FileStream("card.pdf", FileMode.Create, FileAccess.Write, FileShare.None); var docFont = GetFont(); var pageSize = PageSize.A6.Rotate(); // سایز کارت را اینجا باید مشخص کرد var doc = new Document(pageSize); doc.SetMargins(18f, 18f, 15f, 2f); var pdfWriter = PdfWriter.GetInstance(doc, fileStream); doc.Open();
برای درج عکس به صورت شفاف در پس زمینه کارت باید از کد زیر استفاده کرد:
// درج لوگوی مسابقات به صورت شفاف در پس زمینه var canvas = pdfWriter.DirectContentUnder; var logoImg = Image.GetInstance(competitionImagePath); logoImg.SetAbsolutePosition(0, 0); logoImg.ScaleAbsolute(pageSize); var graphicsState = new PdfGState { FillOpacity = 0.2F }; canvas.SetGState(graphicsState); canvas.AddImage(logoImg);
چیدمان و طرح بندی بندی عناصر در iTextSharp
برای طراحی کارت یا کلا کار طراحی، باید با نحوهی قرار دادن و طرح بندی عناصر مثل تصاویر و نوشتهها و ابزارهای مورد نیاز برای این کار، آشنا شوید. خوشبختانه در iText برای این کار ابزارهای خوبی وجود دارد.
حتما با تگ Table در HTML آشنایی دارید. در سالهای دور، حتی کل صفحهی وب را به وسیلهی Table ساختار دهی میکردند. در iTextSharp نیز کلاسی به نام PdfPTable در دسترس است که میتوان از آن به عنوان قالبی برای قرار دادن عناصر، در صفحه استفاده کرد. این Table همانند هر جدولی دارای یک سری سطر و ستون است که میتوانیم عناصر مورد نظرمان مثل تصویر و نوشته و... را در آن قرار دهیم.
// جدولی که برای چیدمان عناصر ارم دانشگاه و عنوان و عکس شخص استفاده میشود var topTable = new PdfPTable(3) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, }; var universityLogoImage = Image.GetInstance(universityLogoPath); universityLogoImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(universityLogoImage) { HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); topTable.AddCell(new PdfPCell(new Phrase("کارت مسابقات دانشگاه آزاد اسلامی", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_CENTER, Border = 0, }); var userImage = Image.GetInstance(userModel.ImagePath); userImage.Border = Rectangle.TOP_BORDER | Rectangle.RIGHT_BORDER | Rectangle.BOTTOM_BORDER | Rectangle.LEFT_BORDER; userImage.BorderWidth = 1f; userImage.BorderColor = new BaseColor(204, 204, 204); // gray color userImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(userImage) { HorizontalAlignment = 2, Border = 0 }); int[] topTableColumnsWidth = { 10, 25, 10 }; topTable.SetWidths(topTableColumnsWidth); doc.Add(topTable);
public class UserModel { public string FirstName { get; set; } public string LastName { get; set; } public string StudentNumber { get; set; } public string NationalCode { get; set; } public string UniversityName { get; set; } public string ImagePath { get; set; } }
using System; using System.IO; using iTextSharp.text; using iTextSharp.text.pdf; using Font = iTextSharp.text.Font; using Image = iTextSharp.text.Image; using Rectangle = iTextSharp.text.Rectangle; namespace ITextSharpCardSample { public class CardReport { public static void Generate(UserModel userModel, string competitionImagePath, string universityLogoPath) { var fileStream = new FileStream("card.pdf", FileMode.Create, FileAccess.Write, FileShare.None); var docFont = GetFont(); var pageSize = PageSize.A6.Rotate(); // سایز کارت را اینجا باید مشخص کرد var doc = new Document(pageSize); doc.SetMargins(18f, 18f, 15f, 2f); var pdfWriter = PdfWriter.GetInstance(doc, fileStream); doc.Open(); // درج لوگوی مسابقات به صورت شفاف در پس زمینه var canvas = pdfWriter.DirectContentUnder; var logoImg = Image.GetInstance(competitionImagePath); logoImg.SetAbsolutePosition(0, 0); logoImg.ScaleAbsolute(pageSize); var graphicsState = new PdfGState { FillOpacity = 0.2F }; canvas.SetGState(graphicsState); canvas.AddImage(logoImg); // جدولی که برای چیدمان عناصر ارم دانشگاه و عنوان و عکس شخص استفاده میشود var topTable = new PdfPTable(3) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, }; var universityLogoImage = Image.GetInstance(universityLogoPath); universityLogoImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(universityLogoImage) { HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); topTable.AddCell(new PdfPCell(new Phrase("کارت مسابقات دانشگاه آزاد اسلامی", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_CENTER, Border = 0, }); var userImage = Image.GetInstance(userModel.ImagePath); userImage.Border = Rectangle.TOP_BORDER | Rectangle.RIGHT_BORDER | Rectangle.BOTTOM_BORDER | Rectangle.LEFT_BORDER; userImage.BorderWidth = 1f; userImage.BorderColor = new BaseColor(204, 204, 204); // gray color userImage.ScaleAbsolute(70, 100); topTable.AddCell(new PdfPCell(userImage) { HorizontalAlignment = 2, Border = 0 }); int[] topTableColumnsWidth = { 10, 25, 10 }; topTable.SetWidths(topTableColumnsWidth); doc.Add(topTable); // جدول مشخصات شرکت کننده مثل نام و نام خانوادگی var infoTable = new PdfPTable(4) { WidthPercentage = 100, RunDirection = PdfWriter.RUN_DIRECTION_RTL, ExtendLastRow = false, SpacingBefore = 15, }; infoTable.AddCell(new PdfPCell(new Phrase("نام:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0, PaddingBottom = 15 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.FirstName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("نام خانوادگی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.LastName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("شماره\nدانشجویی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0, PaddingBottom = 15 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.StudentNumber, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("کد ملی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.NationalCode, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("واحد دانشگاهی:", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase(userModel.UniversityName, docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); // دو سلول بعدی صرفا جهت تکمیل شدن یک ردیف است تا عملکرد صحیح خود را داشته باشد infoTable.AddCell(new PdfPCell(new Phrase("", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); infoTable.AddCell(new PdfPCell(new Phrase("", docFont)) { RunDirection = PdfWriter.RUN_DIRECTION_RTL, HorizontalAlignment = Element.ALIGN_LEFT, Border = 0 }); int[] infoTableColumnsWidth = { 20, 15, 20, 15 }; infoTable.SetWidths(infoTableColumnsWidth); doc.Add(infoTable); doc.Close(); } private static Font GetFont() { const string fontName = "Iranian Sans"; if (FontFactory.IsRegistered(fontName)) return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); var fontPath = "Fonts/irsans.ttf"; // مسیر فونت FontFactory.Register(fontPath); return FontFactory.GetFont(fontName, BaseFont.IDENTITY_H, BaseFont.EMBEDDED); } } }
class Program { static void Main(string[] args) { var userModel = new UserModel { FirstName = "علی", LastName = "احمدی", NationalCode = "1234567890", StudentNumber = "23242342", UniversityName = "آزاد", ImagePath = "Images/avatar.jpg" }; CardReport.Generate(userModel, "Images/competition_logo.jpg", "Images/university_logo.png"); System.Diagnostics.Process.Start("card.pdf"); } }
// <Name>Aggregate Type Complexity</Name> from t in Application.Types let aggregateTypeCC = t.MethodsAndContructors.Sum(m => m.CyclomaticComplexity / 10) let rawCC = t.MethodsAndContructors.Sum(m => m.CyclomaticComplexity) // optional optimization if not comparing rawCC // where aggregateCC >= 10 orderby aggregateTypeCC descending select new { t, aggregateTypeCC, rawCC }
چند راهنمایی در مورد گروه بندی
آموزش Linq
2 - آموزش Linq - بخش دوم
3 - آموزش Linq - بخش سوم
4 - آموزش Linq - بخش چهارم
5 - آموزش Linq - بخش پنجم
6 - آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت اول
7 - آموزش Linq - بخش ششم : عملگرهای پرس و جو - قسمت دوم
8 - آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت سوم
9 - آموزش Linq - بخش ششم : عملگرهای پرس و جو قسمت چهارم
10- آموزش Linq - بخش ششم: عملگرهای پرس و جو قسمت پنجم (پایانی)
Roslyn #4
Compilation API، یک abstraction سطح بالا از فعالیتهای کامپایل Roslyn است. برای مثال در اینجا میتوان یک اسمبلی را از Syntax tree موجود، تولید کرد و یا جایگزینهایی را برای APIهای قدیمی CodeDOM و Reflection Emit ارائه داد. به علاوه این API امکان دسترسی به گزارشات خطاهای کامپایل را میسر میکند؛ به همراه دسترسی به اطلاعات Semantic analysis. در مورد تفاوت Syntax tree و Semantics در قسمت قبل بیشتر بحث شد.
با ارائهی Roslyn، اینبار کامپایلرهای خط فرمان تولید شده مانند csc.exe، صرفا یک پوسته بر فراز Compilation API آن هستند. بنابراین دیگر نیازی به فراخوانی Process.Start بر روی فایل اجرایی csc.exe مانند یک سری کتابخانههای قدیمی نیست. در اینجا با کدنویسی، به تمام اجزاء و تنظیمات کامپایلر، دسترسی وجود دارد.
کامپایل پویای کد توسط Roslyn
برای کار با API کامپایل، سورس کد، به صورت یک رشته در اختیار کامپایلر قرار میگیرد؛ به همراه تنظیمات ارجاعاتی به اسمبلیهایی که نیاز دارد. سپس کار کامپایلر شروع خواهد شد و شامل مواردی است مانند تبدیل متن دریافتی به Syntax tree و همچنین تبدیل مواردی که اصطلاحا به آنها Syntax sugars گفته میشود مانند خواص get و set دار به معادلهای اصلی آنها. در اینجا کار Semantic analysis هم انجام میشود و شامل تشخیص حوزهی دید متغیرها، تشخیص overloadها و بررسی نوعهای بکار رفتهاست. در نهایت کار تولید فایل باینری اسمبلی، از اطلاعات آنالیز شده صورت میگیرد. البته خروجی کامپایلر میتواند اسمبلیهای exe یا dll، فایل XML مستندات اسمبلی و یا فایلهای .netmudule و .winmdobj مخصوص WinRT هم باشد.
در ادامه، اولین مثال کار با Compilation API را مشاهده میکنید. پیشنیاز اجرای آن همان مواردی هستند که در قسمت قبل بحث شدند. یک برنامهی کنسول سادهی .NET 4.6 را آغاز کرده و سپس بستهی نیوگت Microsoft.CodeAnalysis را در آن نصب کنید. در ادامه کدهای ذیل را به پروژهی آماده شده اضافه کنید:
static void firstCompilation() { var tree = CSharpSyntaxTree.ParseText("class Foo { void Bar(int x) {} }"); var mscorlibReference = MetadataReference.CreateFromFile(typeof (object).Assembly.Location); var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); var comp = CSharpCompilation.Create("Demo") .AddSyntaxTrees(tree) .AddReferences(mscorlibReference) .WithOptions(compilationOptions); var res = comp.Emit("Demo.dll"); if (!res.Success) { foreach (var diagnostic in res.Diagnostics) { Console.WriteLine(diagnostic.GetMessage()); } } }
متدهای تعریف شده توسط Compilation API به یک s جمع، ختم میشوند؛ به این معنا که در اینجا در صورت نیاز، چندین Syntax tree یا ارجاع را میتوان تعریف کرد.
پس از وهله سازی Compilation API و تنظیم آن، اکنون با فراخوانی متد Emit، کار تولید فایل اسمبلی نهایی صورت میگیرد. در اینجا اگر خطایی وجود داشته باشد، استثنایی را دریافت نخواهید کرد. بلکه باید خاصیت Success نتیجهی آنرا بررسی کرده و درصورت موفقیت آمیز نبودن عملیات، خطاهای دریافتی را از مجموعهی Diagnostics آن دریافت کرد. کلاس Diagnostic، شامل اطلاعاتی مانند محل سطر و ستون وقوع مشکل و یا پیام متناظر با آن است.
معرفی مقدمات Semantic analysis
Compilation API به اطلاعات Semantics نیز دسترسی دارد. برای مثال آیا Type A قابل تبدیل به Type B هست یا اصلا نیازی به تبدیل ندارد و به صورت مستقیم قابل انتساب هستند؟ برای درک بهتر این مفهوم نیاز است یک مثال را بررسی کنیم:
static void semanticQuestions() { var tree = CSharpSyntaxTree.ParseText(@" using System; class Foo { public static explicit operator DateTime(Foo f) { throw new NotImplementedException(); } void Bar(int x) { } }"); var mscorlib = MetadataReference.CreateFromFile(typeof (object).Assembly.Location); var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); var comp = CSharpCompilation.Create("Demo").AddSyntaxTrees(tree).AddReferences(mscorlib).WithOptions(options); // var res = comp.Emit("Demo.dll"); // boxing var conv1 = comp.ClassifyConversion( comp.GetSpecialType(SpecialType.System_Int32), comp.GetSpecialType(SpecialType.System_Object) ); // unboxing var conv2 = comp.ClassifyConversion( comp.GetSpecialType(SpecialType.System_Object), comp.GetSpecialType(SpecialType.System_Int32) ); // explicit reference conversion var conv3 = comp.ClassifyConversion( comp.GetSpecialType(SpecialType.System_Object), comp.GetTypeByMetadataName("Foo") ); // explicit user-supplied conversion var conv4 = comp.ClassifyConversion( comp.GetTypeByMetadataName("Foo"), comp.GetSpecialType(SpecialType.System_DateTime) ); }
برای مثال نتیجهی بررسی آخرین تبدیل انجام شده در تصویر فوق مشخص است. با توجه به تعریف public static explicit operator DateTime در سورس کد مورد آنالیز، این تبدیل explicit بوده و همچنین user defined. به علاوه متدی هم که این تبدیل را انجام میدهد، مشخص کردهاست.
تراکنشها در 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(); }
ابتدا لازم است Entity framework را توسط Nuget packages manager دانلود نمایید. سپس به پروژهی خود یک فولدر جدید را به نام Models و درون آن ابتدا یک کلاس را به نام User اضافه کرده و بدین صورت خواهیم داشت :
using System; namespace Console1.Models { public enum UserType { Admin, Ordinary } public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } public UserType Type { get; set; } } }
UserType نیز کاملا مشخص است؛ هر User نقش Admin یا Ordinary را میتواند داشته باشد.
نوبت به نوشتن اینترفیس IUser میرسد. در همین پوشهای که قرار داریم، آن را پیاده سازی مینماییم:
namespace Console1.Models { public interface IUser { int UserId { get; set; } User User { get; set; } } }
هر entity که با User ارتباط دارد، باید اینترفیس فوق را پیاده سازی نماید. حال یک کلاس دیگر را به نام Post در همین پوشه درست کرده و بدین صورت پیاده سازی مینماییم.
using System.ComponentModel.DataAnnotations.Schema; namespace Console1.Models { public class Post : IUser { public int Id { get; set; } public string Context { get; set; } public int UserId { get; set; } [ForeignKey(nameof(UserId))] public User User { get; set; } } }
واضح است که relation از نوع one to many برقرار است و هر User میتواند n تا Post داشته باشد.
خوب تا اینجا کافیست و میخواهیم مدلهای خود را با استفاده از EF به Context معرفی کنیم. میتوانیم در همین پوشه کلاسی را به نام Context ساخته و بصورت زیر بنویسیم
using System.Data.Entity; namespace Console1.Models { public class Context : DbContext { public Context() : base("Context") { } public DbSet<User> Users { get; set; } public DbSet<Post> Posts { get; set; } } }
در اینجا مشخص کردهایم که دو Dbset از نوع User و Post را داریم. بدین معنا که EF دو table را برای ما تولید خواهد کرد. همچنین نام کلید رشتهی اتصالی به دیتابیس خود را نیز، Context معرفی کردهایم.
خوب تا اینجا قسمت اول پروژهی خود را تکمیل کردهایم. الان میتوانیم با استفاده از Migration دیتابیس خود را ساخته و همچنین رکوردهایی را بدان اضافه کنیم. در Package Manager Console خود دستور زیر را وارد نمایید:
enable-migrations
به صورت خودکار پوشهای به نام Migrations ساخته شده و درون آن Configuration.cs قرار میگیرد که آن را بدین صورت تغییر میدهیم:
namespace Console1.Migrations { using Models; using System.Data.Entity.Migrations; internal sealed class Configuration : DbMigrationsConfiguration<Console1.Models.Context> { public Configuration() { AutomaticMigrationsEnabled = true; } protected override void Seed(Console1.Models.Context context) { context.Users.AddOrUpdate(x => x.Id, new User { Id = 1, Name = "aaa", Age = 30, Type = UserType.Admin }, new User { Id = 2, Name = "bbb", Age = 20, Type = UserType.Ordinary }, new User { Id = 3, Name = "ccc", Age = 25, Type = UserType.Ordinary } ); context.Posts.AddOrUpdate(x => x.Id, new Post { Context = "ccc 1", UserId = 3 }, new Post { Context = "bbb 1", UserId = 2 }, new Post { Context = "bbb 2", UserId = 2 }, new Post { Context = "aaa 1", UserId = 1 }, new Post { Context = "bbb 3", UserId = 2 }, new Post { Context = "ccc 2", UserId = 3 }, new Post { Context = "ccc 3", UserId = 3 } ); context.SaveChanges(); } } }
در متد seed، رکوردهای اولیه را به شکل فوق وارد کرده ایم (رکوردها فقط به منظور تست میباشند*). در کنسول دستور Update-database را ارسال کرده، دیتابیس تولید خواهد شد.
قطعا مراحل بالا کاملا بدیهی بوده و نوشتن آنها بدین دلیل بوده که در Repository که الان میخواهیم شروع به نوشتنش کنیم به مدلهای فوق نیاز داریم تا بصورت کاملا عملی با مراحل کار آشنا شویم.
حال میخواهیم به پیاده سازی بخش اصلی این مقاله یعنی repository که از Row Level Security پشتیبانی میکند بپردازیم. در ریشهی پروژهی خود پوشهای را به نام Repository ساخته و درون آن کلاسی را به نام GenericRepository میسازیم. پروژهی شما هم اکنون باید ساختاری شبیه به این را داشته باشد.
GenericRepository.cs را اینگونه پیاده سازی مینماییم
using Console1.Models; using System; using System.Linq; using System.Linq.Dynamic; using System.Linq.Expressions; namespace Console1.Repository { public interface IGenericRepository<T> { IQueryable<T> CustomizeGet(Expression<Func<T, bool>> predicate); void Add(T entity); IQueryable<T> GetAll(); } public class GenericRepository<TEntity, DbContext> : IGenericRepository<TEntity> where TEntity : class, new() where DbContext : Models.Context, new() { private DbContext _entities = new DbContext(); public IQueryable<TEntity> CustomizeGet(Expression<Func<TEntity, bool>> predicate) { IQueryable<TEntity> query = _entities.Set<TEntity>().Where(predicate); return query; } public void Add(TEntity entity) { int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { ((IUser)entity).UserId = userId; } _entities.Set<TEntity>().Add(entity); } public IQueryable<TEntity> GetAll() { IQueryable<TEntity> result = _entities.Set<TEntity>(); int userId = Program.UserId; // یوزد آی دی بصورت فیک ساخته شده // اگر از آیدنتیتی استفاده میکنید میتوان آی دی و هر چیز دیگری که کلیم شده را در اختیار گرفت if (typeof(IUser).IsAssignableFrom(typeof(TEntity))) { User me = _entities.Users.Single(c => c.Id == userId); if (me.Type == UserType.Admin) { return result; } else if (me.Type == UserType.Ordinary) { string query = $"{nameof(IUser.UserId).ToString()}={userId}"; return result.Where(query); } } return result; } public void Commit() { _entities.SaveChanges(); } } }
1) یک اینترفیس Generic را به نام IGenericRepository داریم که کلاس GenericRepository قرار است آن را پیاده سازی نماید.
2) این اینترفیس شامل متدهای CustomizeGet است که فقط یک predicate را گرفته و خیلی مربوط به این مقاله نیست (صرفا جهت اطلاع). اما متد Add و GetAll بصورت مستقیم قرار است هدف row level security را برای ما انجام دهند.
3) کلاس GenericRepository دو Type عمومی را به نام TEntity و DbContext گرفته و اینترفیس IGenericRepository را پیاده سازی مینماید. همچنین صریحا اعلام کردهایم TEntity از نوع کلاس و DbContext از نوع Context ایی است که قبلا نوشتهایم.
4) پیاده سازی متد CustomizeGet را مشاهده مینمایید که کوئری مربوطه را ساخته و بر میگرداند.
5) پیاده سازی متد Add بدین صورت است که به عنوان پارامتر، TEntity را گرفته (مدلی که قرار است save شود). بعد مشاهد میکنید که من به صورت hard code به UserId مقدار دادهام. قطعا میدانید که برای این کار به فرض اینکه از Asp.net Identity استفاده میکنید، میتوانید Claim آن Id کاربر Authenticate شده را بازگردانید.
با استفاده از IsAssignableFrom مشخص کردهایم که آیا TEntity یک Typeی از IUser را داشته است یا خیر؟ در صورت true بودن شرط، UserId را به TEntity اضافه کرده و بطور مثال در Serviceهای خود نیازی به اضافه کردن متوالی این فیلد نخواهید داشت و در مرحلهی بعد نیز آن را به entity_ اضافه مینماییم.
مشاهده مینمایید که این متد به قدری انعطاف پذیری دارد که حتی مدلهای مختلف به صورت کاملا یکپارچه میتوانند از آن استفاده نمایند.
6) به جالبترین متد که GetAll میباشد میرسیم. ابتدا کوئری را از آن Entity ساخته و در مرحلهی بعد مشخص مینماییم که آیا TEntity یک Typeی از IUser میباشد یا خیر؟ در صورت برقرار بودن شرط، User مورد نظر را یافته در صورتیکه Typeی از نوع Admin داشت، همهی مجموعه را برخواهیم گرداند (Admin میتواند همهی پستها را مشاهده نماید) و در صورتیکه از نوع Ordinary باشد، با استفاده از dynamic linq، کوئری مورد نظر را ساخته و شرط را ایجاد میکنیم که UserId برابر userId مورد نظر باشد. در این صورت بطور مثال همهی پستهایی که فقط مربوط به user خودش میباشد، برگشت داده میشود.
نکته: برای دانلود dynamic linq کافیست از طریق nuget آن را جست و جو نمایید: System.Linq.Dynamic
و اگر هم از نوع IUser نبود، result را بر میگردانیم. بطور مثال فرض کنید مدلی داریم که قرار نیست security روی آن اعمال شود. پس کوئری ساخته شده قابلیت برگرداندن همهی رکوردها را دارا میباشد.
7) متد Commit هم که پرواضح است عملیات save را اعمال میکند.
قبلا در قسمت Seed رکوردهایی را ساخته بودیم. حال میخواهیم کل این فرآیند را اجرا نماییم. Program.cs را از ریشهی پروژهی خود باز کرده و اینگونه تغییر میدهیم:
using System; using Console1.Models; using Console1.Repository; using System.Collections.Generic; using System.Linq; namespace Console1 { public class Program { public static int UserId = 1; //fake userId static void Main() { GenericRepository<Post, Context> repo = new GenericRepository<Post, Context>(); List<Post> posts = repo.GetAll().ToList(); foreach (Post item in posts) Console.WriteLine(item.Context); Console.ReadKey(); } } }
همانطور که ملاحظه میکنید، UserId به صورت fake ساخته شده است. آن چیزی که هم اکنون در دیتابیس رفته، بدین صورت است که UserId = 1 برابر Admin و بقیه Ordinary میباشند. در متد Main برنامه، یک instance از GenericRepository را گرفته و بعد با استفاده از متد GetAll و لیست کردن آن، همهی رکوردهای مورد نظر را برگردانده و سپس چاپ مینماییم. در صورتی که UserId برابر 1 باشد، توقع داریم که همهی رکوردها بازگردانده شود:
حال کافیست مقدار userId را بطور مثال تغییر داده و برابر 2 بگذاریم. برنامه را اجرا کرده و مشاهد میکنیم که با تغییر یافتن userId، عملیات مورد نظر متفاوت میگردد و به صورت زیر خواهد شد:
میبینید که تنها با تغییر userId رفتار عوض شده و فقط Postهای مربوط به آن User خاص برگشت داده میشود.
از همین روش میتوان برای طراحی Repositoryهای بسیار پیچیدهتر نیز استفاده کرد و مقدار زیادی از validationها را به طور مستقیم بدان واگذار نمود!
دانلود کدها در Github