<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
bower install angular-route --save
<script src="bower_components/angular-route/angular-route.min.js" type="text/javascript"></script>
<div class="col-md-9"> <ng-view></ng-view> </div>
model.panel = { title: "Panel Title", items: [ { title: "Home", url: "#/home" }, { title: "Articles", url: "#/articles" }, { title: "Authors", url: "#/authors" } ] };
var module = angular.module("dntModule", ["ngRoute"]);
module.config(function ($routeProvider) { $routeProvider .when("/home", { template: "<app-home></app-home>" }) .when("/articles", { template: "<app-articles></app-articles>" }) .when("/authors", { template: "<app-authors></app-authors>" }) .otherwise({ redirectTo: "/home" }); });
module.component("appHome", { template: ` <hr><div> <div>Panel heading = HomePage</div> <div> HomePage </div> </div>` }); module.component("appArticles", { template: ` <hr><div> <div>Panel heading = Articles</div> <div> Articles </div> </div>` }); module.component("appAuthors", { template: ` <hr><div> <div>Panel heading = Authors</div> <div> Authors </div> </div>` });
اکنون توسط لینکهای تعریف شده میتوانیم به راحتی درون تمپلیتها، پیمایش کنیم. همانطور که عنوان شد تا اینجا مسیریاب پیشفرض Angular هیچ اطلاعی از کامپوننتها ندارد؛ بلکه آنها را با کمک template، به صورت غیر مستقیم، درون صفحه نمایش دادهایم.
معرفی Component Router
مزیت این روتر این است که به صورت اختصاصی برای کار با کامپوننتها طراحی شده است. بنابراین دیگر نیازی به استفاده از template درون route configuration نیست. برای استفاده از این روتر ابتدا باید پکیج آن را نصب کنیم:
bower install angular-component-router --save
سپس وابستگی فوق را با روتر پیشفرضی که در مثال قبل بررسی کردیم، جایگزین خواهیم کرد:
<script src="bower_components/angular-component-router/angular_1_router.js"></script>
همچنین درون فایل module.js به جای وابستگی ngRoute از ngComponentrouter استفاده خواهیم کرد:
var module = angular.module("dntModule", ["ngComponentRouter"]);
در ادامه به جای تمامی route configurations قبلی، اینبار یک کامپوننت جدید را به صورت زیر ایجاد خواهیم کرد:
module.component("appHome", { template: ` <hr> <div> <div>Panel heading = HomePage</div> <div> HomePage </div> </div>` });
همانطور که مشاهده میکنید برای پاسخگویی به تغییرات URL، مقدار routeConfig$ را مقداردهی کردهایم. در اینجا به جای بارگذاری تمپلیت، خود کامپوننت، در هر یک از ruleهای فوق بارگذاری خواهد شد. برای حالت otherwise نیز از سینتکس **/ استفاده کردهایم.
تمپلیت کامپوننت فوق نیز به صورت زیر است:
<div class="container"> <div class="row"> <div class="col-md-3"> <hr> <dnt-widget></dnt-widget> </div> <div class="col-md-9"> <ng-outlet></ng-outlet> </div> </div> </div>
لازم به ذکر است دیگر نباید از دایرکتیو ng-view استفاده کنیم؛ زیرا این دایرکتیو برای استفاده از روتر اصلی طراحی شده است. به جای آن از دایرکتیو ng-outlet استفاده شده است. این کامپوننت به عنوان یک کامپوننت top level عمل خواهد کرد. بنابراین درون صفحهی index.html از کامپوننت فوق استفاده خواهیم کرد:
<html ng-app="dntModule"> <head> <meta charset="UTF-8"> <title>Using Angular 1.5 Component Router</title> <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css"> <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css"> </head> <body> <dnt-app></dnt-app> <script src="bower_components/angular/angular.js" type="text/javascript"></script> <script src="bower_components/angular-component-router/angular_1_router.js"></script> <script src="scripts/module.js" type="text/javascript"></script> <script src="scripts/dnt-app.component.js"></script> <script src="scripts/dnt-widget.component.js"></script> </body> </html>
در نهایت باید جهت فعالسازی سیستم مسیریابی جدید، سرویس زیر را همراه با نام کامپوننت فوق ریجستر کنیم:
module.value("$routerRootComponent", "dntApp");
اکنون اگر برنامه را اجرا کنید خواهید دید که همانند قبل، کار خواهد کرد. اما اینبار از روتر جدید و سازگار با کامپوننتها استفاده میکند.
کدهای این قسمت را نیز از اینجا میتوانید دریافت کنید.
mkdir ps_cmdlet_with_csharp && cd "$_" dotnet new classlib dotnet add package PowerShellStandard.Library mv Class1.cs GetHelloCommand.cs
namespace ps_cmdlet_with_csharp; using System.Management.Automation; [Cmdlet(VerbsCommon.Get, "Hello")] public class GetHelloCommand : PSCmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] public string Name { get; set; } protected override void BeginProcessing() { WriteObject("Start processing"); } protected override void ProcessRecord() { WriteObject("Hello " + Name); } protected override void EndProcessing() { WriteObject("End processing"); } }
using System.Collections.ObjectModel; using System.Management.Automation.Host; namespace System.Management.Automation { public abstract class PSCmdlet : Cmdlet { protected PSCmdlet(); public PSEventManager Events { get; } public PSHost Host { get; } public CommandInvocationIntrinsics InvokeCommand { get; } public ProviderIntrinsics InvokeProvider { get; } public JobManager JobManager { get; } public JobRepository JobRepository { get; } public InvocationInfo MyInvocation { get; } public PagingParameters PagingParameters { get; } public string ParameterSetName { get; } public SessionState SessionState { get; } public PathInfo CurrentProviderLocation(string providerId); public Collection<string> GetResolvedProviderPathFromPSPath(string path, out ProviderInfo provider); public string GetUnresolvedProviderPathFromPSPath(string path); public object GetVariableValue(string name); public object GetVariableValue(string name, object defaultValue); } }
PS /> dotnet build PS /> Import-Module ./bin/Debug/net7.0/ps_cmdlet_with_csharp.dll
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ------ Cmdlet Get-Hello 1.0.0.0 ps_cmdlet_with_csharp
namespace ps_cmdlet_with_csharp; using System.Management.Automation; [Cmdlet(VerbsCommon.Get, "Hello")] public class GetHelloCommand : PSCmdlet { // as before } [Cmdlet(VerbsCommon.Get, "Greetings")] public class GetGreetingsCommand : PSCmdlet { [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true)] public string Name { get; set; } protected override void BeginProcessing() { WriteObject("Start processing"); } protected override void ProcessRecord() { WriteObject("Greetings " + Name); } protected override void EndProcessing() { WriteObject("End processing"); } }
Import-Module: Invalid assembly public key.
<Project Sdk="Microsoft.NET.Sdk"> <!-- other tags --> <ItemGroup> <Compile Include="./GetHelloCommand.cs" /> </ItemGroup> </Project>
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ------ Cmdlet Get-Greetings 1.0.0.0 ps_cmdlet_with_csharp Cmdlet Get-Hello 1.0.0.0 ps_cmdlet_with_csharp
namespace ps_cmdlet_with_csharp; using System.Management.Automation; using System.Net.Http.Headers; [Cmdlet(VerbsCommon.Push, "SlackMessage")] [Alias("ssm")] [OutputType(typeof(string))] public class SlackMessageCommand : PSCmdlet { [Parameter(Mandatory = true)] [Alias("m")] public string Message { get; set; } [Parameter(Mandatory = true)] [Alias("t")] [ValidatePattern(@"xoxp-[0-9]{11}-[0-9]{12}-[0-9]{13}-[0-9a-zA-Z]{32}")] public string Token { get; set; } [Parameter(Mandatory = true)] [Alias("cid")] public string ChannelID { get; set; } protected async override void ProcessRecord() { base.ProcessRecord(); try { using var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", this.Token); var values = new Dictionary<string, string> { { "channel", this.ChannelID }, { "text", this.Message } }; var content = new FormUrlEncodedContent(values); var response = await client.PostAsync("https://slack.com/api/chat.postMessage", content); var responseString = await response.Content.ReadAsStringAsync(); WriteObject("Message sent"); } catch (Exception ex) { WriteObject(ex.Message); } } }
PS /> $PROFILE | Get-Member -Type NoteProperty | Select-Object Name, Definitionbject Name, Definition Name Definition ---- ---------- AllUsersAllHosts string AllUsersAllHosts=/usr/local/microsoft/powershell/7/… AllUsersCurrentHost string AllUsersCurrentHost=/usr/local/microsoft/powershell… CurrentUserAllHosts string CurrentUserAllHosts=/Users/sirwanafifi/.config/powe… CurrentUserCurrentHost string CurrentUserCurrentHost=/Users/sirwanafifi/.config/p…
$SlackProjectPath = "/Users/sirwanafifi/Desktop/ps_cmdlet_with_csharp/bin/Debug/net7.0/ps_cmdlet_with_csharp.dll" Import-Module $SlackProjectPath
PS /> Get-Command -Module ps_cmdlet_with_csharp CommandType Name Version Source ----------- ---- ------- ---- Cmdlet Push-SlackMessage 1.0.0.0 ps_…
در برنامههای تحت وب، در بعضی موارد نیاز داریم تا برای کاربر، امکان ثبت دادههایش را با آپلود فایلهای Excel فراهم کنیم. برای مثال در مطلب خواندن اطلاعات از فایل اکسل با استفاده از LinqToExcel ، امکان خواندن از Excel توضیح داده شده، اما نقطه ضعف این روشها، وابستگی به Providerهای مایکروسافت است که در صورت عدم نصب آن ها:
Microsoft.Jet.OLEDB.4.0 provider --> Excel 97-2003 format (.xls) Microsoft.ACE.OLEDB.12.0 provider --> Excel 2007+ format (.xlsx)
The ‘Microsoft.Jet.OLEDB.4.0’ provider is not registered on the local machine The ‘Microsoft.ACE.OLEDB.12.0’ provider is not registered on the local machine
البته راه حل، نصب Office 2007 Data Connectivity Components یا Office 2010 Database Engine بر روی سرور میباشد. اما اگر هاست اشتراکی بوده و اجازه نصب نداشته باشیم؟
در این مقاله به بررسی کتابخانه ExcelDataReader میپردازیم که امکان خواندن فایلهای اکسل را بدون نیاز به نصب هرگونه پیش نیازی بر روی سرور، برای ما فراهم میکند.
برای این کار:
1- ابتدا یک پروژه خالی Asp.Net MVC را ایجاد میکنیم.
2- با استفاده از دستورات زیر در Package Manager Console بستههای ExcelDataReader و ExcelDataReader.DataSet را نصب میکنیم:
PM> Install-Package ExcelDataReader PM> Install-Package ExcelDataReader.DataSet
3- سپس کنترلر مورد نظر (در اینجا HomeController) را ایجاد نموده و اکشن Upload را بصورت زیر در آن قرار میدهیم:
public ActionResult Upload() { return View(); }
4- برای آپلود فایل، اکشن دیگری را با نام Upload نیاز داریم که آن را بصورت زیر ایجاد میکنیم:
توجه: در قطعه کد زیر سعی شده از حداقل کانفیگ کتابخانه استفاده شود. کانفیگ بیشتر
[HttpPost] [ValidateAntiForgeryToken] public ActionResult Upload(HttpPostedFileBase upload) { // اعتبار سنجی فایل آپلود شده if (upload != null && upload.ContentLength > 0 && (upload.FileName.EndsWith(".xls") || upload.FileName.EndsWith(".xlsx"))) { // با خواندن فایل به صورت باینری، این کتابخانه نیازی به نصب پیش نیازهای آفیس ندارد Stream stream = upload.InputStream; // نیازی به نگرانی در مورد پسوند فایل نیست // کتابخانه به صورت خودکار کلاس مورد نظر برای پسوند مربوطه را استفاده میکند // ExcelDataReader.ExcelBinaryReader یا ExcelDataReader.ExcelOpenXmlReader IExcelDataReader reader = ExcelReaderFactory.CreateReader(stream); // روش ذکر شده در قسمت دوم برای خواندن کل اطلاعات بصورت یکجا DataSet result = reader.AsDataSet(new ExcelDataSetConfiguration() { ConfigureDataTable = (tableReader) => new ExcelDataTableConfiguration() { // true: ردیف اول از فایل را به عنوان هدر در نظر میگیرد // مقدار پیش فرض: false UseHeaderRow = true } }); reader.Close(); return View(result.Tables[0]); } ModelState.AddModelError("File", "Please upload Excel file ..."); return View(); }
روش دیگر خواندن اطلاعات:
do { while (reader.Read()) { // reader.GetDouble(0); } } while (reader.NextResult());
5- خب حالا از یک View (با نام Upload) هم برای ارسال فایل و همچنین نمایش محتویات آپلود شده بصورت زیر استفاده میکنیم:
@model System.Data.DataTable @using System.Data; <h2>Upload File</h2> @using (Html.BeginForm("Upload", "Home", null, FormMethod.Post, new { enctype = "multipart/form-data" })) { @Html.AntiForgeryToken() @Html.ValidationSummary() <div> <input type="file" id="dataFile" name="upload" /> </div> <div> <input type="submit" value="Upload" /> </div> if (Model != null) { <table> <thead> <tr> @foreach (DataColumn col in Model.Columns) { <th>@col.ColumnName</th> } </tr> </thead> <tbody> @foreach (DataRow row in Model.Rows) { <tr> @foreach (DataColumn col in Model.Columns) { <td>@row[col.ColumnName]</td> } </tr> } </tbody> </table> } }
6- حالا پروژه را اجرا میکنیم تا خروجی را مشاهده کنیم.
ابتدا فایل مورد نظر را انتخاب و آپلود میکنیم:
و خروجی به صورت زیر خواهد بود:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: UploadExcelFiles.rar
در ادامه قصد داریم تمام این موارد را مانند Minimal API's و معماری برشهای عمودی به همراه CQRS، در طی یک سری و یک پروژهی عملی ساخت یک Blog به نام MinimalBlog، بررسی کنیم. البته هدف ما در اینجا صرفا ساخت backend ساختار یافتهی این برنامهاست؛ منهای UI آن. هدف اصلی ما از این سری، ارائهی یک معماری، جهت کار با Minimal API's است.
دریافت کدهای کامل این سری
جهت مرور سریعتر و سادهتر این سری، کدهای کامل آنرا از اینجا میتوانید دریافت کنید: MinimalBlog.zip
پروژههایی که برنامهی MinimalBlog را تشکیل میدهند
برنامهی MinimalBlog، تنها از سه پروژهی زیر تشکیل میشود:
MinimalBlog.Api: این پروژه از نوع minimal API's است که توسط دستور جدید «dotnet new webapi --use-minimal-apis» آغاز خواهد شد و به صورت پیشفرض به همراه پشتیبانی از OpenAPI نیز هست. البته اگر از VS2022 استفاده میکنید، در حین آغاز یک پروژهی Web API جدید، تیک مربوط به use controllers را در UI بردارید تا از Minimal API's استفاده شود.
MinimalBlog.Dal: که Dal در اینجا مخفف data access layer است و یک class library میباشد و با دستور dotnet new classlib آغاز میشود.
MinimalBlog.Domain: نیز یک class library است و با دستور dotnet new classlib آغاز میشود.
همانطور که مشاهده میکنید، این طراحی جدید، بدون وجود لایهی متداول سرویسها و یا مخازن است.
بررسی ساختار ابتدایی پروژهی MinimalBlog.Api
در اینجا تنها تک فایل Program.cs، به همراه تنظیمات برنامه قابل مشاهدهاست و فایل Starup.cs از آن حذف شدهاست (اطلاعات بیشتر). این فایل نیز بر مبنای مفهوم top level programs طراحی شدهاست و به همراه تعریف class و یا فضای نامی نیست:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen();
پس از آن به تعاریف زیر میرسیم؛ تعاریف میان افزارهایی که پیشتر در متد Configure کلاس Startup انجام میشدند، الان همگی در تک فایل Program.cs قرار دارند:
var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection();
در انتهای این فایل نیز تعاریف پیشفرض زیر قرار دارند:
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; app.MapGet("/weatherforecast", () => { var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast ( DateTime.Now.AddDays(index), Random.Shared.Next(-20, 55), summaries[Random.Shared.Next(summaries.Length)] )) .ToArray(); return forecast; }) .WithName("GetWeatherForecast"); app.Run(); record WeatherForecast(DateTime Date, int TemperatureC, string? Summary) { public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); }
در همین حال اگر برنامهی Api را اجرا کنیم، به تصویر زیر خواهیم رسید:
در ادامه کدهای موجود در این فایل را Refactor کرده و به کلاسهای دیگری منتقل میکنیم؛ چون اگر قرار باشد در طول زمان تمام endpoints مدنظر را در همینجا تعریف کنیم، کنترل برنامه از دست خارج خواهد شد.
غنی سازی Solution و کامپایلر #C با استفاده از فایلهای editorconfig. و Directory.Build.props
در مورد این دو فایل در مطلب «غنی سازی کامپایلر C# 9.0 با افزونهها » بیشتر بحث شدهاست. هدف از آنها، اعمال یکسری تنظیمات سراسری، به تمام پروژههای یک solution به صورت یکدست است؛ مانند تنظیمات کامپایلر جهت نمایش اخطارها به صورت خطاها، تعریف usingهای سراسری سیشارپ 10 و یا اعمال Roslyn analyzers به تمام پروژهها. این دو فایل را به همراه پروژهی پیوست میتوانید دریافت کنید و ... باید جزء استاندارد تمام پروژههای جدید باشند. چون وجود آنها سبب خواهد شد که به شدت کیفیت کدهای نهایی افزایش یابند و مبتنی بر یکسری best practices شوند.
شروع به کار با RavenDB
- مروری بر مفاهیم مقدماتی NoSQL
- ردهها و انواع مختلف بانکهای اطلاعاتی NoSQL
- چه زمانی بهتر است از بانکهای اطلاعاتی NoSQL استفاده کرد و چه زمانی خیر؟
لطفا یکبار این پیشنیازها را پیش از شروع به کار مطالعه نمائید؛ چون بسیاری از مفاهیم پایهای و اصطلاحات مرسوم دنیای NoSQL در این سه قسمت بررسی شدهاند و از تکرار مجدد آنها در اینجا صرفنظر خواهد شد.
RavenDB چیست؟
RavenDB یک بانک اطلاعاتی سورس باز NoSQL سندگرای تهیه شده با دات نت است. ساختار کلی بانکهای اطلاعاتی NoSQL سندگرا، از لحاظ نحوه ذخیره سازی اطلاعات، با بانکهای اطلاعاتی رابطهای متداول، کاملا متفاوت است. در اینگونه بانکهای اطلاعاتی، رکوردهای اطلاعات، به صورت اشیاء JSON ذخیره میشوند. اشیاء JSON یا JavaScript Object Notation بسیار شبیه به anonymous objects سی شارپ هستند. JSON روشی است که توسط آن JavaScript اشیاء خود را معرفی و ذخیره میکند. به عنوان رقیبی برای XML مطرح است؛ نسبت به XML اندکی فشردهتر بوده و عموما دارای اسکیمای خاصی نیست و در بسیاری از اوقات تفسیر المانهای آن به مصرف کننده واگذار میشود.
در JSON عموما سه نوع المان پایه مشاهده میشوند:
- اشیاء که به صورت {object} تعریف میشوند.
- مقادیر "key":"value" که شبیه به نام خواص و مقادیر آنها در دات نت هستند.
- و آرایهها به صورت [array]
همچنین ترکیبی از این سه عنصر یاد شده نیز همواره میسر است. برای مثال، یک key مشخص میتواند دارای مقداری حاوی یک آرایه یا شیء نیز باشد.
JSON: JavaScript Object Notation document :{ key: "Value", another_key: { name: "embedded object" }, some_date: new Date(), some_number: 12 } C# anonymous object var Document = new { Key= "Value", AnotherKey= new { Name = "embedded object" }, SomeDate = DateTime.Now(), SomeNumber = 12 };
یک نکته مهم
اگر پیشنیازهای بحث را مطالعه کرده باشید، حتما بارها با این جمله که دنیای NoSQL از تراکنشها پشتیبانی نمیکند، برخورد داشتهاید. این مطلب در مورد RavenDB صادق نیست و این بانک اطلاعاتی NoSQL خاص، از تراکنشها پشتیبانی میکند. RavenDB در Document store خود ACID عملکرده و از تراکنشها پشتیبانی میکند. اما تهیه ایندکسهای آن بر مبنای مفهوم عاقبت یک دست شدن عمل میکند.
مجوز استفاده از RavenDB
هرچند مجموعه سرور و کلاینت RavenDB سورس باز هستند، اما این مورد به معنای رایگان بودن آن نیست. مجوز استفاده از RavenDB نوع خاصی به نام AGPL است. به این معنا که یا کل کار مشتق شده خود را باید به صورت رایگان و سورس باز ارائه دهید و یا اینکه مجوز استفاده از آنرا برای کارهای تجاری بسته خود خریداری نمائید. نسخه استاندارد آن نزدیک به هزار دلار است و نسخه سازمانی آن نزدیک به 2800 دلار به ازای هر سرور.
شروع به نوشتن اولین برنامه کار با RavenDB
ابتدا یک پروژه کنسول ساده را آغاز کنید. سپس کلاسهای مدل زیر را به آن اضافه نمائید:
using System.Collections.Generic; namespace RavenDBSample01.Models { public class Question { public string By { set; get; } public string Title { set; get; } public string Content { set; get; } public List<Comment> Comments { set; get; } public List<Answer> Answers { set; get; } public Question() { Comments = new List<Comment>(); Answers = new List<Answer>(); } } } namespace RavenDBSample01.Models { public class Comment { public string By { set; get; } public string Content { set; get; } } } namespace RavenDBSample01.Models { public class Answer { public string By { set; get; } public string Content { set; get; } } }
PM> Install-Package RavenDB.Client PM> Install-Package RavenDB.Server
اکنون به پوشه packages\RavenDB.Server.2.5.2700\tools مراجعه کرده و برنامه Raven.Server.exe را اجرا کنید تا سرور RavenDB شروع به کار کند. این سرور به صورت پیش فرض بر روی پورت 8080 اجرا میشود. از این جهت که در RavenDB نیز همانند سایر Document Stores مطرح، امکان دسترسی به اسناد از طریق REST API و Urlها وجود دارد.
البته لازم به ذکر است که RavenDB در 4 حالت برنامه کنسول (همین سرور فوق)، نصب به عنوان یک سرویس ویندوز NT، هاست شدن در IIS و حالت مدفون شده یا Embedded قابل استفاده است.
خوب؛ همین اندازه برای برپایی اولیه RavenDB کفایت میکند.
using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { session.Store(new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }); session.SaveChanges(); } } } } }
کار با ایجاد یک DocumentStore که به آدرس سرور اشاره میکند و کار مدیریت اتصالات را برعهده دارد، شروع خواهد شد. اگر نمیخواهید Url را درون کدهای برنامه مقدار دهی کنید، میتوان از فایل کانفیگ برنامه نیز برای این منظور کمک گرفت:
<connectionStrings> <add name="ravenDB" connectionString="Url=http://localhost:8080"/> </connectionStrings>
سپس با ایجاد Session در حقیقت یک Unit of work آغاز میشود که درون آن میتوان انواع و اقسام دستورات را صادر نمود و سپس در پایان کار، با فراخوانی SaveChanges، این اعمال ذخیره میگردند. در RavenDB یک سشن باید طول عمری کوتاه داشته باشد و اگر تعداد عملیاتی که در آن صادر کردهاید، زیاد است با خطای زیر متوقف خواهید شد:
The maximum number of requests (30) allowed for this session has been reached.
در یک برنامه واقعی، ایجاد DocumentStore یکبار در آغاز کار برنامه باید انجام گردد. اما هر سشن یا هر واحد کاری آن، به ازای تراکنشهای مختلفی که باید صورت گیرند، بر روی این DocumentStore، ایجاد شده و سپس بسته خواهند شد. برای مثال در یک برنامه ASP.NET، در فایل Global.asax در زمان آغاز برنامه، کار ایجاد DocumentStore انجام شده و سپس به ازای هر درخواست رسیده، یک سشن RavenDB ایجاد و در پایان درخواست، این سشن آزاد خواهد شد.
برنامه را اجرا کنید، سپس به کنسول سرور RavenDB که پیشتر آنرا اجرا نمودیم مراجعه نمائید تا نمایی از عملیات انجام شده را بتوان مشاهده کرد:
Raven is ready to process requests. Build 2700, Version 2.5.0 / 6dce79a Server started in 14,438 ms Data directory: D:\Prog\RavenDBSample01\packages\RavenDB.Server.2.5.2700\tools\Database\System HostName: <any> Port: 8080, Storage: Esent Server Url: http://localhost:8080/ Available commands: cls, reset, gc, q Request # 1: GET - 514 ms - <system> - 404 - /docs/Raven/Replication/Destinations Request # 2: GET - 763 ms - <system> - 200 - /queries/?&id=Raven%2FHilo%2Fquestions&id=Raven%2FServerPrefixForHilo Request # 3: PUT - 185 ms - <system> - 201 - /docs/Raven/Hilo/questions Request # 4: POST - 103 ms - <system> - 200 - /bulk_docs PUT questions/1
عملیات PUT حتما نیاز به یک Id از پیش مشخص دارد و این Id، پیشتر در سطری که Hilo در آن ذکر شده (یکی از الگوریتمهای محاسبه Id در RavenDB)، محاسبه گردیده است. برای نمونه در اینجا الگوریتم Hilo مقدار "questions/1" را به عنوان Id محاسبه شده بازگشت داده است.
در سطری که عملیات Post به آدرس bulk_docs سرور ارسال گردیده است، کار ارسال یکباره چندین شیء JSON برای کاهش رفت و برگشتها به سرور انجام میشود.
و برای کوئری گرفتن مقدماتی از اطلاعات ثبت شده میتوان نوشت:
using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); }
نگاهی به بانک اطلاعاتی ایجاد شده
در همین حال که سرور RavenDB در حال اجرا است، مرورگر دلخواه خود را گشوده و سپس آدرس http://localhost:8080 را وارد نمائید. بلافاصله، کنسول مدیریتی تحت وب این بانک اطلاعاتی که با سیلورلایت نوشته شده است، ظاهر خواهد شد:
و اگر بر روی هر سطر اطلاعات دوبار کلیک کنید، به معادل JSON آن نیز خواهید رسید:
اینبار برنامه را به صورت زیر تغییر دهید تا روابط بین کلاسها را نیز پیاده سازی کند:
using System; using Raven.Client.Document; using RavenDBSample01.Models; namespace RavenDBSample01 { class Program { static void Main(string[] args) { using (var store = new DocumentStore { Url = "http://localhost:8080" }.Initialize()) { using (var session = store.OpenSession()) { var question = new Question { By = "users/Vahid", Title = "Raven Intro", Content = "Test...." }; question.Answers.Add(new Answer { By = "users/Farid", Content = "بررسی میشود" }); session.Store(question); session.SaveChanges(); } using (var session = store.OpenSession()) { var question1 = session.Load<Question>("questions/1"); Console.WriteLine(question1.By); } } } } }
اگر برنامه فوق را اجرا کرده و به ساختار اطلاعات ذخیره شده نگاهی بیندازیم به شکل زیر خواهیم رسید:
نکته جالبی که در اینجا وجود دارد، عدم نیاز به join نویسی برای دریافت اطلاعات وابسته به یک شیء است. اگر سؤالی وجود دارد، پاسخهای به آن و یا سایر نظرات، یکجا داخل همان سؤال ذخیره میشوند و به این ترتیب سرعت دسترسی نهایی به اطلاعات بیشتر شده و همچنین قفل گذاری روی سایر اسناد کمتر. این مساله نیز به ذات NoSQL و یا غیر رابطهای RavenDB بر میگردد. در بانکهای اطلاعاتی NoSQL، مفاهیمی مانند کلیدهای خارجی، JOIN بین جداول و امثال آن وجود خارجی ندارند. برای نمونه اگر به کلاسهای مدلهای برنامه دقت کرده باشید، خبری از وجود Id در آنها نیست. RavenDB یک Document store است و نه یک Relation store. در اینجا کل درخت تو در توی خواص یک شیء دریافت و به صورت یک سند ذخیره میشود. به حاصل این نوع عملیات در دنیای بانکهای اطلاعاتی رابطهای، Denormalized data هم گفته میشود.
البته میتوان به کلاسهای تعریف شده خاصیت رشتهای Id را نیز اضافه کرد. در این حالت برای مثال در حالت فراخوانی متد Load، این خاصیت رشتهای، با Id تولید شده توسط RavenDB مانند "questions/1" مقدار دهی میشود. اما از این Id برای تعریف ارجاعات به سؤالات و پاسخهای متناظر استفاده نخواهد شد؛ چون تمام آنها جزو یک سند بوده و داخل آن قرار میگیرند.
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> ... <aspNetCore processPath="bin\IISSupport\VSIISExeLauncher.exe" arguments="-argFile IISExeLauncherArgs.txt" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" stdoutLogEnabled="false" /> ... </system.webServer> </configuration>
<?xml version="1.0" encoding="utf-8"?> <configuration> <system.webServer> ... <aspNetCore processPath=".\ApplicationName.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" /> ... </system.webServer> </configuration>
یک مثال: تهیهی یک TagHelperComponent جهت ویرایش تگهای article
using System; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; namespace TestTagHelperComponent2.Utils { public class ArticleTagHelperComponent : TagHelperComponent { public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase)) { output.PostContent.AppendHtml("<script>console.log('Running ArticleTagHelperComponent');</script>"); } return Task.CompletedTask; } } }
<article> For Testing the TagHelperComponent. </article>
در ادامه برای اینکه سیستم را از وجود این TagHelperComponent مطلع کنیم، باید آنرا به صورت یک سرویس جدید، به فایل آغازین برنامه معرفی کنیم:
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>(); services.AddMvc(); }
نکتهی مهم TagHelperComponentها این است که در قسمت بررسی تگ در حال پردازش:
if (string.Equals(context.TagName, "article", StringComparison.OrdinalIgnoreCase))
using System; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Razor.TagHelpers; using Microsoft.AspNetCore.Razor.TagHelpers; using Microsoft.Extensions.Logging; namespace TestTagHelperComponent2.Utils { [HtmlTargetElement("article")] [EditorBrowsable(EditorBrowsableState.Never)] public class ArticleTagHelperComponentTagHelper : TagHelperComponentTagHelper { public ArticleTagHelperComponentTagHelper( ITagHelperComponentManager componentManager, ILoggerFactory loggerFactory) : base(componentManager, loggerFactory) { } } }
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper *, TestTagHelperComponent2
پس از این تنظیمات است که اگر برنامه را اجرا کنید، این تغییر را (درج اسکریپت در بین تگ article) ملاحظه خواهید کرد:
Tag Helper Components توکار ASP.NET Core 2.0
در حال حاضر دو TagHelperComponent به نامهای HeadTagHelper و BodyTagHelper به صورت پیش فرض به سیستم اضافه شدهاند. یعنی تگهای head و body در ASP.NET Core 2.0 را میتوان توسط TagHelperComponent تحت تاثیر قرار داد و نیازی به تنظیمات TagHelperComponentTagHelper اضافهی فوق برای آنها وجود ندارد.
یک مثال:
public class MyHeadTagHelperComponent : TagHelperComponent { public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { if (string.Equals(context.TagName, "head", StringComparison.OrdinalIgnoreCase)) { output.PostContent.AppendHtml("<script>console.log('head tag');</script>"); } return Task.CompletedTask; } }
public void ConfigureServices(IServiceCollection services) { services.AddTransient<ITagHelperComponent, MyHeadTagHelperComponent>(); services.AddTransient<ITagHelperComponent, ArticleTagHelperComponent>(); services.AddMvc(); }
EF Code First #12
پیاده سازی الگوی Context Per Request در برنامههای مبتنی بر EF Code first
در طراحی برنامههای چند لایه مبتنی بر EF مرسوم نیست که در هر کلاس و متدی که قرار است از امکانات آن استفاده کند، یکبار DbContext و کلاس مشتق شده از آن وهله سازی شوند؛ به این ترتیب امکان انجام امور مختلف در طی یک تراکنش از بین میرود. برای حل این مشکل الگویی مطرح شده است به نام Session/Context Per Request و یا به اشتراک گذاری یک Unit of work در لایههای مختلف برنامه در طی یک درخواست، که در ادامه یک پیاده سازی آنرا با هم مرور خواهیم کرد.
البته این سشن با سشن ASP.NET یکی نیست. در NHibernate معادل DbContextایی که در اینجا ملاحظه میکنید، Session نام دارد.
اهمیت بکارگیری الگوی Unit of work و به اشتراک گذاری آن در طی یک درخواست
در الگوی واحد کار یا همان DbContext در اینجا، تمام درخواستهای رسیده به آن، در صف قرار گرفته و تمام آنها در پایان کار، به بانک اطلاعاتی اعمال میشوند. برای مثال زمانیکه شیءایی را به یک وهله از DbContext اضافه/حذف میکنیم، یا در ادامه مقدار خاصیتی را تغییر میدهیم، هیچکدام از این تغییرات تا زمانیکه متد SaveChanges فراخوانی نشود، به بانک اطلاعاتی اعمال نخواهند شد. این مساله مزایای زیر را به همراه خواهد داشت:
الف) کارآیی بهتر
در اینجا از یک کانکشن باز شده، حداکثر استفاده صورت میگیرد. چندین و چند عملیات در طی یک batch به بانک اطلاعاتی اعمال میگردند؛ بجای اینکه برای اعمال هرکدام، یکبار اتصال جداگانهای به بانک اطلاعاتی باز شود.
ب) بررسی مسایل همزمانی
استفاده از یک الگوی واحد کار، امکان بررسی خودکار تمام تغییرات انجام شده بر روی یک موجودیت را در متدها و لایههای مختلف میسر کرده و به این ترتیب مسایل مرتبط با ConcurrencyMode عنوان شده در قسمتهای قبل به نحو بهتری قابل مدیریت خواهند بود.
ج) استفاده صحیح از تراکنشها
الگوی واحد کار به صورت خودکار از تراکنشها استفاده میکند. اگر در حین فراخوانی متد SaveChanges مشکلی رخ دهد، کل عملیات Rollback خواهد شد و تغییری در بانک اطلاعاتی رخ نخواهد داد. بنابراین استفاده از یک تراکنش در حین چند عملیات ناشی از لایههای مختلف برنامه، منطقیتر است تا اینکه هر کدام، در تراکنشی جدا مشغول به کار باشند.
کلاسهای مدل مثال جاری
در مثالی که در این قسمت بررسی خواهیم کرد، از کلاسهای مدل گروه محصولات کمک گرفته شده است:
using System.Collections.Generic;
namespace EF_Sample07.DomainClasses { public class Category { public int Id { get; set; } public virtual string Name { get; set; } public virtual string Title { get; set; } public virtual ICollection<Product> Products { get; set; } } }
using System.ComponentModel.DataAnnotations;
namespace EF_Sample07.DomainClasses { public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; }
[ForeignKey("CategoryId")] public virtual Category Category { get; set; } public int CategoryId { get; set; } } }
در کلاس Product، یک خاصیت اضافی به نام CategoryId اضافه شده است که توسط ویژگی ForeignKey، به عنوان کلید خارجی جدول معرفی خواهد شد. از این خاصیت در برنامههای ASP.NET برای مقدار دهی یک کلید خارجی توسط یک DropDownList پر شده با لیست گروهها، استفاده خواهیم کرد.
پیاده سازی الگوی واحد کار
همانطور که در قسمت قبل نیز ذکر شد، DbContext در EF Code first بر اساس الگوی واحد کار تهیه شده است، اما برای به اشتراک گذاشتن آن بین لایههای مختلف برنامه نیاز است یک لایه انتزاعی را برای آن تهیه کنیم، تا بتوان آنرا به صورت خودکار توسط کتابخانههای Dependency Injection یا به اختصار DI در زمان نیاز به استفاده از آن، به کلاسهای استفاده کننده تزریق کنیم. کتابخانهی DI ایی که در این قسمت مورد استفاده قرار میگیرد، کتابخانه معروف StructureMap است. برای دریافت آن میتوانید از Nuget استفاده کنید؛ یا از صفحه اصلی آن در Github : (^).
اینترفیس پایه الگوی واحد کار ما به شرح زیر است:
using System.Data.Entity; using System;
namespace EF_Sample07.DataLayer.Context { public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveChanges(); } }
برای استفاده اولیه آن، تنها تغییری که در برنامه حاصل میشود به نحو زیر است:
using System.Data.Entity; using EF_Sample07.DomainClasses;
namespace EF_Sample07.DataLayer.Context { public class Sample07Context : DbContext, IUnitOfWork { public DbSet<Category> Categories { set; get; } public DbSet<Product> Products { set; get; }
#region IUnitOfWork Members public new IDbSet<TEntity> Set<TEntity>() where TEntity : class { return base.Set<TEntity>(); } #endregion } }
توضیحات:
با کلاس Context در قسمتهای قبل آشنا شدهایم. در اینجا به معرفی کلاسهایی خواهیم پرداخت که در معرض دید EF Code first قرار خواهند گرفت.
DbSetها هم معرف الگوی Repository هستند. کلاس Sample07Context، معرفی الگوی واحد کار یا Unit of work برنامه است.
برای اینکه بتوانیم تعاریف کلاسهای سرویس برنامه را مستقل از تعریف کلاس Sample07Context کنیم، یک اینترفیس جدید را به نام IUnitOfWork به برنامه اضافه کردهایم.
در اینجا کلاس Sample07Context پیاده سازی کننده اینترفیس IUnitOfWork خواهد بود (اولین تغییر).
دومین تغییر هم استفاده از متد base.Set میباشد. به این ترتیب به سادگی میتوان به DbSetهای مختلف در حین کار با IUnitOfWork دسترسی پیدا کرد. به عبارتی ضرورتی ندارد به ازای تک تک DbSetها یکبار خاصیت جدیدی را به اینترفیس IUnitOfWork اضافه کرد. به کمک استفاده از امکانات Generics مهیا، اینبار
uow.Set<Product>
معادل همان db.Products سابق است؛ در حالتیکه از Sample07Context به صورت مستقیم استفاده شود.
همچنین نیازی به پیاده سازی متد SaveChanges نیست؛ زیرا پیاده سازی آن در کلاس DbContext قرار دارد.
استفاده از الگوی واحد کار در کلاسهای لایه سرویس برنامه
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface ICategoryService { void AddNewCategory(Category category); IList<Category> GetAllCategories(); } }
using EF_Sample07.DomainClasses; using System.Collections.Generic;
namespace EF_Sample07.ServiceLayer { public interface IProductService { void AddNewProduct(Product product); IList<Product> GetAllProducts(); } }
لایه سرویس برنامه را با دو اینترفیس جدید شروع میکنیم. هدف از این اینترفیسها، ارائه پیاده سازیهای متفاوت، به ازای ORMهای مختلف است. برای مثال در کلاسهای زیر که نام آنها با Ef شروع شده است، پیاده سازی خاص Ef Code first را تدارک خواهیم دید. این پیاده سازی، قابل انتقال به سایر ORMها نیست چون نه پیاده سازی یکسانی را از مباحث LINQ ارائه میدهند و نه متدهای الحاقی همانندی را به همراه دارند و نه اینکه مباحث نگاشت کلاسهای آنها به جداول مختلف یکی است:
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfCategoryService : ICategoryService { IUnitOfWork _uow; IDbSet<Category> _categories; public EfCategoryService(IUnitOfWork uow) { _uow = uow; _categories = _uow.Set<Category>(); }
public void AddNewCategory(Category category) { _categories.Add(category); }
public IList<Category> GetAllCategories() { return _categories.ToList(); } } }
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses;
namespace EF_Sample07.ServiceLayer { public class EfProductService : IProductService { IUnitOfWork _uow; IDbSet<Product> _products; public EfProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); }
public void AddNewProduct(Product product) { _products.Add(product); }
public IList<Product> GetAllProducts() { return _products.Include(x => x.Category).ToList(); } } }
توضیحات:
همانطور که ملاحظه میکنید در هیچکدام از کلاسهای سرویس برنامه، وهله سازی مستقیمی از الگوی واحد کار وجود ندارد. این لایه از برنامه اصلا نمیداند که کلاسی به نام Sample07Context وجود خارجی دارد یا خیر.
همچنین لایه اضافی دیگری را به نام Repository جهت مخفی سازی سازوکار EF به برنامه اضافه نکردهایم. این لایه شاید در نگاه اول برنامه را مستقل از ORM جلوه دهد اما در عمل قابل انتقال نیست و سبب تحمیل سربار اضافی بی موردی به برنامه میشود؛ ORMها ویژگیهای یکسانی را ارائه نمیدهند. حتی در حالت استفاده از LINQ، پیاده سازیهای یکسانی را به همراه ندارند.
بنابراین اگر قرار است برنامه مستقل از ORM کار کند، نیاز است لایه استفاده کننده از سرویس برنامه، با دو اینترفیس IProductService و ICategoryService کار کند و نه به صورت مستقیم با پیاده سازی آنها. به این ترتیب هر زمان که لازم شد، فقط باید پیاده سازیهای کلاسهای سرویس را تغییر داد؛ باز هم برنامه نهایی بدون نیاز به تغییری کار خواهد کرد.
تا اینجا به معماری پیچیدهای نرسیدهایم و اصطلاحا over-engineering صورت نگرفته است. یک اینترفیس بسیار ساده IUnitOfWork به برنامه اضافه شده؛ در ادامه این اینترفیس به کلاسهای سرویس برنامه تزریق شده است (تزریق وابستگی در سازنده کلاس). کلاسهای سرویس ما «میدانند» که EF وجود خارجی دارد و سعی نکردهایم توسط لایه اضافی دیگری آنرا مخفی کنیم. شیوه کار با IDbSet تعریف شده دقیقا همانند روال متداولی است که با EF Code first کار میشود و بسیار طبیعی جلوه میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه کنسول ویندوزی
در ادامه برای وهله سازی اینترفیسهای سرویس و واحد کار برنامه، از کتابخانه StructureMap که یاد شد، استفاده خواهیم کرد. بنابراین، تمام برنامههای نهایی ارائه شده در این قسمت، ارجاعی را به اسمبلی StructureMap.dll نیاز خواهند داشت.
کدهای برنامه کنسول مثال جاری را در ادامه ملاحظه خواهید کرد:
using System.Collections.Generic; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07 { class Program { static void Main(string[] args) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>());
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().CacheBy(InstanceScope.Hybrid).Use<Sample07Context>(); x.For<ICategoryService>().Use<EfCategoryService>(); });
var uow = ObjectFactory.GetInstance<IUnitOfWork>(); var categoryService = ObjectFactory.GetInstance<ICategoryService>();
var product1 = new Product { Name = "P100", Price = 100 }; var product2 = new Product { Name = "P200", Price = 200 }; var category1 = new Category { Name = "Cat100", Title = "Title100", Products = new List<Product> { product1, product2 } }; categoryService.AddNewCategory(category1); uow.SaveChanges(); } } }
در اینجا بیشتر هدف، معرفی نحوه استفاده از StructureMap است.
ابتدا توسط متد ObjectFactory.Initialize مشخص میکنیم که اگر برنامه نیاز به اینترفیس IUnitOfWork داشت، لطفا کلاس Sample07Context را وهله سازی کرده و مورد استفاده قرار بده. اگر ICategoryService مورد استفاده قرار گرفت، وهله مورد نظر باید از کلاس EfCategoryService تامین شود.
توسط ObjectFactory.GetInstance نیز میتوان به وهلهای از این کلاسها دست یافت و نهایتا با فراخوانی uow.SaveChanges میتوان اطلاعات را ذخیره کرد.
چند نکته:
- به کمک کتابخانه StructureMap، تزریق IUnitOfWork به سازنده کلاس EfCategoryService به صورت خودکار انجام میشود. اگر به کدهای فوق دقت کنید ما فقط با اینترفیسها مشغول به کار هستیم، اما وهلهسازیها در پشت صحنه انجام میشود.
- حین معرفی IUnitOfWork از متد CacheBy با پارامتر InstanceScope.Hybrid استفاده شده است. این enum مقادیر زیر را میتواند بپذیرد:
public enum InstanceScope { PerRequest = 0, Singleton = 1, ThreadLocal = 2, HttpContext = 3, Hybrid = 4, HttpSession = 5, HybridHttpSession = 6, Unique = 7, Transient = 8, }
برای مثال اگر در برنامهای نیاز داشتید یک کلاس به صورت Singleton عمل کند، فقط کافی است نحوه کش شدن آنرا تغییر دهید.
حالت PerRequest در برنامههای وب کاربرد دارد (و حالت پیش فرض است). با انتخاب آن وهله سازی کلاس مورد نظر به ازای هر درخواست رسیده انجام خواهد شد.
در حالت ThreadLocal، به ازای هر Thread، وهلهای متفاوت در اختیار مصرف کننده قرار میگیرد.
با انتخاب حالت HttpContext، به ازای هر HttpContext ایجاد شده، کلاس معرفی شده یکبار وهله سازی میگردد.
حالت Hybrid ترکیبی است از حالتهای HttpContext و ThreadLocal. اگر برنامه وب بود، از HttpContext استفاده خواهد کرد در غیراینصورت به ThreadLocal سوئیچ میکند.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET MVC
یک برنامه خالی ASP.NET MVC را آغاز کنید. سپس یک HomeController جدید را نیز به آن اضافه نمائید و کدهای آنرا مطابق اطلاعات زیر تغییر دهید:
using System.Web.Mvc; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer; using EF_Sample07.DataLayer.Context; using System.Collections.Generic;
namespace EF_Sample07.MvcAppSample.Controllers { public class HomeController : Controller { IProductService _productService; ICategoryService _categoryService; IUnitOfWork _uow; public HomeController(IUnitOfWork uow, IProductService productService, ICategoryService categoryService) { _productService = productService; _categoryService = categoryService; _uow = uow; }
[HttpGet] public ActionResult Index() { var list = _productService.GetAllProducts(); return View(list); }
[HttpGet] public ActionResult Create() { ViewBag.CategoriesList = new SelectList(_categoryService.GetAllCategories(), "Id", "Name"); return View(); }
[HttpPost] public ActionResult Create(Product product) { if (this.ModelState.IsValid) { _productService.AddNewProduct(product); _uow.SaveChanges(); }
return RedirectToAction("Index"); }
[HttpGet] public ActionResult CreateCategory() { return View(); }
[HttpPost] public ActionResult CreateCategory(Category category) { if (this.ModelState.IsValid) { _categoryService.AddNewCategory(category); _uow.SaveChanges(); }
return RedirectToAction("Index"); } } }
نکته مهم این کنترلر، تزریق وابستگیها در سازنده کلاس کنترلر است؛ به این ترتیب کنترلر جاری نمیداند که با کدام پیاده سازی خاصی از این اینترفیسها قرار است کار کند.
اگر برنامه را به همین نحو اجرا کنیم، موتور ASP.NET MVC ایراد خواهد گرفت که یک کنترلر باید دارای سازندهای بدون پارامتر باشد تا من بتوانم به صورت خودکار وهلهای از آنرا ایجاد کنم. برای رفع این مشکل از کتابخانه StructureMap برای تزریق خودکار وابستگیها کمک خواهیم گرفت:
using System; using System.Data.Entity; using System.Web.Mvc; using System.Web.Routing; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.MvcAppSample
{ // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801
public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); }
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
protected void Application_Start() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); initStructureMap(); }
private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>(); });
//Set current Controller factory as StructureMapControllerFactory ControllerBuilder.Current.SetControllerFactory(new StructureMapControllerFactory()); }
protected void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); } }
public class StructureMapControllerFactory : DefaultControllerFactory { protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType) { return ObjectFactory.GetInstance(controllerType) as Controller; } } }
توضیحات:
کدهای فوق متعلق به کلاس Global.asax.cs هستند. در اینجا در متد Application_Start، متد initStructureMap فراخوانی شده است.
با پیاده سازی ObjectFactory.Initialize در کدهای برنامه کنسول معرفی شده آشنا شدیم. اینبار فقط حالت کش شدن کلاس Context برنامه را HttpContextScoped قرار دادهایم تا به ازای هر درخواست رسیده یک بار الگوی واحد کار وهله سازی شود.
نکته مهمی که در اینجا اضافه شدهاست، استفاده از متد ControllerBuilder.Current.SetControllerFactory میباشد. این متد نیاز به وهلهای از نوع DefaultControllerFactory دارد که نمونهای از آنرا در کلاس StructureMapControllerFactory مشاهده میکنید. به این ترتیب در زمان وهله سازی خودکار یک کنترلر، اینبار StructureMap وارد عمل شده و وابستگیهای برنامه را مطابق تعاریف ObjectFactory.Initialize ذکر شده، به سازنده کلاس کنترلر تزریق میکند.
همچنین در متد Application_EndRequest با فراخوانی ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects از نشتی اتصالات به بانک اطلاعاتی جلوگیری خواهیم کرد. چون وهله الگوی کار برنامه HttpScoped تعریف شده، در پایان یک درخواست به صورت خودکار توسط StructureMap پاکسازی میشود و به نشتی منابع نخواهیم رسید.
استفاده از الگوی واحد کار و کلاسهای سرویس تهیه شده در یک برنامه ASP.NET Web forms
در یک برنامه ASP.NET Web forms نیز میتوان این مباحث را پیاده سازی کرد:
using System; using System.Data.Entity; using EF_Sample07.DataLayer.Context; using EF_Sample07.ServiceLayer; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class Global : System.Web.HttpApplication { private static void initStructureMap() { ObjectFactory.Initialize(x => { x.For<IUnitOfWork>().HttpContextScoped().Use(() => new Sample07Context()); x.ForRequestedType<ICategoryService>().TheDefaultIsConcreteType<EfCategoryService>(); x.ForRequestedType<IProductService>().TheDefaultIsConcreteType<EfProductService>();
x.SetAllProperties(y=> { y.OfType<IUnitOfWork>(); y.OfType<ICategoryService>(); y.OfType<IProductService>(); }); }); }
void Application_Start(object sender, EventArgs e) { Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>()); HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize(); initStructureMap(); }
void Application_EndRequest(object sender, EventArgs e) { ObjectFactory.ReleaseAndDisposeAllHttpScopedObjects(); }
در اینجا کدهای کلاس Global.asax.cs را ملاحظه میکنید. توضیحات آن با قسمت ASP.NET MVC آنچنان تفاوتی ندارد و یکی است. البته منهای تعاریف SetAllProperties که جدید است و در ادامه به علت اضافه کردن آنها خواهیم رسید.
در ASP.NET Web forms برخلاف ASP.NET MVC نیاز است کار وهله سازی اینترفیسها را به صورت دستی انجام دهیم. برای این منظور و کاهش کدهای تکراری برنامه میتوان یک کلاس پایه را به نحو زیر تعریف کرد:
using System.Web.UI; using StructureMap;
namespace EF_Sample07.WebFormsAppSample { public class BasePage : Page { public BasePage() { ObjectFactory.BuildUp(this); } } }
سپس برای استفاده از آن خواهیم داشت:
using System; using EF_Sample07.DataLayer.Context; using EF_Sample07.DomainClasses; using EF_Sample07.ServiceLayer;
namespace EF_Sample07.WebFormsAppSample { public partial class AddProduct : BasePage { public IUnitOfWork UoW { set; get; } public IProductService ProductService { set; get; } public ICategoryService CategoryService { set; get; }
protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { bindToCategories(); } }
private void bindToCategories() { ddlCategories.DataTextField = "Name"; ddlCategories.DataValueField = "Id"; ddlCategories.DataSource = CategoryService.GetAllCategories(); ddlCategories.DataBind(); }
protected void btnAdd_Click(object sender, EventArgs e) { var product = new Product { Name = txtName.Text, Price = int.Parse(txtPrice.Text), CategoryId = int.Parse(ddlCategories.SelectedItem.Value) }; ProductService.AddNewProduct(product); UoW.SaveChanges(); Response.Redirect("~/Default.aspx"); } } }
اینبار وابستگیهای کلاس افزودن محصولات، به صورت خواصی عمومی تعریف شدهاند. این خواص عمومی توسط متد SetAllProperties که در فایل global.asax.cs معرفی شدند، باید یکبار تعریف شوند (مهم!).
سپس اگر دقت کرده باشید، اینبار کلاس AddProduct از BasePage ما ارث بری کرده است. در سازند کلاس BasePage، با فراخوانی متد ObjectFactory.BuildUp، تزریق وابستگیها به خواص عمومی کلاس جاری صورت میگیرد.
در ادامه نحوه استفاده از این اینترفیسها را جهت مقدار دهی یک DropDownList یا ذخیره سازی اطلاعات یک محصول مشاهده میکنید. در اینجا نیز کار با اینترفیسها انجام شده و کلاس جاری دقیقا نمیداند که با چه وهلهای مشغول به کار است. تنها در زمان اجرا است که توسط StructureMap ، به ازای هر اینترفیس معرفی شده، وهلهای مناسب بر اساس تعاریف فایل Global.asax.cs در اختیار برنامه قرار میگیرد.
کدهای کامل مثالهای این سری را از آدرس زیر هم میتوانید دریافت کنید: (^)
به روز رسانی
کدهای قسمت جاری را به روز شده جهت استفاده از EF 6 و StructureMap 3 در VS 2013، از اینجا میتوانید دریافت کنید:
EF_Sample07