public class ApplicationClaimsTransformation : IClaimsTransformation { }
و برای ثبت آن:
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>();
public class ApplicationClaimsTransformation : IClaimsTransformation { }
services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>();
public class IdentityUser<TKey> where TKey : IEquatable<TKey> { [PersonalData] public virtual TKey Id { get; set; }
var personalData = new Dictionary<string, string>(); var personalDataProps = typeof(TUser).GetProperties().Where( prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); foreach (var p in personalDataProps) { personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); }
Response.Headers.Add("Content-Disposition", "attachment; filename=PersonalData.json"); return new FileContentResult(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(personalData)), "text/json");
var user = await _userManager.GetUserAsync(User); if (user == null) { return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); } RequirePassword = await _userManager.HasPasswordAsync(user); if (RequirePassword) { if (!await _userManager.CheckPasswordAsync(user, Input.Password)) { ModelState.AddModelError(string.Empty, "Password not correct."); return Page(); } }
var result = await _userManager.DeleteAsync(user);
await _signInManager.SignOutAsync();
ASPNET Core has a lot of changes compared with the others versions of ASP.NET. One change I want to highlight is the new way to configure settings. With previous versions, this configuration was made by using the common file web.config, but this file simply doesn’t exist anymore in ASP.NET Core. Instead, a new easy way to declare and access to the settings is provided. Let’s take a look.
using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; await using var context = new MyDbContext(); await context.Database.EnsureDeletedAsync(); await context.Database.EnsureCreatedAsync(); context.Users.Add(new User { Name = "John Doe", Birthday = new(1980, 1, 20), ShiftStart = new (8, 0), ShiftLength = TimeSpan.FromHours(8) }); await context.SaveChangesAsync(); public class MyDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"...") .LogTo(Console.WriteLine, LogLevel.Information) .EnableSensitiveDataLogging(); } public DbSet<User> Users { get; set; } } public class User { public int Id { get; set; } public required string Name { get; set; } public DateOnly Birthday { get; set; } public TimeOnly ShiftStart { get; set; } public TimeSpan ShiftLength { get; set; } }
CREATE TABLE [Users] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, [Birthday] date NOT NULL, [ShiftStart] time NOT NULL, [ShiftLength] time NOT NULL, CONSTRAINT [PK_Users] PRIMARY KEY ([Id]) );
TimeSpan.FromMinutes(0)
if (validate) { var manager = context.OwinContext.GetUserManager<ApplicationUserManager>(); var userId = getUserIdCallback(context.Identity); if (manager != null) { var user = await manager.FindByIdAsync(userId).WithCurrentCulture(); var reject = true; // Refresh the identity if the stamp matches, otherwise reject if (user != null && manager.SupportsUserSecurityStamp) { var securityStamp = context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType); if (securityStamp == await manager.GetSecurityStampAsync(userId).WithCurrentCulture()) { reject = false; // Regenerate fresh claims if possible and resign in if (user.IsChangedPermissions && regenerateIdentityCallback != null) { var identity = await regenerateIdentityCallback.Invoke(manager, user).WithCurrentCulture();
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); }
{ "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" }
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 }); }
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[]; }
>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 } }
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]); }
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 } }); } }
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
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] } ];
در خیلی از مواقع 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; } } } }
برای پیاده سازی از طریق کد، یک کلاس با نام 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 صورت میگیرد.
در قسمت قبل دیدیم چگونه باید پروفایل کاربر را بدرستی بارگذاری کنیم. در این مقاله به مالکیت وهلهها (instance ownership) میپردازیم.
در پایان قسمت قبلی، اپلیکیشن وب را در این حالت رها کردیم:
همانطور که مشاهده میکنید با خطای زیر مواجه هستیم:
System.Data.SqlClient.SqlException: Cannot open database "OldFashionedDB" requested by the login. The login failed.
Login failed for user 'IIS APPPOOL\ASP.NET v4.0'.
این بار پیغام خطا واضح و روشن است. LocalDb با موفقیت اجرا شده و اپلیکیشن وب هم توانسته به آن وصل شود، اما این کانکشن سپس قطع شده چرا که دسترسی به وهله جاری وجود نداشته است. اکانت ApplicationPoolIdentity (در اینجا IIS APPPOOL\ASP.NET v4.0) نتوانسته به دیتابیس LocalDb وارد شود، چرا که دیتابیس مورد نظر در رشته اتصال اپلیکیشن (OldFashionedDB) وجود ندارد. عجیب است، چرا که وصل شدن به همین دیتابیس با رشته اتصال جاری در ویژوال استودیو با موفقیت انجام میشود.
همانطور که در تصویر بالا مشاهده میکنید از ابزار SQL Server Object Explorer استفاده شده است. این ابزار توسط SQL Server Data Tools معرفی شد و در نسخههای بعدی ویژوال استودیو هم وجود دارد و توسعه یافته است. چطور ممکن است ویژوال استودیو براحتی بتواند به دیتابیس وصل شود، اما اپلیکیشن وب ما با همان رشته اتصال نمیتواند دیتابیس را باز کند؟ در هر دو صورت رشته اتصال ما بدین شکل است:
Data Source=(localdb)\v11.0;Initial Catalog=OldFashionedDB;Integrated Security=True
پاسخ این است که در اینجا، دو وهله از LocalDb وجود دارد. بر خلاف وهلههای SQL Server Express که بعنوان سرویسهای ویندوزی اجرا میشوند، وهلههای LocalDb بصورت پروسسهای کاربری (user processes) اجرا میشوند. هنگامی که کاربران مختلفی سعی میکنند به LocalDb متصل شوند، برای هر کدام از آنها پروسسهای مجزایی اجرا خواهد شد. هنگامی که در ویژوال استودیو به localdb)\v11.0) وصل میشویم، وهله ای از LocalDb ساخته شده و در حساب کاربری ویندوز جاری اجرا میشود. اما هنگامی که اپلیکیشن وب ما در IIS میخواهد به همین دیتابیس وصل شود، وهله دیگری ساخته شده و در ApplicationPoolIdentity اجرا میشود. گرچه ویژوال استودیو و اپلیکیشن ما هر دو از یک رشته اتصال استفاده میکنند، اما در عمل هر کدام به وهلههای متفاوتی از LocalDb دسترسی پیدا خواهند کرد. پس مسلما دیتابیسی که توسط وهله ای در ویژوال استودیو ساخته شده است، برای اپلیکیشن وب ما در IIS در دسترس نخواهد بود.
یک مقایسه خوب از این وضعیت، پوشه My Documents در ویندوز است. فرض کنید در ویژوال استودیو کدی بنویسیم که در این پوشه یک فایل جدید میسازد. حال اگر با حساب کاربری دیگری وارد ویندوز شویم و به پوشه My Documents برویم این فایل را نخواهیم یافت. چرا که پوشه My Documents برای هر کاربر متفاوت است. بهمین شکل، وهلههای LocalDb برای هر کاربر متفاوت است و به پروسسها و دیتابیسهای مختلفی اشاره میکنند.
به همین دلیل است که اپلیکیشن وب ما میتواند بدون هیچ مشکلی روی IIS Express اجرا شود و دیتابیس را باز کند. چرا که IIS Express درست مانند LocalDb یک پروسس کاربری است. IIS Express توسط ویژوال استودیو راه اندازی میشود و روی حساب کاربری جاری اجرا میگردد، پس پروسس آن با پروسس خود ویژوال استودیو یکسان خواهد بود و هر دو زیر یک اکانت کاربری اجرا خواهند شد.
درک ماهیت مشکل جاری، راه حالهای مختلفی را برای رفع آن بدست میدهد. از آنجا که هر راه حل مزایا و معایب خود را دارد، بجای معرفی یک راه حال واحد چند راهکار را بررسی میکنیم.
اگر مشکل، حسابهای کاربری مختلف است، چرا خود IIS را روی کاربر جاری اجرا نکنیم؟ در این صورت ویژوال استودیو و اپلیکیشن ما هر دو به یک وهله از LocalDb وصل خواهند شد و همه چیز بدرستی کار خواهد کرد. ایجاد تغییرات لازم نسبتا ساده است. IIS را اجرا کنید و Application Pool مناسب را انتخاب کنید، یعنی همان گزینه که برای اپلیکیشن شما استفاده میشود.
قسمت Advanced Settings را باز کنید:
روی دکمه سه نقطه کنار خاصیت Identity کلیک کنید تا پنجره Application Pool Identity باز شود:
در این قسمت میتوانید از حساب کاربری جاری استفاده کنید. روی دکمه Set کلیک کنید و نام کاربری و رمز عبور خود را وارد نمایید. حال اگر اپلیکیشن را مجددا اجرا کنید، همه چیز باید بدرستی اجرا شود.
خوب، معایب این رویکرد چیست؟ مسلما اجرای اپلیکیشن وب روی اکانت کاربری جاری، ریسکهای امنیتی متعددی را معرفی میکند. اگر کسی بتواند اپلیکیشن وب ما را هک کند، به تمام منابع سیستم که اکانت کاربری جاری به آنها دسترسی دارد، دسترسی خواهد داشت. اما اجرای اپلیکیشن مورد نظر روی ApplicationPoolIdentity امنیت بیشتری را ارائه میکند، چرا که اکانتهای ApplicationPoolIdentity دسترسی بسیار محدودتری به منابع سیستم محلی دارند. بنابراین استفاده از این روش بطور کلی توصیه نمیشود، اما در سناریوهای خاصی با در نظر داشتن ریسکهای امنیتی میتواند رویکرد خوبی باشد.
یک راه حال دیگر استفاده از قابلیت instance sharing است. این قابلیت به ما این امکان را میدهد تا یک وهله LocalDb را بین کاربران یک سیستم به اشتراک بگذاریم. وهله به اشتراک گذاشته شده، توسط یک نام عمومی (public name) قابل دسترسی خواهد بود.
سادهترین راه برای به اشتراک گذاشتن وهلههای LocalDb استفاده از ابزار SqlLocalDB.exe است. بدین منظور Command Prompt را بعنوان مدیر سیستم باز کنید و فرمان زیر را اجرا نمایید:
sqllocaldb share v11.0 IIS_DB
Data Source=(localdb)\.\IIS_DB;Initial Catalog=OldFashionedDB;Integrated Security=True
create login [IIS APPPOOL\ASP.NET v4.0] from windows; exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin
معایب این روش چیست؟ مشکل اصلی در این رویکرد این است که پیش از آنکه اپلیکیشن ما بتواند به وهله مشترک دسترسی داشته باشد، باید وهله مورد نظر را راه اندازی و اجرا کنیم. بدین منظور، حساب کاربری ویندوزی که مالکیت وهله را دارد باید به آن وصل شود و کانکشن را زنده نگه دارد، در غیر اینصورت وهله LocalDb قابل دسترسی نخواهد بود.
از آنجا که نسخه کامل SQL Server Express بعنوان یک سرویس ویندوزی اجرا میشود، شاید بهترین راه استفاده از همین روش باشد. کافی است یک نسخه از SQL Server Express را نصب کنیم، دیتابیس مورد نظر را در آن بسازیم و سپس به آن متصل شویم. برای این کار حتی میتوانید از ابزار جدید SQL Server Data Tools استفاده کنید، چرا که با تمام نسخههای SQL Server سازگار است. در صورت استفاده از نسخههای کامل تر، رشته اتصال ما بدین شکل تغییر خواهد کرد:
Data Source=.\SQLEXPRESS;Initial Catalog=OldFashionedDB;Integrated Security=True
create login [IIS APPPOOL\ASP.NET v4.0] from windows; exec sp_addsrvrolemember N'IIS APPPOOL\ASP.NET v4.0', sysadmin