public class UseAmpImageAttribute : ActionFilterAttribute { private HtmlTextWriter _htmlTextWriter; private StringWriter _stringWriter; private StringBuilder _stringBuilder; private HttpWriter _output; public override void OnActionExecuting(ActionExecutingContext filterContext) { _stringBuilder = new StringBuilder(); _stringWriter = new StringWriter(_stringBuilder); _htmlTextWriter = new HtmlTextWriter(_stringWriter); _output = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output; filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter; } public override void OnResultExecuted(ResultExecutedContext filterContext) { var response = _stringBuilder.ToString(); // Change images to Amp-specific images response = UpdateAmpImages(response); // For AMP pages, change Script content to a link. response = ReplaceWithLink("script", response); // For AMP pages, change iFrame content to a link. response = ReplaceWithLink("iframe", response); _output.Write(response); } private string ReplaceWithLink(string tag, string response) { var doc = GetHtmlDocument(response); var elements = doc.DocumentNode.Descendants(tag); foreach (var htmlNode in elements) { if (htmlNode.Attributes["data-link"] == null) continue; var dataLink = htmlNode.Attributes["data-link"].Value; var paragraph = doc.CreateElement("p"); var text = String.Format("[Embedded Link] {0}", dataLink); var anchor = doc.CreateElement("a"); anchor.InnerHtml = text; anchor.Attributes.Add("href", dataLink); anchor.Attributes.Add("title", text); paragraph.InnerHtml = anchor.OuterHtml; var original = htmlNode.OuterHtml; var replacement = paragraph.OuterHtml; response = response.Replace(original, replacement); } return response; } private string UpdateAmpImages(string response) { // Use HtmlAgilityPack (install-package HtmlAgilityPack) var doc = GetHtmlDocument(response); var imageList = doc.DocumentNode.Descendants("img"); const string ampImage = "amp-img"; if (!imageList.Any()) return response; if (!HtmlNode.ElementsFlags.ContainsKey("amp-img")) { HtmlNode.ElementsFlags.Add("amp-img", HtmlElementFlag.Closed); } foreach (var imgTag in imageList) { var original = imgTag.OuterHtml; var replacement = imgTag.Clone(); replacement.Name = ampImage; replacement.Attributes.Remove("caption"); response = response.Replace(original, replacement.OuterHtml); } return response; } private HtmlDocument GetHtmlDocument(string htmlContent) { var doc = new HtmlDocument { OptionOutputAsXml = true, OptionDefaultStreamEncoding = Encoding.UTF8 }; doc.LoadHtml(htmlContent); return doc; } }
ASP.NET MVC #6
- برای طراحی CSS بین برنامه نویسهای دات نت، فریم ورکی به نام LESS خیلی محبوبیت دارد (^).
- کتابخانههای جاوا اسکریپتی سمت کلاینت معنا پیدا میکنند. بنابراین آنچنان وارد بحث ASP.NET که سمت سرور است نمیشوند مگر اینکه کمک حالی در این رابطه باشند مانند jQuery. یا البته جاوا اسکریپت سمت سرور هم به نام نودجیاس وجود دارد که بحث دیگری است. در کل حین کار با جاوا اسکریپت دست بازتر است چون داخل مرورگر کاربر اجرا میشود که stateful است (مثل برنامههای سیلورلایت). به همین جهت کتابخانه MVVM هم برای جاوا اسکریپت وجود دارد (^). جالب اینجا است که این کتابخانه MVVM توسط یکی از اعضای تیم ASP.NET MVC طراحی شده.
معرفی JSON Web Token
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
@Html.RenderInput( Attributes.Configure() .AddType("text") .AddId("UserId") .AddName("UserId") .AddCssClass("TxtBoxCssClass") )
<input class="TxtBoxCssClass" id="UserId" name="UserId" type="text">
public class Attributes : RouteValueDictionary { /// <summary> /// Configures this instance. /// </summary> /// <returns></returns> public static Attributes Configure() { return new Attributes(); } /// <summary> /// Adds the type. /// </summary> /// <param name="value">The value.</param> public Attributes AddType(string value) { this.Add("type", value); return this; } /// <summary> /// Adds the name. /// </summary> /// <param name="value">The value.</param> public Attributes AddName(string value) { this.Add("name", value); return this; } /// <summary> /// Adds the id. /// </summary> /// <param name="value">The value.</param> public Attributes AddId(string value) { this.Add("id", value); return this; } /// <summary> /// Adds the value. /// </summary> /// <param name="value">The value.</param> public Attributes AddValue(string value) { this.Add("value", value); return this; } /// <summary> /// Adds the CSS class. /// </summary> /// <param name="value">The value.</param> public Attributes AddCssClass(string value) { this.Add("class", value); return this; } }
public static MvcHtmlString RenderInput(this HtmlHelper htmlHelper, Attributes attributes) { TagBuilder input = new TagBuilder("input"); input.MergeAttributes(attributes); return new MvcHtmlString(input.ToString(TagRenderMode.SelfClosing)); }
namespace SimpleFormGenerator.DomainClasses { public class Form { public int Id { get; set; } public string Title { get; set; } public virtual ICollection<Field> Fields { get; set; } } public class Field { public int Id { get; set; } public string TitleEn { get; set; } public string TitleFa { get; set; } public FieldType FieldType { get; set; } public virtual Form Form { get; set; } public int FormId { get; set; } } public enum FieldType { Button, Checkbox, File, Hidden, Image, Password, Radio, Reset, Submit, Text } }
namespace SimpleFormGenerator.DataLayer.Context { public class SimpleFormGeneratorContext : DbContext, IUnitOfWork { public SimpleFormGeneratorContext() : base("SimpleFormGenerator") {} public DbSet<Form> Forms { get; set; } public DbSet<Field> Fields { get; set; } public DbSet<Value> Values { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Value>() .HasRequired(d => d.Form) .WithMany() .HasForeignKey(d => d.FormId) .WillCascadeOnDelete(false); } } }
@model SimpleFormGenerator.DomainClasses.Form @{ ViewBag.Title = "صفحه ایجاد یک فرم"; } @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div> <span>عنوان</span> <div> @Html.EditorFor(model => model.Title, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.Title, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="ذخیره" /> </div> </div> </div> } <div> @Html.ActionLink("بازگشت", "Index") </div>
@model SimpleFormGenerator.DomainClasses.Field @{ ViewBag.Title = "CreateField"; } @using (Html.BeginForm()) { @Html.AntiForgeryToken() <div> <hr /> @Html.ValidationSummary(true, "", new { @class = "text-danger" }) <div> <span>عنوان انگلیسی</span> <div> @Html.EditorFor(model => model.TitleEn, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.TitleEn, "", new { @class = "text-danger" }) </div> </div> <div> <span>عنوان فارسی</span> <div> @Html.EditorFor(model => model.TitleFa, new { htmlAttributes = new { @class = "form-control" } }) @Html.ValidationMessageFor(model => model.TitleFa, "", new { @class = "text-danger" }) </div> </div> <div> <span>نوع فیلد</span> <div> @Html.EnumDropDownListFor(model => model.FieldType, htmlAttributes: new { @class = "form-control" }) @Html.ValidationMessageFor(model => model.FieldType, "", new { @class = "text-danger" }) </div> </div> <div> <span>فرم</span> <div> @Html.DropDownList("FormId", (SelectList)ViewBag.FormList) @Html.ValidationMessageFor(model => model.FormId, "", new { @class = "text-danger" }) </div> </div> <div> <div> <input type="submit" value="ذخیره" /> </div> </div> </div> } <div> @Html.ActionLink("بازگشت ", "Index") </div>
@using SimpleFormGenerator.DomainClasses @model IEnumerable<SimpleFormGenerator.DomainClasses.Field> @{ ViewBag.Title = "نمایش فرم"; } <div> <div> <div> @using (Html.BeginForm()) { @Html.AntiForgeryToken() for (int i = 0; i < Model.Count(); i++) { if (Model.ElementAt(i).FieldType == FieldType.Text) { <text> <input type="hidden" name="[@i].FieldType" value="@Model.ElementAt(i).FieldType" /> <input type="hidden" name="[@i].Id" value="@Model.ElementAt(i).Id" /> <input type="hidden" name="[@i].FormId" value="@Model.ElementAt(i).FormId" /> <div> <label>@Model.ElementAt(i).TitleFa</label> <div> <input type="text" name="[@i].TitleEn" /> </div> </div> </text> } } <div data-formId ="@ViewBag.FormId"> <div> <input type="submit" value="ارسال فرم" /> </div> </div> } </div> <div> @Html.ActionLink("بازگشت", "Index") </div> </div> </div>
for (int i = 0; i < Model.Count(); i++) { // code }
if (Model.ElementAt(i).FieldType == FieldType.Text) { // code }
<input type="text" name="[@i].TitleEn" />
[@i].FieldTyp
public class Value { public int Id { get; set; } public string Val { get; set; } public virtual Field Field { get; set; } [ForeignKey("Field")] public int FieldId { get; set; } public virtual Form Form { get; set; } [ForeignKey("Form")] public int FormId { get; set; } }
[HttpPost] public ActionResult ShowForm(IEnumerable<Field> values) { if (ModelState.IsValid) { foreach (var value in values) { _valueService.AddValue(new Value { Val = value.TitleEn, FormId = value.FormId, FieldId = value.Id}); _uow.SaveAllChanges(); } } return View(values); }
دستکاری mime types در IIS
و اگر لازم باشه بر اساس پسوند هم میتونند ...
var container = new UnityContainer();
container.RegisterType<ISomeService, SomeService>(new PerRequestLifetimeManager()); container.RegisterType<ISomeBusiness, SomeBusiness>(new PerRequestLifetimeManager()); container.RegisterType<ISomeController, SomeController>(new PerRequestLifetimeManager());
DependencyResolver.SetResolver(new UnityDependencyResolver(container));
var someService=(ISomeService)DependencyResolver.Current.GetService(typeof(ISomeService)); var data=someService.GetData();
در لایه سرویس یک کلاس Service Factory داریم که قرار است همه سرویسها، برای برقراری ارتباط با یکدیگر از آن استفاده کنند.این کلاس معمولا در لایه سرویس به اشکال گوناگونی پیاده سازی میشود که کارش وهه سازی از Interfaceهای درخواستی است. اما برای یکپارچه کردن آن با Unity من آنرا به شکل زیر پیاده سازی کرده ام
public class ServiceFactory : MarshalByRefObject { static IUnityContainer uContainer = new UnityContainer(); public static Type DataContextType { get; set; } public static void Initialise(IUnityContainer unityContainer, Type dbContextType) { uContainer = unityContainer; DataContextType = dbContextType; uContainer.RegisterType(typeof(BaseDataContext), DataContextType, new HierarchicalLifetimeManager()); } public static T Create<T>() { return (T)Activator.CreateInstance<T>(); } public static T Create<T>(string fullTypeName) { return (T)System.Reflection.Assembly.GetExecutingAssembly().CreateInstance(fullTypeName); } public static T Create<T>(Type entityType) { return (T)Activator.CreateInstance(entityType); } public static dynamic Create(Type entityType) { return Activator.CreateInstance(entityType); } public static T Get<T>() { return uContainer.Resolve<T>(); } public static object Get(Type type) { return uContainer.Resolve(type); } }
در این کلاس ما بجای ایجاد داینامیک آبجکتها، از Unity استفاده کردهایم. در همان ابتدا که برنامهی وب ما برای اولین بار اجرا میشود و بعد از Register کردن کلاسها، میتوانیم container را به صورت پارامتر سازنده به کلاس Service Factory ارسال کنیم. به این ترتیب برای استفاده از سرویسها در لایه Business از Unity بهره میبریم.
البته استفاده از Unity برای DataContext خیلی منطقی نیست و بهتر است نوع DataContext را در ابتدا بگیریم و هرجا نیاز داشتیم با استفاده از متد Create از آن وهله سازی بکنیم.