ابزارهای زیادی برای محافظت و یا فشرده سازی و رمزنگاری اسمبلیهای دات نت موجود هستند که اکثر آنها تجاری هستند. برنامه netz نمونهای است سورس باز و رایگان که تنها کار فشرده سازی اسمبلی موجود را انجام میدهد. همچنین با استفاده از آن سورس اسمبلی شما بهوسیله برنامه reflector قابل مرور نخواهد بود. هر چند این برنامه سورس باز است و امکان unpack کردن نتیجه آن نیز احتمالا با اندکی سعی میسر خواهد بود اما باز هم یک مرحله پیشرفت محسوب میشود! خصوصا اینکه میتوان برای آن Custom Compression Provider نوشت و برای مثال فایل زیپ شده نهایی را رمزنگاری نیز کرد.
قبل از عمل:
بعد از عمل:
نحوه استفاده:
فشردن کردن یک فایل exe توسط آن
netz app.exe
الحاق کردن فایل zip.dll همراه با فایل exe (بدون نیاز به توزیع فایل zip.dll):
netz -z app.exe
یکی کردن تمام dll های برنامه با فایل exe در قالب یک فایل نهایی:
netz -s app.exe lib1.dll lib2.dll
نکته:
در اینجا به صورت پیش فرض از فایل zip.dll برای فشرده سازی استفاده میشود (که برای تمام نگارشهای دات نت قابل استفاده است). در نگارشهای جدید دات نت، فشرده سازی نیز به کلاسهای استاندارد اضافه شده است که امکان استفاده از آن نیز در اینجا مهیا است (و دیگر نیازی به استفاده از zip.dll آن نخواهد بود).
netz.exe -r net20comp.dll app.exe
نحوه برنامه نویسی یک compression provider سفارشی برای آن در آدرس زیر توضیح داده شده است. (اعمال موارد امنیتی دلخواه و استفاده از آن)
http://madebits.com/netz/compress.php
و موارد دیگری که در راهنمای سایت آن توضیح داده شدهاند.
معرفی Babel Obfuscator
با این نوع ابزارها میشود درک کد حاصل شده را مشکل و آنرا تقریبا بدون استفاده کرد.
روش استفادهی از jQuery نیز در حالت کلی همانند مطلب «استفاده از کتابخانههای ثالث جاوا اسکریپتی در برنامههای AngularJS 2.0» است؛ اما به همراه چند نکتهی اضافهتر مانند محل فراخوانی و دسترسی به DOM، در کدهای یک کامپوننت.
هدف: استفاده از کتابخانهی Chosen
میخواهیم جهت غنیتر کردن ظاهر یک دراپ داون در برنامههای AngularJS 2.0، از یک افزونهی بسیار معروف jQuery به نام Chosen استفاده کنیم.
تامین پیشنیازهای اولیه
میتوان فایلهای این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است اینکار را توسط bower مدیریت کنیم. این کتابخانه هنوز دارای بستهی رسمی npm نیست (و بستهی chosen-npm که در مخزن npm وجود دارد، توسط این تیم ایجاد نشدهاست). اما همانطور که در مستندات آن نیز آمدهاست، توسط دستور ذیل نصب میشود:
bower install chosen
در اینجا نام پیش فرض bower.json را پذیرفته و سپس محتوای فایل ایجاد شده را به نحو ذیل تغییر دهید:
{ "name": "asp-net-mvc5x-angular2x", "version": "1.0.0", "authors": [ "DNT" ], "license": "MIT", "ignore": [ "node_modules", "bower_components" ], "dependencies": { "chosen": "1.4.2" }, "devDependencies": { } }
پس از دریافت خودکار chosen، بستهی آنرا در مسیر bower_components\chosen واقع در ریشهی پروژه میتوانید مشاهده کنید.
استفاده از jQuery و chosen به صورت untyped
سادهترین و متداولترین روش استفاده از jQuery و افزونههای آن شامل موارد زیر هستند:
الف) تعریف مداخل مرتبط با آنها در فایل index.html
<script src="~/node_modules/jquery/dist/jquery.min.js"></script> <script src="~/node_modules/bootstrap/dist/js/bootstrap.min.js"></script> <script src="~/bower_components/chosen/chosen.jquery.min.js"></script> <link href="~/bower_components/chosen/chosen.min.css" rel="stylesheet" type="text/css" />
ب) تعریف jQuery به صورت untyped
declare var jQuery: any;
محل صحیح فراخوانی متدهای مرتبط با jQuery
در تصویر ذیل، چرخهی حیات یک کامپوننت را مشاهده میکنید که با تعدادی از آنها پیشتر آشنا شدهایم:
روش متداول استفاده از jQuery، فراخوانی آن پس از رخداد document ready است. در اینجا معادل این رخداد، hook ویژهای به نام ngAfterViewInit است. بنابراین تمام فراخوانیهای jQuery را باید در این متد انجام داد.
همچنین جیکوئری نیاز دارد تا بداند هم اکنون قرار است با چه المانی کار کنیم و کامپوننت بارگذاری شده کدام است. برای این منظور، یکی از سرویسهای توکار AngularJS 2.0 را به نام ElementRef، به سازندهی کلاس تزریق میکنیم. توسط خاصیت this._el.nativeElement آن میتوان به المان ریشهی کامپوننت جاری دسترسی یافت.
constructor(private _el: ElementRef) { }
تهیهی کامپوننت نمایش یک دراپ داون مزین شده با chosen
در ادامه قصد داریم نکاتی را که تاکنون مرور کردیم، به صورت یک مثال پیاده سازی کنیم. به همین جهت فایل جدید using-jquery-addons.component.ts را به پروژه اضافه کنید به همراه فایل قالب آن به نام using-jquery-addons.component.html؛ با این محتوا:
الف) کامپوننت using-jquery-addons.component.ts
import { Component, ElementRef, AfterViewInit } from "@angular/core"; declare var jQuery: any; // untyped @Component({ templateUrl: "app/using-jquery-addons/using-jquery-addons.component.html" }) export class UsingJQueryAddonsComponent implements AfterViewInit { dropDownItems = ["First", "Second", "Third"]; selectedValue = "Second"; constructor(private _el: ElementRef) { } ngAfterViewInit() { jQuery(this._el.nativeElement) .find("select") .chosen() .on("change", (e, args) => { this.selectedValue = args.selected; }); } }
ب) قالب using-jquery-addons.component.html
<select> <option *ngFor="let item of dropDownItems" [value]="item" [selected]="item == selectedValue"> {{item}} option </option> </select> <h4> {{selectedValue}}</h4>
توضیحات
کلاس UsingJQueryAddonsComponent، اینترفیس AfterViewInit را پیاده سازی کردهاست؛ تا توسط متد ngAfterViewInit بتوانیم با عناصر DOM کار کنیم. هرچند در کل اینکار باید صرفا محدود شود به مواردی مانند مثال جاری و در حد آغاز یک افزونهی jQuery و اگر قرار است تغییراتی دیگری صورت گیرند بهتر است از همان روش binding توکار AngularJS 2.0 استفاده کرد.
در سازندهی کلاس، سرویس ElementRef تزریق شدهاست تا توسط خاصیت this._el.nativeElement آن بتوان به المان ریشهی کامپوننت جاری دسترسی یافت. به همین جهت است که پس از آن از متد find، برای یافتن دراپ داون استفاده شدهاست و سپس chosen به نحو متداولی به آن اعمال گردیدهاست.
در اینجا هر زمانیکه یکی از آیتمهای دراپ داون انتخاب شوند، مقدار آن به خاصیت selectedValue انتساب داده شده و این انتساب سبب فعال سازی binding و نمایش مقدار آن در ذیل دراپ داون میگردد.
در قالب این کامپوننت هم با استفاده از ngFor، عناصر دراپ داون از آرایهی dropDownItems تعریف شده در کلاس کامپوننت، تامین میشوند. متغیر محلی item تعریف شدهی در اینجا، در محدودی همین المان قابل دسترسی است. برای مثال از آن جهت تنظیم دومین آیتم لیست به صورت انتخاب شده، در حین اولین بار نمایش view استفاده شدهاست.
استفاده از jQuery و chosen به صورت typed
کتابخانهی jQuery در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jquery دارای فایل d.ts. خاص خود است. برای نصب آن میتوان از روش ذیل استفاده کرد:
npm install -g typings typings install jquery --save --ambient
/// <reference path="../typings/browser/ambient/jquery/index.d.ts" />
در ادامه به فایل using-jquery-addons.component.ts مراجعه کرده و تغییر ذیل را اعمال کنید:
// declare var jQuery: any; // untyped declare var jQuery: JQueryStatic; // typed
interface JQuery { //... chosen(options?:any):JQuery; } declare module "jquery" { export = $; }
کدهای کامل این پروژه را از اینجا میتوانید دریافت کنید.
در خیلی از مواقع workflowها به مرحلهای میرسند که احتیاج به دستوری از بیرون از فرآیند دارند. در هنگام انتظار، اگر به هر دلیلی workflow از حافظه حذف شود، امکان ادامه فرآیند وجود ندارد. اما میتوان با Persist (ذخیره) کردن آن، در زمان انتظار و فراخوانی مجدد آن در هنگام نیاز، این ریسک را برطرف نمود.
قصد دارم با این مثال، طریقه persist شدن یک workflow در زمانیکه نیاز به انتظار برای تایید دارد و فراخوانی آن از همان نقطه پس از تایید مربوطه را توضیح دهم.
ساختار اینترفیس کاربری ما WPF میباشد. پس در ابتدا یک پروژه از نوع WPF ایجاد میکنیم. اسم solution را PersistWF و اسم Project را PersistWF.UI انتخاب میکنیم.
در پروژه UI نام فایل MainWindow.xaml را به AddRequest.xaml تغییر میدهیم. همچنین اسم کلاس مربوطه را در codebehind
همین طور مقدار StartupUri را هم در app.xaml اصلاح میکنیم
StartupUri="AddRequest.xaml"
Reference های زیر رو هم به پروژه اضافه میکنیم
•System.Activities •System.Activities.DurableInstancing •System.Configuration •System.Data.Linq •System.Runtime.DurableInstancing •System.ServiceModel •System.ServiceModel.Activities •System.Workflow.ComponentModel •System.Runtime.DurableInstancing •System.Activities.DurableInstancing
قرار است کاربری ثبت نام کند، در فرایند ثبت، منتظر تایید یکی از مدیران قرار میگیرد. مدیر، لیست کاربران جدید را میبینید، یک کاربر را انتخاب میکند؛ مقادیر لازم را وارد میکند و سپس پروسه تایید را انجام میدهد که فراخوانی فرآیند مربوطه از همان قسمتیاست که منتظر تایید مانده است.
برای Persist کردن workflow از کلاس SqlWorkflowInstanceStore استفاده میکنم. این شی به connection ای به یک دیتابیس با یک ساختار معین احتیاج دارد. خوشبختانه اسکریپتهای مورد نیاز این ساختار در پوشه [Drive]:\Windows\Microsoft.NET\Framework\v4.0.30319\SQL\en وجود دارند. دو اسکریپت با نامهای SqlWorkflowInstanceStoreSchema و SqlWorkflowInstanceStoreLogic باید به ترتیب در دیتابیس اجرا شوند.
من یک دیتابیس با نام PersistWF ایجاد میکنم و اسکریپتها را بر روی آن اجرا میکنم. یک جدول هم برای نگهداری کاربران ثبت شده در همین دیتابیس ایجاد میکنم.
و شمایل دیتابیس ما پس از اجرا کردن اسکریپتها و ساختن جدول User بدین شکل است:
XAML زیر، ساختار فرم AddRequest میباشد که قرار است نقش UI برنامه را ایفا کند. آن را با XAMLهای پیش فرض عوض کنید.
<Window x:Class="PersistWF.UI.AddRequest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="520" Width="550" Loaded="Window_Loaded"> <Grid MinWidth="300" MinHeight="100" Width="514"> <Label Height="30" Margin="5,10,10,10" Name="lblName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Name:</Label> <Label Height="30" Margin="270,10,10,10" Name="lblPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Phone Number:</Label> <Label Height="30" Margin="5,40,10,10" Name="lblEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="90" HorizontalContentAlignment="Right">Email:</Label> <TextBox Height="25" Margin="100,10,10,10" Name="txtName" VerticalAlignment="Top" HorizontalAlignment="Left" Width="170" /> <TextBox Height="25" Margin="365,10,10,10" Name="txtPhone" VerticalAlignment="Top" HorizontalAlignment="Left" Width="100" /> <TextBox Height="25" Margin="100,40,10,10" Name="txtEmail" VerticalAlignment="Top" HorizontalAlignment="Left" Width="300" /> <Button Height="23" Margin="100,86,0,0" Name="brnRegister" VerticalAlignment="Top" HorizontalAlignment="Left" Width="70" Click="brnRegister_Click">Register</Button> <ListView x:Name="lstUsers" Margin="10,125,10,10" Height="145" VerticalAlignment="Top" ItemsSource="{Binding}" HorizontalContentAlignment="Center" SelectionChanged="lstUsers_SelectionChanged" > <ListView.View> <GridView> <GridViewColumn Header="Current User" Width="480"> <GridViewColumn.CellTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Name}" Width="110"/> <TextBlock Text="{Binding Phone}" Width="70"/> <TextBlock Text="{Binding Email}" Width="130"/> <TextBlock Text="{Binding Status}" Width="70"/> <TextBlock Text="{Binding AcceptedBy}" Width="100"/> </StackPanel> </DataTemplate> </GridViewColumn.CellTemplate> </GridViewColumn> </GridView> </ListView.View> </ListView> <Label Height="37" HorizontalAlignment="Stretch" Margin="10,272,5,10" Name="lblSelectedNotes" VerticalAlignment="Top" Visibility="Hidden" /> <Label Height="30" Margin="10,0,0,140" Name="lblAgent" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="40" HorizontalContentAlignment="Left" Visibility="Hidden">Admin Name:</Label> <TextBox Height="25" Margin="60,0,0,140" Name="txtAcceptedBy" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="190" Visibility="Hidden" /> <Button Height="25" Margin="270,0,0,140" Name="btnAccept" VerticalAlignment="Bottom" HorizontalAlignment="Left" Width="90" Click="btnAccept_Click" Visibility="Hidden">Accept</Button> <Label Height="27" HorizontalAlignment="Left" Margin="10,0,0,110" Name="lblEvent" VerticalAlignment="Bottom" Width="76">Event Log</Label> <ListBox Margin="12,0,5,12" Name="lstEvents" Height="100" VerticalAlignment="Bottom" FontStretch="Condensed" FontSize="10" /> </Grid> </Window>
اگر همه چیز مرتب باشد؛ ساختار فرم شما باید به این شکل
باشد
اکثر workflowها از activity معروف WrteLine استفاده میکنند که برای نمایش یک رشته به کار میرود. ما هم در workflow مثالمان از این Activity استفاده میکنیم. اما برای اینکه مقادیری که توسط این Activity ایجاد میشوند در کادر event log فرم خودمان نمایش داده شود؛ احتیاج داریم که یک TextWriter سفارشی برای خودمان ایجاد کنیم. اما قبل از آن یک کلاس static در پروژه ایجاد میکنیم که بتوانیم در هر قسمتی، به فرم دسترسی داشته باشیم.
کلاسی را با نام ApplicationInterface به پروژه اضافه کرده و یک Property استاتیک از جنس فرم AddRequest هم
برای آن تعریف میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public static class ApplicationInterface { public static AddRequest _app { get; set; } } }
به Constructor کلاس موجود در فایل AddRequest.xaml.cs این خط کد رو اضافه میکنم
public AddRequest() { InitializeComponent(); ApplicationInterface._app = this; }
private void AddEvent(string szText) { lstEvents.Items.Add(szText); } public ListBox GetEventListBox() { return this.lstEvents; }
متد اول برای اضافه کردن یک event Log و متد دوم هم که کنسول لاگ را در اختیار درخواست کنندهاش قرار میدهد.
و حالا کلاس TextWriter سفارشیامان را مینویسیم. یک کلاس به نام ListBoxTextWriter به پروژه اضافه میکنیم که از TextWriter مشتق میشود و محتویات آنرا در زیر میبینید:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Controls; namespace PersistWF.UI { public class ListBoxTextWriter : TextWriter { const string textClosed = "This TextWriter must be opened before use"; private Encoding _encoding; private bool _isOpen = false; private ListBox _listBox; public ListBoxTextWriter() { // Get the static list box _listBox = ApplicationInterface._app.GetEventListBox(); if (_listBox != null) _isOpen = true; } public ListBoxTextWriter(ListBox listBox) { this._listBox = listBox; this._isOpen = true; } public override Encoding Encoding { get { if (_encoding == null) { _encoding = new UnicodeEncoding(false, false); } return _encoding; } } public override void Close() { this.Dispose(true); } protected override void Dispose(bool disposing) { this._isOpen = false; base.Dispose(disposing); } public override void Write(char value) { if (!this._isOpen) throw new ApplicationException(textClosed); ; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value.ToString()))); } public override void Write(string value) { if (!this._isOpen) throw new ApplicationException(textClosed); if (value != null) this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(value))); } public override void Write(char[] buffer, int index, int count) { String toAdd = ""; if (!this._isOpen) throw new ApplicationException(textClosed); ; if (buffer == null || index < 0 || count < 0) throw new ArgumentOutOfRangeException("buffer"); if ((buffer.Length - index) < count) throw new ArgumentException("The buffer is too small"); for (int i = 0; i < count; i++) toAdd += buffer[i]; this._listBox.Dispatcher.BeginInvoke(new Action(() => this._listBox.Items.Add(toAdd))); } } }
همان طور که میبینید کلاس ListBoxTextWriter از کلاس abstract TextWriter مشتق شده و پیاده سازی از متد Write را فراهم میکند تا یک رشته را به کنترل ListBox اضافه کنه. (البته سه تا از این متدها را Override میکنیم تا بتوانیم یک رشته، یک کاراکتر و یا آرایه ای از کاراکترها را به ListBox اضافه کنیم) در constructor پیشفرض از کلاس ApplicationInterface استفاده کردیم تا بتوانیم کنترل lstEvents را از فرم اصلی برنامه به دست بیاوریم. برای Add کردن از Dispatcher و متد BeginInvoke مرتبط با آن استفاده کردیم . این کار، متد را قادر میسازد حتی وقتیکه از یک thread متفاوت فراخوانی میشود، کار کند.
حالا میتوانیم از این کلاس، به عنوان مقدار خاصیت TextWriter برای WriteLine استفاده کنیم.
به کلاس ApplicationInterface برگردیم تا متد زیر را هم به آن اضافه کنیم
public static void AddEvent(String status) { if (_app != null) { new ListBoxTextWriter(_app.GetEventListBox()).WriteLine(status); } }
این هم از constructor دومی استفاده میکنه برای معرفی ListBox.
برای ارتباط با دیتابیس از LINQ to SQL استفاده میکنیم تا User رو ذخیره و بازیابی کنیم. به پروژه یک آیتم از نوع LINQ to SQL با نام UserData.dbml اضافه میکنیم. به دیتابیس متصل شده و جدول User رو به محیط Design میکشیم. در ادامه برای شی کلاس SQLWorkflowInstanceStore هم از همین Connectionstring استفاده میکنیم.
برای ایجاد workflow مورد نظر، به دو Activity سفارشی احتیاج داریم که باید خودمان ایجاد نماییم. یک پوشه با نام Activities به پروژه اضافه میکنم تا کلاسهای مورد نظر را آنجا ایجاد کنیم.
1. یک Activity برای ایجاد User
این Activity تعدادی پارامتر از نوع InArgument دارد که توسط آنها یک Instance از کلاس User ایجاد میکند و در حقیقت آن را به دیتابیس میفرستد و دخیره میکند. Connectionstring را هم میشود توسط یک آرگومان ورودی دیگر مقدار دهی کرد. یک آرگومان خروجی هم برای این Activity در نظر میگیریم تا User ایجاد شده را برگردانیم. روی پوشهی Activities کلیک راست میکنیم و Add - NewItem را انتخاب میکنیم. از لیست workflowها Template مربوط به CodeActivity را انتخاب کرده و یک CodeActivity با نام CreateUser ایجاد میکنیم
محتویات این کلاس را هم مانند زیر کامل میکنیم
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; namespace PersistWF.UI.Activities { public sealed class CreateUser : CodeActivity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public OutArgument<User> User { get; set; } protected override void Execute(CodeActivityContext context) { // ایجاد کاربر User user = new User(); user.Email = Email.Get(context); user.Name = Name.Get(context); user.Phone = Phone.Get(context); user.Status = "New"; user.WorkflowID = context.WorkflowInstanceId; UserDataDataContext db = new UserDataDataContext(ConnectionString.Get(context)); db.Users.InsertOnSubmit(user); db.SubmitChanges(); User.Set(context, user); } } }
متد Execute، توسط مقادیری که به عنوان پارامتر دریافت شده، یک شی از کلاس User ایجاد میکند و به کمک DataContext آنرا در دیتابیس دخیره کرده و در آخر User ذخیره شده را در اختیار پارامتر خروجی قرار میدهد.
1. یک Activity برای انتظار دریافت تایید
این Activity قرار است Workflow را Idle کند تا زمانیکه مدیر دستور تایید را با فراخوانی مجدد workflow از این همین
قسمت صادر نماید.
این Activity باید از NativeActivity مشتق شده و برای اینکه workflow را وادرا به معلق شدن کند کافیاست خاصیت CanInduceIdle را با مقدار برگشتی true , override کنیم.
مثل قسمت قبل یک CodeActivity ایجاد میکنیم. اینبار با نام WaitForAccept که محتویاتش را هم مانند زیر تغییر میدهیم.
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Activities; using System.Workflow.ComponentModel; namespace PersistWF.UI.Activities { public sealed class WaitForAccept<T> : NativeActivity<T> { public WaitForAccept() :base() { } public string BookmarkName { get; set; } public OutArgument<T> Input { get; set; } protected override void Execute(NativeActivityContext context) { context.CreateBookmark(BookmarkName, new BookmarkCallback(this.Continue)); } private void Continue(NativeActivityContext context, Bookmark bookmark, object value) { Input.Set(context, (T)value); } protected override bool CanInduceIdle { get { return true; } } } }
ما User هایی را که به این نقطه رسیدنْ نمایش میدهیم. مدیر اونها را دیده و با مقدار دهی فیلد AcceptedBy، آن User را از اینجا به workflow میفرستد و ما user وارد شده را در ادامهی فرآیند Accept میکنیم.
برای ایجاد workflow هم میتوانید از designer استفاده کنید و هم میتوانید کد مربوط به workflow را پیاده سازی کنید.
برای پیاده سازی از طریق کد، یک کلاس با نام UserWF ایجاد میکنیم و محتویات workflow را مانند زیر پیاده سازی خواهیم کرد:
using PersistWF.UI.Activities; using System; using System.Activities; using System.Activities.Statements; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace PersistWF.UI { public sealed class UserWF : Activity { public InArgument<string> Name { get; set; } public InArgument<string> Email { get; set; } public InArgument<string> Phone { get; set; } public InArgument<string> ConnectionString { get; set; } public InArgument<TextWriter> Writer { get; set; } public UserWF() { Variable<User> User = new Variable<User> { Name = "User" }; this.Implementation = () => new Sequence { DisplayName = "EnterUser", Variables = { User }, Activities = { new CreateUser // 1. ایجاد کاربر با ورود پارامترهای ورودی { ConnectionString = new InArgument<string>(c=> ConnectionString.Get(c)), Email = new InArgument<string>(c=> Email.Get(c)), Name = new InArgument<string>(c=> Name.Get(c)), Phone = new InArgument<string>(c=> Phone.Get(c)), User = new OutArgument<User>(c=> User.Get(c)) }, new WriteLine // 2. لاگ مربوط به دخیره کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Registered and waiting for Accept", Name.Get(c) ) ) }, new InvokeMethod { TargetType = typeof(ApplicationInterface), // 3. برای به روزرسانی لیست کاربران ثبت شده در نمایش فرم MethodName = "NewUser", Parameters = { new InArgument<User>(env => User.Get(env)) } }, new WaitForAccept<User> // 4. اینجا فرایند متوقف میشود و منتظر تایید مدیر میماند { BookmarkName = "GetAcceptes", Input = new OutArgument<User>(env => User.Get(env)) }, new WriteLine // 5. لاگ مربوط به تایید شدن کاربر { TextWriter = new InArgument<TextWriter>(c=> Writer.Get(c)), Text = new InArgument<string>(c=> string.Format("User {0} Accepter by {1}",Name.Get(c),User.Get(c).AcceptedBy)) } } }; } } }
اگر بخوایم از Designer استفاده کنیم. فرایندمان چیزی شبیه شکل زیر خواهد بود
به Application بر میگردیم تا آن را پیاده سازی کنیم. ابتدا به app.config که اتوماتیک ایجاد شده رفته تا اسم Connectionstring رو به UserGenerator تغییر دهیم. محتویات درون app.config به شکل زیر است.
<?xml version="1.0" encoding="utf-8" ?> <configuration> <configSections> </configSections> <connectionStrings> <add name="UserGenerator" connectionString="Data Source=.;Initial Catalog=PersistWF;Integrated Security=True" providerName="System.Data.SqlClient" /> </connectionStrings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /> </startup> </configuration>
در کلاس AddRequest کد زیر را اضافه میکنم. برای نگهداری مقدار connectionstring
private string _connectionString = "";
همچنین کدهای زیر را به رویداد Load فرم اضافه میکنم تا مقدار ConnectionString را از Config بخوانم:
Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); ConnectionStringsSection css = (ConnectionStringsSection)config.GetSection("connectionStrings"); _connectionString = css.ConnectionStrings["UserGenerator"].ConnectionString;
خط زیر را هم به کلاس AddRequest اضافه نمایید.
private InstanceStore _instanceStore;
این ارجاعیه به کلاس InstanceStore که برای Persist و Load کردن workflow از آن استفاده میکنیم و کدهای زیر را هم به رویداد Load فرم اضافه میکنیم.
_instanceStore = new SqlWorkflowInstanceStore(_connectionString); InstanceView view = _instanceStore.Execute(_instanceStore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30)); _instanceStore.DefaultInstanceOwner = view.InstanceOwner;
InstanceStore یک کلاس abstract می باشد که همهی Providerهای مربوط به persistence از آن مشتق میشوند. در این پروژه من از کلاس SqlWorkflowInstanceStore استفاده کردم تا workflowها را در دیتابیسSQL Server ذخیره کنم.
برای ایجاد یک Request مقادیر را از فرم دریافت کرده، یک User ایجاد میکنیم و آن را در فرآیند به جریان میاندازیم. این کار را در رویداد کلیک دکمه Register انجام میدهیم
private void brnRegister_Click(object sender, RoutedEventArgs e) { Dictionary<string, object> parameters = new Dictionary<string, object>(); parameters.Add("Name", txtName.Text); parameters.Add("Phone", txtPhone.Text); parameters.Add("Email", txtEmail.Text); parameters.Add("ConnectionString", _connectionString); parameters.Add("Writer", new ListBoxTextWriter(lstEvents)); WorkflowApplication i = new WorkflowApplication (new UserWF(), parameters); // Setup persistence i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Run(); }
پارامترهای ورودی را از روی فرم مقدار دهی میکنیم. یک شی از کلاس WorkflowApplication ایجاد میکنیم. خاصیت InstanceStore آن را با Store ای که ایجاد کردیم مقدار دهی میکنیم. توسط رویداد PersistableIdle فرآیند رو مجبور میکنیم به Persist شدن و Unload شدن.
و سپس فرایند را اجرا میکنم.
اگر یادتان باشد، در فرآیند، از یک InvoceMethod استفاده کردیم. متد مورد نظر را هم در کلاس ApplicationInterface.cs ایجاد میکنیم.
public static void NewUser(User l) { if (_app != null) _app.AddNewUser(l); }
همین طور که میبینید، یک متد هم در کلاس AddRequest ایجاد میشود؛ با این محتوا
public void AddNewUser(User l) { this.lstUsers.Dispatcher.BeginInvoke(new Action(() => this.lstUsers.Items.Add(l))); }
این متد فقط یک کاربر را به لیست کاربران اضافه میکند. این لیست همه کاربران را نشان میدهد. توسط رویداد SelectionChanged این کنترل، کاربر انتخاب شده را بررسی کرده در صورتی که کاربر جدید باشد، امکان تایید شدن را برایش فراهم میکنیم؛ که نمایش دکمه تایید است.
private void lstUsers_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User l = (User)lstUsers.Items[lstUsers.SelectedIndex]; lblSelectedNotes.Visibility = Visibility.Visible; if (l.Status == "New") { lblAgent.Visibility = Visibility.Visible; txtAcceptedBy.Visibility = Visibility.Visible; btnAccept.Visibility = Visibility.Visible; } else { lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } } else { lblSelectedNotes.Content = ""; lblSelectedNotes.Visibility = Visibility.Hidden; lblAgent.Visibility = Visibility.Hidden; txtAcceptedBy.Visibility = Visibility.Hidden; btnAccept.Visibility = Visibility.Hidden; } }
و برای رویداد کلیک دکمه تایید کاربر :
private void btnAccept_Click(object sender, RoutedEventArgs e) { if (lstUsers.SelectedIndex >= 0) { User u = (User)lstUsers.Items[lstUsers.SelectedIndex]; Guid id = u.WorkflowID.Value; UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); u = dc.Users.SingleOrDefault<User>(x => x.WorkflowID == id); if (u != null) { u.AcceptedBy = txtAcceptedBy.Text; u.Status = "Assigned"; dc.SubmitChanges(); // Clear the input txtAcceptedBy.Text = ""; } // Update the grid lstUsers.Items[lstUsers.SelectedIndex] = u; lstUsers.Items.Refresh(); WorkflowApplication i = new WorkflowApplication(new UserWF()); i.InstanceStore = _instanceStore; i.PersistableIdle = (waiea) => PersistableIdleAction.Unload; i.Load(id); try { i.ResumeBookmark("GetAcceptes", u); } catch (Exception e2) { AddEvent(e2.Message); } } }
کاربر را انتخاب میکنم مقادیرش را تنظیم میکنیم. آن را دخیره کرده و workflow را از روی guid مربوط به آن که قبلا در فرآیند به Entity دادیم، Load میکنیم و همانطور که میبینید توسط متد ResumeBookmark فرآیند رو از جایی که میخواهیم ادامه میدهیم. البته میتوان تایید کاربر را هم در خود فرآیند انجام داد و چون نوشتن Activity مرتبط با آن تقریبا تکراری است با اجازهی شما من اون رو ننوشتم و زحمتش با خودتونه.
حالا فقط ماندهاست که همه کاربران را در ابتدای نمایش فرم از دیتابیس فراخوانی کنیم و در لیست نمایش دهیم:
private void LoadExistingLeads() { UserDataDataContext dc = new UserDataDataContext(_connectionString); dc.Refresh(RefreshMode.OverwriteCurrentValues, dc.Users); IEnumerable<User> q = dc.Users; foreach (User u in q) { AddNewUser(u); } }
و فراخوانی این متد را به انتهای رویداد Load صفحه واگذار میکنیم.
پروژه رو اجرا کرده و یک کاربر را اضافه میکنم. همانطور که میدانید این کاربر در فرآیند ایجاد و در دیتابیس ذخیره میشود
برنامه را میبندم و دوباره اجرا میکنم. کاربر را انتخاب میکنم و یک نام برای admin انتخاب و آن را تایید میکنم. فرآیند را از bookmark مورد نظر اجرا کرده و به پایان میرسد. با بسته شدن برنامه، فرایند Idle و Unload میشود و ذخیره آن در sqlserver صورت میگیرد.
معماری N-Tier چالشهای بخصوصی را برای قابلیتهای change-tracking در EF اضافه میکند. در ابتدا دادهها توسط یک آبجکت EF Context بارگذاری میشوند اما این آبجکت پس از ارسال دادهها به کلاینت از بین میرود. تغییراتی که در سمت کلاینت روی دادهها اعمال میشوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیتها نمیداند.
در نسخههای قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities میتوانستند تغییرات موجودیتها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی میشود، آبجکت DbContext از آن پشتیبانی نمیکند.
در این سری از مقالات روی عملیات پایه CRUD تمرکز میکنیم که در اکثر اپلیکیشنهای n-Tier استفاده میشوند. همچنین خواهیم دید چگونه میتوان تغییرات موجودیتها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیتهای منفصل (disconnected) توسط سرویسهای Web API نگاهی خواهیم داشت.
بروز رسانی موجودیتهای منفصل با Web API
سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده میشود. همچنین مدیریت دادهها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی میکند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژهها برای شبیه سازی یک محیط n-Tier انجام شده است.
فرض کنید مدلی مانند تصویر زیر داریم.
همانطور که میبینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی میکند. میخواهیم مدل و کد دسترسی به دادهها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده میکند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.
- در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
- کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
- کلاس جدیدی با نام Order در پوشه مدلها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order { public int OrderId { get; set; } public string Product { get; set; } public int Quantity { get; set; } public string Status { get; set; } public byte[] TimeStamp { get; set; } }
- با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
- حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext { public Recipe1Context() : base("Recipe1ConnectionString") { } public DbSet<Order> Orders { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Order>().ToTable("Orders"); // Following configuration enables timestamp to be concurrency token modelBuilder.Entity<Order>().Property(x => x.TimeStamp) .IsConcurrencyToken() .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed); } }
- فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings> <add name="Recipe1ConnectionString" connectionString="Data Source=.; Initial Catalog=EFRecipes; Integrated Security=True; MultipleActiveResultSets=True" providerName="System.Data.SqlClient" /> </connectionStrings>
- فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال میکند.
protected void Application_Start() { // Disable Entity Framework Model Compatibilty Database.SetInitializer<Recipe1Context>(null); ... }
- در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController { // GET api/order public IEnumerable<Order> Get() { using (var context = new Recipe1Context()) { return context.Orders.ToList(); } } // GET api/order/5 public Order Get(int id) { using (var context = new Recipe1Context()) { return context.Orders.FirstOrDefault(x => x.OrderId == id); } } // POST api/order public HttpResponseMessage Post(Order order) { // Cleanup data from previous requests Cleanup(); using (var context = new Recipe1Context()) { context.Orders.Add(order); context.SaveChanges(); // create HttpResponseMessage to wrap result, assigning Http Status code of 201, // which informs client that resource created successfully var response = Request.CreateResponse(HttpStatusCode.Created, order); // add location of newly-created resource to response header response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = order.OrderId })); return response; } } // PUT api/order/5 public HttpResponseMessage Put(Order order) { using (var context = new Recipe1Context()) { context.Entry(order).State = EntityState.Modified; context.SaveChanges(); // return Http Status code of 200, informing client that resouce updated successfully return Request.CreateResponse(HttpStatusCode.OK, order); } } // DELETE api/order/5 public HttpResponseMessage Delete(int id) { using (var context = new Recipe1Context()) { var order = context.Orders.FirstOrDefault(x => x.OrderId == id); context.Orders.Remove(order); context.SaveChanges(); // Return Http Status code of 200, informing client that resouce removed successfully return Request.CreateResponse(HttpStatusCode.OK); } } private void Cleanup() { using (var context = new Recipe1Context()) { context.Database.ExecuteSqlCommand("delete from [orders]"); } } }
در قدم بعدی اپلیکیشن کلاینت را میسازیم که از سرویس Web API استفاده میکند.
- در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
- کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.
نکته: قسمت هایی از اپلیکیشن که باید در لایههای مختلف مورد استفاده قرار گیرند - مانند کلاسهای موجودیتها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا میتوانید پروژه ای از نوع Class Library بسازید و تمام موجودیتها را در آن تعریف کنید. سپس لایههای مختلف این پروژه را ارجاع خواهند کرد.
فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.
private HttpClient _client; private Order _order; private static void Main() { Task t = Run(); t.Wait(); Console.WriteLine("\nPress <enter> to continue..."); Console.ReadLine(); } private static async Task Run() { // create instance of the program class var program = new Program(); program.ServiceSetup(); program.CreateOrder(); // do not proceed until order is added await program.PostOrderAsync(); program.ChangeOrder(); // do not proceed until order is changed await program.PutOrderAsync(); // do not proceed until order is removed await program.RemoveOrderAsync(); } private void ServiceSetup() { // map URL for Web API cal _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") }; // add Accept Header to request Web API content // negotiation to return resource in JSON format _client.DefaultRequestHeaders.Accept. Add(new MediaTypeWithQualityHeaderValue("application/json")); } private void CreateOrder() { // Create new order _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" }; } private async Task PostOrderAsync() { // leverage Web API client side API to call service var response = await _client.PostAsJsonAsync("api/order", _order); Uri newOrderUri; if (response.IsSuccessStatusCode) { // Capture Uri of new resource newOrderUri = response.Headers.Location; // capture newly-created order returned from service, // which will now include the database-generated Id value _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully created order. Here is URL to new resource: {0}", newOrderUri); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private void ChangeOrder() { // update order _order.Quantity = 10; } private async Task PutOrderAsync() { // construct call to generate HttpPut verb and dispatch // to corresponding Put method in the Web API Service var response = await _client.PutAsJsonAsync("api/order", _order); if (response.IsSuccessStatusCode) { // capture updated order returned from service, which will include new quanity _order = await response.Content.ReadAsAsync<Order>(); Console.WriteLine("Successfully updated order: {0}", response.StatusCode); } else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); } private async Task RemoveOrderAsync() { // remove order var uri = "api/order/" + _order.OrderId; var response = await _client.DeleteAsync(uri); if (response.IsSuccessStatusCode) Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode); else Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase); }
Successfully updated order: OK
Sucessfully deleted order: OK
شرح مثال جاری
با اجرای اپلیکیشن Web API شروع کنید. این اپلیکیشن یک کنترلر Web API دارد که پس از اجرا شما را به صفحه خانه هدایت میکند. در این مرحله اپلیکیشن در حال اجرا است و سرویسهای ما قابل دسترسی هستند.
حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی میکنیم. با این کار از سرویس مورد نظر درخواست میکنیم که دادهها را با فرمت JSON بازگرداند. سپس یک آبجکت Order میسازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال میکنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت میکند و آن را به لیست موجودیتها در Context جاری اضافه مینماید. این عمل باعث میشود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات میکند. در آخر با فراخوانی متد SaveChanges دادهها را ذخیره میکنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار میدهیم و به کلاینت ارسال میکنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینتها درخواستهای ایجاد رکورد جدید را بصورت POST ارسال میکنند. درخواستهای HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت میشوند.
در مرحله بعد عملیات بعدی را اجرا میکنیم، تعداد سفارش را تغییر میدهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال میکنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت میشود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر میدهیم، که این کار موجودیت را به Context جاری میچسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی میکنیم. در شمارههای بعدی این سری از مقالات، خواهیم دید چگونه میتوان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام میرسانیم.
در مرحله بعد، عملیات نهایی را اجرا میکنیم که موجودیت Order را از منبع داده حذف میکند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه میکنیم و متد DeleteAsync را فراخوانی میکنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی میکنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری میکند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد میشود.
در یک اپلیکیشن واقعی بهتر است کد دسترسی دادهها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.
ASP.NET MVC #22
حدس من این است که از نکته تغییر «Access modifier» که در متن اشاره کردم، استفاده نکردید و هنوز سطح دسترسی منابع شما internal است که باید public شود.
ابتدا یک پروژه با دو Console Application با نام های Service و Client ایجاد کنید. سپس در پروژه Service یک سرویس به نام BookService ایجاد کنید و کدهای زیر را در آن کپی نمایید:
Contract مربوطه به صورت زیر است:
[ServiceContract] public interface IBookService { [OperationContract] int GetCountOfBook(); }
[ServiceBehavior(IncludeExceptionDetailInFaults = true)] public class BookService : IBookService { public int GetCountOfBook() { return 10; } }
class Program { static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(BookService)); var binding = new BasicHttpBinding(); host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService"); host.Open(); Console.Write("BookService host"); Console.ReadKey(); } }
حال نوبت به پیاده سازی سمت کلاینت میرسد. فایل Program سمت کلاینت را باز کرده و کدهای زیر را نیز در آن کپی نمایید:
static void Main(string[] args) { Thread.Sleep(2000); BasicHttpBinding binding = new BasicHttpBinding(); ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService")); Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook()); Console.ReadKey(); }
خروجی زیر مشاهده میشود:
تا اینجا هیچ گونه اعتبار سنجی انجام نشد. برای پیاده سازی اعتبار سنجی باید یک سری تنظیمات بر روی Binding و Hosting سمت سرور و البته کلاینت بر قرار شود. فایل Program پروزه Service را باز نمایید و محتویات آن را به صورت زیر تغییر دهید:
static void Main(string[] args) { ServiceHost host = new ServiceHost(typeof(BookService)); var binding = new BasicHttpBinding(); binding.Security = new BasicHttpSecurity(); binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; host.Credentials.UserNameAuthentication.UserNamePasswordValidationMode = System.ServiceModel.Security.UserNamePasswordValidationMode.Custom; host.Credentials.UserNameAuthentication.CustomUserNamePasswordValidator = new CustomUserNamePasswordValidator(); host.AddServiceEndpoint(typeof(IBookService), binding, "http://localhost/BookService"); host.Open(); Console.Write("BookService host"); Console.ReadKey(); }
ابتدا نوع Security در Binding را به حالت TransportCredentialOnly تنظیم کردیم. در یک جمله هیچ گونه تضمینی برای صحت اطلاعات انتقالی در این حالت وجود ندارد و فقط یک اعتبار سنجی اولیه انجام خواهد شد. در نتیجه هنگام استفاده از این حالت باید با دقت عمل نمود و نباید فقط به پیاده سازی این حالت اکتفا کرد.( Encryption اطلاعات سرویسها مورد بحث این پست نیست)
ClientCredentialType نیز باید به حالت Basic تنظیم شود. در WCF اعتبار سنجی به صورت پیش فرض در حالت Windows است (بعنی UserNamePasswordValidationMode برابر مقدار Windows است و اعتبار سنجی بر اساس کاربر انجام میشود) . این مورد باید به مقدار Custom تغییر یابد. در انتها نیز باید مدل اعتبار سنجی دلخواه خود را به صورت زیر پیاده سازی کنیم:
در پروژه سرویس یک کلاس به نام CustomUserNamePasswordValidator بسازید و کدهای زیر را در آن کپی کنید:
public class CustomUserNamePasswordValidator : UserNamePasswordValidator { public override void Validate(string userName, string password) { if (userName != "Masoud" || password != "Pakdel") throw new SecurityException("Incorrect userName or password"); } }
تغییرات مورد نیاز سمت کلاینت:
اگر در این حالت پروژه را اجرا نمایید از آن جا که از این به بعد، درخواستها سمت سرور اعتبار سنجی میشوند در نتیجه با خطای زیر روبرو خواهید شد:
این خطا از آن جا ناشی میشود که تنظیمات کلاینت و سرور از نظر امنیتی با هم تناسب ندارد. در نتیجه باید تنظیمات Binding کلاینت و سرور یکی شود. برای این کار کد زیر را به فایل Program سمت کلاینت اضافه میکنیم:
static void Main(string[] args) { Thread.Sleep(2000); BasicHttpBinding binding = new BasicHttpBinding(); binding.Security = new BasicHttpSecurity(); binding.Security.Mode = BasicHttpSecurityMode.TransportCredentialOnly; binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Basic; ChannelFactory<IBookService> channel = new ChannelFactory<IBookService>(binding, new EndpointAddress("http://localhost/BookService")); channel.Credentials.UserName.UserName = "WrongUserName"; channel.Credentials.UserName.Password = "WrongPassword";
Console.WriteLine("Count of book: {0}", channel.CreateChannel().GetCountOfBook()); Console.ReadKey(); }
channel.Credentials.UserName.UserName = "WrongUserName"; channel.Credentials.UserName.Password = "WrongPassword";
دریافت سورس مثال بالا
6- کل مدیریت فایل در فایل index.html میباشد و اگر تمایل به تغییر عنوان، درج لوگوی خودتان و ... را دارید به این فایل مراجعه کنید.
خوب، اکنون فایل Tinymce.html را اجرا کنید و نتیجه را ملاحظه بفرمایید.
Microsoft.AspNet.Identity.Core Microsoft.AspNet.Identity.EntityFrameWork
namespace MyShop.Models { // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. public class ApplicationUser : IdentityUser { } public class ApplicationDbContext : IdentityDbContext<ApplicationUser> { public ApplicationDbContext() : base("DefaultConnection") { } }
namespace MyShop.Models { // You can add profile data for the user by adding more properties to your ApplicationUser class, please visit http://go.microsoft.com/fwlink/?LinkID=317594 to learn more. public class ApplicationUser : IdentityUser { [Display(Name = "نام انگلیسی")] public string EnglishName { get; set; } [Display(Name = "نام سیستمی")] public string NameInSystem { get; set; } [Display(Name = "نام فارسی")] public string PersianName { get; set; } [Required] [DataType(DataType.EmailAddress)] [Display(Name = "آدرس ایمیل")] public string Email { get; set; } } }
namespace MyShop.Models { public class AuthorProduct { [Key] public int AuthorProductId { get; set; } /* public int UserId { get; set; }*/ [Display(Name = "User")] public string ApplicationUserId { get; set; } public int ProductID { get; set; } public virtual Product Product { get; set; } public virtual ApplicationUser ApplicationUser { get; set; } } }
namespace MyShop.Models { [DisplayName("محصول")] [DisplayPluralName("محصولات")] public class Product { [Key] public int ProductID { get; set; } [Display(Name = "گروه محصول")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public int ProductGroupID { get; set; } [Display(Name = "مدت زمان")] public string Duration { get; set; } [Display(Name = "نام تهیه کننده")] public string Producer { get; set; } [Display(Name = "عنوان محصول")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public string ProductTitle { get; set; } [StringLength(200)] [Display(Name = "کلید واژه")] public string MetaKeyword { get; set; } [StringLength(200)] [Display(Name = "توضیح")] public string MetaDescription { get; set; } [Display(Name = "شرح محصول")] [UIHint("RichText")] [AllowHtml] public string ProductDescription { get; set; } [Display(Name = "قیمت محصول")] [DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:#,0 ریال}")] [UIHint("Integer")] [Required(ErrorMessage = "لطفا {0} را وارد کنید")] public int ProductPrice { get; set; } [Display(Name = "تاریخ ثبت محصول")] public DateTime? RegisterDate { get; set; } } }
protected override void Seed(MyShop.Models.ApplicationDbContext context) { context.Users.AddOrUpdate(u => u.Id, new ApplicationUser() { Id = "1",EnglishName = "MortezaDalil", PersianName = "مرتضی دلیل", UserDescription = "توضیح در مورد مرتضی", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" }, new ApplicationUser() { Id = "2", EnglishName = "MarhamatZeinali", PersianName = "محسن احمدی", UserDescription = "توضیح در مورد محسن", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" }, new ApplicationUser() { Id = "3", EnglishName = "MahdiMilani", PersianName = "مهدی محمدی", UserDescription = "توضیح در مورد مهدی", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" }, new ApplicationUser() { Id = "4", EnglishName = "Babak", PersianName = "بابک", UserDescription = "کاربر معمولی بدون توضیح", Email = "mm@mm.com", Phone = "2323", Address = "test", NationalCode = "2222222222", ZipCode = "2222222222" } ); context.AuthorProducts.AddOrUpdate(u => u.AuthorProductId, new AuthorProduct() { AuthorProductId = 1, ProductID = 1, ApplicationUserId = "2" }, new AuthorProduct() { AuthorProductId = 2, ProductID = 2, ApplicationUserId = "1" }, new AuthorProduct() { AuthorProductId = 3, ProductID = 3, ApplicationUserId = "3" } );
if (!context.Users.Where(u => u.UserName == "Admin").Any()) { var roleStore = new RoleStore<IdentityRole>(context); var rolemanager = new RoleManager<IdentityRole>(roleStore); var userstore = new UserStore<ApplicationUser>(context); var usermanager = new UserManager<ApplicationUser>(userstore); var user = new ApplicationUser {UserName = "Admin"}; usermanager.Create(user, "121212"); rolemanager.Create(new IdentityRole {Name = "admin"}); usermanager.AddToRole(user.Id, "admin"); }