مطالب
پیاده سازی یک تامین کننده MySQL برای ASP.NET Identity
در این مقاله جایگزینی پیاده سازی پیش فرض ASP.NET Identity را بررسی می‌کنیم. در ادامه خواهید خواند:

  • جزئیات نحوه پیاده سازی یک Storage Provider برای ASP.NET Identity
  • تشریح اینترفیس هایی که باید پیاده سازی شوند، و نحوه استفاده از آنها در ASP.NET Identity
  • ایجاد یک دیتابیس MySQL روی Windows Azure
  • نحوه استفاده از یک ابزار کلاینت (MySQL Workbench) برای مدیریت دیتابیس مذکور
  • نحوه جایگزینی پیاده سازی سفارشی با نسخه پیش فرض در یک اپلیکیشن ASP.NET MVC
در انتهای این مقاله یک اپلیکیشن ASP.NET MVC خواهیم داشت که از ASP.NET Identity و تامین کننده سفارشی جدید استفاده می‌کند. دیتابیس اپلیکیشن MySQL خواهد بود و روی Windows Azure میزبانی می‌شود. سورس کد کامل این مثال را هم می‌توانید از این لینک دریافت کنید.


پیاده سازی یک Storage Provider سفارشی برای ASP.NET Identity

ASP.NET Identity سیستم توسعه پذیری است که می‌توانید بخش‌های مختلف آن را جایگزین کنید.در این سیستم بناهای سطح بالایی مانند Managers و Stores وجود دارند.
Managers کلاس‌های سطح بالایی هستند که توسعه دهندگان از آنها برای اجرای عملیات مختلف روی ASP.NET Identity استفاده می‌کنند. مدیریت کننده‌های موجود عبارتند از UserManager و RoleManager. کلاس UserManager برای اجرای عملیات مختلف روی کاربران استفاده می‌شود، مثلا ایجاد کاربر جدید یا حذف آنها. کلاس RoleManager هم برای اجرای عملیات مختلف روی نقش‌ها استفاده می‌شود.

Stores کلاس‌های سطح پایین‌تری هستند که جزئیات پیاده سازی را در بر می‌گیرند، مثلا اینکه موجودیت‌های کاربران و نقش‌ها چگونه باید ذخیره و بازیابی شوند. این کلاس‌ها با مکانیزم ذخیره و بازیابی تلفیق شده اند. مثلا Microsoft.AspNet.Identity.EntityFramework کلاسی با نام UserStore دارد که برای ذخیره و بازیابی User‌ها و داده‌های مربوطه توسط EntityFramework استفاده می‌شود.

Managers از Stores تفکیک شده اند و هیچ وابستگی ای به یکدیگر ندارند. این تفکیک بدین منظور انجام شده که بتوانید مکانیزم ذخیره و بازیابی را جایگزین کنید، بدون اینکه اپلیکیشن شما از کار بیافتد یا نیاز به توسعه بیشتر داشته باشد. کلاس‌های Manager می‌توانند با هر Store ای ارتباط برقرار کنند. از آنجا که شما از API‌های سطح بالای UserManager برای انجام عملیات CRUD روی کاربران استفاده می‌کنید، اگر UserStore را با پیاده سازی دیگری جایگزین کنید، مثلا AzureTable Storage یا MySql، نیازی به بازنویسی اپلیکیشن نیست.

در مثال جاری پیاده سازی پیش فرض Entity Framework را با یک  تامین کننده MySQL جایگزین می‌کنیم.

پیاده سازی کلاس‌های Storage
برای پیاده سازی تامین کننده‌های سفارشی، باید کلاس هایی را پیاده سازی کنید که همتای آنها در Microsoft.AspNet.Identity.EntityFramework وجود دارند:
  • <UserStore<TUser
  • IdentityUser
  • <RoleStore<TRole
  • IdentityRole
پیاده سازی پیش فرض Entity Framework را در تصاویر زیر مشاهده می‌کنید.
Users

Roles

در مخزن پیش فرض ASP.NET Identity EntityFramework کلاس‌های بیشتری برای موجودیت‌ها مشاهده می‌کنید.

  • IdentityUserClaim
  • IdentityUserLogin
  • IdentityUserRole
همانطور که از نام این کلاس‌ها مشخص است، اختیارات، نقش‌ها و اطلاعات ورود کاربران توسط این کلاس‌ها معرفی می‌شوند. در مثال جاری این کلاس‌ها را پیاده سازی نخواهیم کرد، چرا که بارگذاری اینگونه رکوردها از دیتابیس به حافظه برای انجام عملیات پایه (مانند افزودن و حذف اختیارات کاربران) سنگین است. در عوض کلاس‌های backend store اینگونه عملیات را بصورت مستقیم روی دیتابیس اجرا خواهند کرد. بعنوان نمونه متد ()UserStore.GetClaimsAsync را در نظر بگیرید. این متد به نوبه خود متد (userClaimTable.FindByUserId(user.Id را فراخوانی می‌کند که یک کوئری روی جدول مربوطه اجرا می‌کند و لیستی از اختیارات کاربر را بر می‌گرداند.
public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}
برای پیاده سازی یک تامین کننده سفارشی MySQL مراحل زیر را دنبال کنید.
1. کلاس کاربر را ایجاد کنید، که اینترفیس IUser را پیاده سازی می‌کند.
public class IdentityUser : IUser
{
    public IdentityUser(){...}

    public IdentityUser(string userName) (){...}

    public string Id { get; set; }

    public string UserName { get; set; }

    public string PasswordHash { get; set; }

    public string SecurityStamp { get; set; }
}
2. کلاس User Store را ایجاد کنید، که اینترفیس‌های IUserStore, IUserClaimStore, IUserLoginStore, IUserRoleStore و IUserPasswordStore را پیاده سازی می‌کند. توجه کنید که تنها اینترفیس IUserStore را باید پیاده سازی کنید، مگر آنکه بخواهید از امکاناتی که دیگر اینترفیس‌ها ارائه می‌کنند هم استفاده کنید.
public class UserStore : IUserStore<IdentityUser>,
                         IUserClaimStore<IdentityUser>,
                         IUserLoginStore<IdentityUser>,
                         IUserRoleStore<IdentityUser>,
                         IUserPasswordStore<IdentityUser>
{
    public UserStore(){...}

    public Task CreateAsync(IdentityUser user){...}

    public Task<IdentityUser> FindByIdAsync(string userId){...}   
...
}
3. کلاس Role را ایجاد کنید که اینترفیس IRole را پیاده سازی می‌کند.
public class IdentityRole : IRole
{
    public IdentityRole(){...}

    public IdentityRole(string roleName) (){...}

    public string Id { get; set; }

    public string Name { get; set; }
}
4. کلاس Role Store را ایجاد کنید که اینترفیس IRoleStore را پیاده سازی می‌کند. توجه داشته باشید که پیاده سازی این مخزن اختیاری است و در صورتی لازم است که بخواهید از نقش‌ها در سیستم خود استفاده کنید.
public class RoleStore : IRoleStore<IdentityRole>                        
{
    public RoleStore(){...}

    public Task CreateAsync(IdentityRole role){...}

    public Task<IdentityRole> FindByIdAsync(string roleId){...}   
....
}
کلاس‌های بیشتری هم وجود دارند که مختص پیاده سازی مثال جاری هستند.
  • MySQLDatabase: این کلاس اتصال دیتابیس MySql و کوئری‌ها را کپسوله می‌کند. کلاس‌های UserStore و RoleStore توسط نمونه ای از این کلاس وهله سازی می‌شوند.
  • RoleTable: این کلاس جدول Roles و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserClaimsTable: این کلاس جدول UserClaims و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserLoginsTable: این کلاس جدول UserLogins و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserRolesTable: این کلاس جدول UserRoles و عملیات CRUD مربوطه به آن را کپسوله می‌کند.
  • UserTable: این کلاس جدول Users و عملیات CRUD مربوط به آن را کپسوله می‌کند.

ایجاد یک دیتابیس MySQL روی Windows Azure

1. به پورتال مدیریتی Windows Azure وارد شوید.
2. در پایین صفحه روی NEW+ کلیک کنید و گزینه STORE را انتخاب نمایید.

در ویزارد Choose Add-on به سمت پایین اسکرول کنید و گزینه ClearDB MySQL Database را انتخاب کنید. سپس به مرحله بعد بروید.

4. راهکار Free بصورت پیش فرض انتخاب شده، همین گزینه را انتخاب کنید و نام دیتابیس را به IdentityMySQLDatabase تغییر دهید. نزدیک‌ترین ناحیه (region) به خود را انتخاب کنید و به مرحله بعد بروید.

5. روی علامت checkmark کلیک کنید تا دیتابیس شما ایجاد شود. پس از آنکه دیتابیس شما ساخته شد می‌توانید از قسمت ADD-ONS آن را مدیریت کنید.

6. همانطور که در تصویر بالا می‌بینید، می‌توانید اطلاعات اتصال دیتابیس (connection info) را از پایین صفحه دریافت کنید.

7. اطلاعات اتصال را با کلیک کردن روی دکمه مجاور کپی کنید تا بعدا در اپلیکیشن MVC خود از آن استفاده کنیم.


ایجاد جداول ASP.NET Identity در یک دیتابیس MySQL

ابتدا ابزار MySQL Workbench را نصب کنید.
1. ابزار مذکور را از اینجا دانلود کنید.
2. هنگام نصب، گزینه Setup Type: Custom را انتخاب کنید.
3. در قسمت انتخاب قابلیت ها، گزینه‌های Applications و MySQLWorkbench را انتخاب کنید و مراحل نصب را به اتمام برسانید.
4. اپلیکیشن را اجرا کرده و روی MySQLConnection کلیک کنید تا رشته اتصال جدیدی تعریف کنید. رشته اتصالی که در مراحل قبل از Azure MySQL Database کپی کردید را اینجا استفاده کنید. بعنوان مثال:
 Connection Name: AzureDB; Host Name: us-cdbr-azure-west-b.cleardb.com; Username: <username>; Password: <password>; Default Schema: IdentityMySQLDatabase 
5. پس از برقراری ارتباط با دیتابیس، یک برگ Query جدید باز کنید. فرامین زیر را برای ایجاد جداول مورد نیاز کپی کنید.
CREATE TABLE `IdentityMySQLDatabase`.`users` (
  `Id` VARCHAR(45) NOT NULL,
  `UserName` VARCHAR(45) NULL,
  `PasswordHash` VARCHAR(100) NULL,
  `SecurityStamp` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `IdentityMySQLDatabase`.`roles` (
  `Id` VARCHAR(45) NOT NULL,
  `Name` VARCHAR(45) NULL,
  PRIMARY KEY (`Id`));

CREATE TABLE `IdentityMySQLDatabase`.`userclaims` (
  `Id` INT NOT NULL AUTO_INCREMENT,
  `UserId` VARCHAR(45) NULL,
  `ClaimType` VARCHAR(100) NULL,
  `ClaimValue` VARCHAR(100) NULL,
  PRIMARY KEY (`Id`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userlogins` (
  `UserId` VARCHAR(45) NOT NULL,
  `ProviderKey` VARCHAR(100) NULL,
  `LoginProvider` VARCHAR(100) NULL,
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userroles` (
  `UserId` VARCHAR(45) NOT NULL,
  `RoleId` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`UserId`, `RoleId`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) 
on delete cascade
on update cascade,
  FOREIGN KEY (`RoleId`)
    REFERENCES `IdentityMySQLDatabase`.`roles` (`Id`)
on delete cascade
on update cascade);
6. حالا تمام جداول لازم برای ASP.NET Identity را در اختیار دارید، دیتابیس ما MySQL است و روی Windows Azure میزبانی شده.


ایجاد یک اپلیکیشن ASP.NET MVC و پیکربندی آن برای استفاده از MySQL Provider

2. در گوشه سمت راست پایین صفحه روی دکمه Download Zip کلیک کنید تا کل پروژه را دریافت کنید.
3. محتوای فایل دریافتی را در یک پوشه محلی استخراج کنید.
4. پروژه AspNet.Identity.MySQL را باز کرده و آن را کامپایل (build) کنید.
5. روی نام پروژه کلیک راست کنید و گزینه Add, New Project را انتخاب نمایید. پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را به IdentityMySQLDemo تغییر دهید.

6. در پنجره New ASP.NET Project قالب MVC را انتخاب کنید و تنظیمات پیش فرض را بپذیرید.

7. در پنجره Solution Explorer روی پروژه IdentityMySQLDemo کلیک راست کرده و Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "Identity.EntityFramework" را وارد کنید. در لیست نتایج این پکیج را انتخاب کرده و آن را حذف (Uninstall) کنید. پیغامی مبنی بر حذف وابستگی‌ها باید دریافت کنید که مربوط به پکیج EntityFramework است، گزینه Yes را انتخاب کنید. از آنجا که کاری با پیاده سازی فرض نخواهیم داشت، این پکیج‌ها را حذف می‌کنیم.

8. روی پروژه IdentityMySQLDemo کلیک راست کرده و Add, Reference, Solution, Projects را انتخاب کنید. در دیالوگ باز شده پروژه AspNet.Identity.MySQL را انتخاب کرده و OK کنید.

9. در پروژه IdentityMySQLDemo پوشه Models را پیدا کرده و کلاس IdentityModels.cs را حذف کنید.

10. در پروژه IdentityMySQLDemo تمام ارجاعات ";using Microsoft.AspNet.Identity.EntityFramework" را با ";using AspNet.Identity.MySQL" جایگزین کنید.

11. در پروژه IdentityMySQLDemo تمام ارجاعات به کلاس "ApplicationUser" را با "IdentityUser" جایگزین کنید.

12. کنترلر Account را باز کنید و متد سازنده آنرا مطابق لیست زیر تغییر دهید.

public AccountController() : this(new UserManager<IdentityUser>(new UserStore(new MySQLDatabase())))
{

}

13. فایل web.config را باز کنید و رشته اتصال DefaultConnection را مطابق لیست زیر تغییر دهید.

<add name="DefaultConnection" connectionString="Database=IdentityMySQLDatabase;Data Source=<DataSource>;User Id=<UserID>;Password=<Password>" providerName="MySql.Data.MySqlClient" />

مقادیر <DataSource>, <UserId> و <Password> را با اطلاعات دیتابیس خود جایگزین کنید.


اجرای اپلیکیشن و اتصال به دیتابیس MySQL

1. روی پروژه IdentityMySQLDemo کلیک راست کرده و Set as Startup Project را انتخاب کنید.
2. اپلیکیشن را با Ctrl + F5 کامپایل و اجرا کنید.
3. در بالای صفحه روی Register کلیک کنید.
4. حساب کاربری جدیدی بسازید.

5. در این مرحله کاربر جدید باید ایجاد شده و وارد سایت شود.

6. به ابزار MySQL Workbench بروید و محتوای جداول IdentityMySQLDatabase را بررسی کنید. جدول users را باز کنید و اطلاعات کاربر جدید را بررسی نمایید.

برای ساده نگاه داشتن این مقاله از بررسی تمام کدهای لازم خودداری شده، اما اگر مراحل را دنبال کنید و سورس کد نمونه را دریافت و بررسی کنید خواهید دید که پیاده سازی تامین کنندگان سفارشی برای ASP.NET Identity کار نسبتا ساده ای است.

مطالب
استفاده از قالب مخصوص Redux Toolkit جهت ایجاد پروژه‌های React/Redux
استفاده از Redux درون پروژه‌های React، به روش‌های مختلفی قابل انجام است؛ یعنی محدودیتی از لحاظ نحوه چیدمان فایل‌ها، تغییر state و نحوه‌ی dispatch کردن action وجود ندارد. به عبارتی این آزادی عمل را خواهیم داشت تا خودمان سیم‌کشی پروژه را انجام دهیم؛ ولی مشکل اصلی اینجاست که نمی‌توانیم مطمئن شویم روشی که پروژه را با آن ستاپ کرده‌ایم آیا به عنوان یک best-practice محسوب می‌شود یا خیر. در نهایت خروجی را که خواهیم داشت، حجم انبوهی از کدهای boilerplate و پکیج‌های زیادی است که در حین توسعه‌ی پروژه، به همراه Redux اضافه شده‌اند. همچنین در نهایت یک store پیچیده را خواهیم داشت که مدیریت آن به مراتب سخت خواهد شد. یک مشکل دیگر این است که روال گفته شده را باید به ازای هر پروژه‌ی جدید تکرار کنیم. برای حل این مشکل، یکی از maintainerهای اصلی تیم ریداکس، یک پروژه را تحت عنوان Redux Toolkit، مدتها قبل برای حل مشکلات عنوان شده شروع کرده است و این پکیج، جدیداً به قالب رسمی create-react-app اضافه شده است. که در واقع یک روش استاندارد و به اصطلاح opinionated برای ایجاد پروژه‌های ریداکسی می‌باشد و شامل تمامی وابستگی‌های موردنیاز برای کار با Redux از قبیل redux-thunk و همچنین Redux DevTools است. 

 ایجاد یک برنامه‌ی خالی React با قالب redux
در ادامه برای بررسی این قالب جدید، یک پروژه‌ی جدید React را ایجاد خواهیم کرد:
> npx create-react-app redux-template --template redux
> cd redux-template
> yarn start


بررسی ساختار پروژه‌ی ایجاد شده
ساختار پروژه‌ی ایجاد شده به صورت زیر است:


این ساختار خیلی شبیه به قالب پیش‌فرض create-react-app می‌باشد. همانطور که در تصویر فوق نیز مشاهده میکنید، پروژه‌ی ایجاد شده‌ی با قالب redux (تصویر سمت چپ)، یک فایل با نام store و همچنین یک دایرکتوری را به نام features دارد. اگر به فایل store.js مراجعه کنید، خواهید دید که تنظیمات اولیه‌ی ایجاد store را در قالب یک مثال Counter ایجاد کرده‌است:
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});
در کد فوق، نحوه‌ی ایجاد store، نسبت به حالت معمول، خیلی تمیزتر است. نکته‌ی جالب این است که به همراه کد فوق، Redux DevTools و همچنین redux-thunk هم از قبل تنظیم شده‌اند و در نتیجه، نیازی به تنظیم و نصب آنها نیست. تغییر مهم دیگر، پوشه‌ی features می‌باشد که یک روش رایج برای گروه‌بندی کامپوننت‌ها، همراه با فایل‌های وابسته‌ی آن‌ها است. درون این پوشه، یک پوشه جدید را تحت عنوان counter داریم که حاوی فایل‌های زیر می‌باشد: 
Counter
Counter.module.css
counterSlice.js
Counter.js، کامپوننتی است که در نهایت درون صفحه رندر خواهد شد. درون این فایل با استفاده از Redux Hooks کار اتصال به store و همچنین dispatch کردن اکشن‌ها انجام گرفته است:
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {
  decrement,
  increment,
  incrementByAmount,
  selectCount,
} from './counterSlice';
import styles from './Counter.module.css';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState(2);

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      <div className={styles.row}>
        <input
          className={styles.textbox}
          value={incrementAmount}
          onChange={e => setIncrementAmount(e.target.value)}
        />
        <button
          className={styles.button}
          onClick={() =>
            dispatch(
              incrementByAmount({ amount: Number(incrementAmount) || 0 })
            )
          }
        >
          Add Amount
        </button>
      </div>
    </div>
  );
}

فایل Counter.module.css نیز در واقع استایل‌های مربوط به کامپوننت فوق میباشد که به صورت CSS module اضافه شده‌است. در نهایت فایل counterSlice.js را داریم که  کار همان reducer سابق را برایمان انجام خواهد داد؛ اما اینبار با یک ساختار جدید و تحت عنوان slice. اگر به فایل عنوان شده مراجعه کنید، کدهای زیر را خواهید دید:
import { createSlice } from '@reduxjs/toolkit';

export const slice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to 'mutate' the state. It doesn't actually
      // mutate the state because it uses the immer library, which detects
      // changes to a "draft state" and produces a brand new immutable state
      // based off those changes
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload.amount;
    },
  },
});

export const selectCount = state => state.counter.value;
export const { increment, decrement, incrementByAmount } = slice.actions;

export default slice.reducer;
در این قالب جدید، ترکیب این قطعات هستند که شیء اصلی یا در واقع همان state کلی پروژه را تشکیل خواهند داد. همانطور که مشاهده میکنید، برای ایجاد یک قطعه جدید، از تابع createSlice استفاده شده است. این تابع، تعدادی پارامتر را از ورودی دریافت می‌کند:
  • name: برای هر بخش از state، می‌توانیم یک نام را تعیین کنیم و این همان عنوانی خواهد بود که می‌توانید توسط Redux DevTools مشاهده کنید.
  • initialValue: در اینجا می‌توانیم مقادیر اولیه‌ای را برای این بخش از state، تعیین کنیم که در مثال فوق، value به مقدار صفر تنظیم شده‌است.
  • reducers: این قسمت محل تعریف actionهایی هستند که قرار است state را تغییر دهند. نکته جالب توجه این است که state در هر کدام از متدهای فوق، به ظاهر mutate شده است؛ اما همانطور که به صورت کامنت نیز نوشته‌است، در پشت صحنه از کتابخانه‌ای با عنوان immer استفاده می‌کند که در عمل بجای تغییر state اصلی، یک کپی از state جدید را جایگزین state قبلی خواهد کرد.
توسط selectCount نیز کار انتخاب مقدار موردنظر از state انجام شده‌است که معادل همان mapPropsToState است و در اینجا امکان دسترسی به state ذخیره شده در مخزن redux فراهم شده است. همچنین در خطوط پایانی فایل نیز اکشن‌ها برای دسترسی ساده‌تر به درون کامپوننت، به صورت Object Destructuring به بیرون export شده‌اند. در نهایت reducer نهایی را از slice ایجاد شده استخراج کرده‌ایم. این پراپرتی برای ایجاد store مورداستفاده قرار می‌گیرد.

چرا قالب Redux Toolkit از immer برای تغییر state استفاده میکند؟
همانطور که در این قسمت از سری Redux توضیح داده شد، اعمال تغییرات بر روی آرایه‌ها و اشیاء، باعث ایجاد ناخالصی خواهد شد؛ بنابراین به جای تغییر شیء اصلی، باید توسط یکی از روش‌های Object.assign و یا spread operator، یک clone از state اصلی را ایجاد کرده و آن را به عنوان state نهایی لحاظ کنیم. اما در حین کار با اشیاء nested، انجام اینکار سخت خواهد شد و همچنین خوانایی کد را نیز کاهش می‌دهد:
return {
    ...state,
    models: state.models.map(c =>
        c.model === action.payload.model
          ? {
              ...c,
              on: action.payload.toggle
            }
          : c
      )
  };
اما با کمک immer می‌توانیم به صورت مستقیم state را تغییر دهیم:
state.models.forEach(item => {
    if (item.model === action.payload.model) {
      item.on = action.payload.toggle;
    }
 });
کاری که immer انجام می‌دهد، تغییر یک شیء، به صورت مستقیم نیست؛ در واقع یک draftState را ایجاد خواهد کرد که در عمل یک proxy برای state فعلی می‌باشد. یعنی با mutate کردن state، یک شیء جدید را در نهایت clone خواهد کرد و به عنوان state نهایی برمی‌گرداند.
نظرات مطالب
مفهوم READ_COMMITTED_SNAPSHOT در EF 6
فرض کنید یک جدول نظرات دارید با این تعریف
CREATE TABLE [BlogComments](
[Id] [int] IDENTITY(1,1) NOT NULL,
[Body] [nvarchar](max) NULL,
[Date1] [datetime] NOT NULL,
 CONSTRAINT [PK_BlogComments] PRIMARY KEY CLUSTERED 
(
[Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF,
 ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
بعد در management studio دو پنجره اجرای کوئری جدید را ایجاد کنید. در پنجره اول بنویسید:
-- در پنجره اول
BEGIN TRAN 
UPDATE [BlogComments] SET Body='Test' WHERE id=1
در پنجره دوم بنویسید
-- در پنجره دوم
SELECT TOP 1000 [Id]  ,[Body] ,[Date1] FROM [BlogComments]
- ابتدا عبارت پنجره اول را اجرا کنید. این پنجره حاوی یک تراکنش نا تمام است. شروع دارد اما به عمد پایان آن‌را ذکر نکردیم.
- الان پنجره دوم را اجرا کنید.
مشاهده خواهید کرد که ... به جواب نمی‌رسید. کوئری اجرا نمی‌شود و سیستم قفل شده چون تراکنش اول commit نشده (مثلا یک تراکنش طولانی را اینجا شبیه سازی کردیم؛ یا حتی یک اشتباه در تعاریف T-SQL انجام شده).

در ادامه، عملیات این پنجره‌ها را دستی متوقف کنید. بعد مطابق دستوراتی که پیشتر ذکر شد، READ_COMMITTED_SNAPSHOT را روی دیتابیس فعال کنید.
مجددا دو مرحله قبل را اجرا کنید. در این حالت کوئری دوم اجرا خواهد شد، چون اطلاعات را از کپی فقط خواندنی بانک اطلاعاتی شما دریافت می‌کند؛ بر اساس آخرین اطلاعات commit شده در سیستم.
مطالب
کار با اسکنر در برنامه های تحت وب (قسمت دوم و آخر)

در قسمت قبل « کار با اسکنر در برنامه‌های تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژه‌ی عملی و پیاده سازی مطالب مطرح شده می‌رویم.

ابتدا پروژه‌ی   WCF را شروع می‌کنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژه‌ی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :

    [ServiceContract]
    public interface IScannerService
    {
        [OperationContract]
        [WebInvoke(Method = "GET",
           BodyStyle = WebMessageBodyStyle.Wrapped,
           RequestFormat = WebMessageFormat.Json,
           ResponseFormat = WebMessageFormat.Json,
           UriTemplate = "GetScan")]
        string GetScan();
    }
در اینجا ما فقط اعلان متدهای مورد نیاز خود را ایجاد کرده‌ایم. علت استفاده از Attribute ایی با نام WebInvoke ، مشخص نمودن نوع خروجی به صورت Json است و همچنین عنوان آدرس مناسبی برای صدا زدن متد. پس از آن کلاس ScannerService را مطابق کدهای زیر تغییر دهید:
    public class ScannerService : IScannerService
    {
        public string GetScan()
        {
            // TODO Add code here
        }
    }
تا اینجا فقط یک WCF Service معمولی ساخته‌ایم .در ادامه به سراغ کلاس WIA برای ارتباط با اسکنر می‌رویم.
بر روی پروژه‌ی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینه‌ی Microsoft Windows Image Acquisition Library v2.0 را به پروژه‌ی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر می‌شود که ارجاعی از آن را در کلاس ScannerService قرار می‌دهیم.
using WIA;
اکنون متد GetScan را مطابق زیر اصلاح می‌نماییم:
        public string GetScan()
        {
            var imgResult = String.Empty;
            var dialog = new CommonDialogClass();
            try
            {
                // نمایش فرم پیشفرض اسکنر
                var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType);
                
                // ذخیره تصویر در یک فایل موقت
                var filename = Path.GetTempFileName();
                image.SaveFile(filename);
                var img = Image.FromFile(filename);

                // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به 
                imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg);
            }
            catch
            {
                // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی 
                // بازگردانده می‌شود که به معنای نبود تصویر می‌باشد
            }

            return imgResult;
        }
دقت داشته باشید که کدها را در زمان توسعه بین Try..Catch قرار ندهید چون ممکن‌است در این زمان به خطاهایی برخورد کنید که نیاز باشد در مرورگر آنها را دیده و رفع خطا نمایید.
CommonDialogClass  کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر می‌باشد و متد‌های مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار می‌دهد که بسته به نیاز خود می‌توانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage می‌باشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش می‌دهد و کاربر از طریق آن می‌تواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :


و سپس فرم پیش فرض اسکنر‌های TWAIN را جهت تعیین تنظیمات اسکن نمایش می‌دهد که این امکان نیز در این فرم فراهم است تا دستگاه‌های Feeder یا Flated انتخاب گردند.

خروجی این متد همان عکس اسکن شده است که از نوع WIA.ImageFile می‌باشد و ما پس از دریافتش، ابتدا آن را در یک فایل موقت ذخیره نموده و سپس با استفاده از یک متد کمکی آن را به فرمت Base64 برای درخواست کننده اسکن ارسال می‌نماییم.

کدهای کلاس کمکی ImageHelper:

        public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format)
        {
            if (image != null)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    // Convert Image to byte[]
                    image.Save(ms, format);
                    byte[] imageBytes = ms.ToArray();

                    // Convert byte[] to Base64 String
                    string base64String = Convert.ToBase64String(imageBytes);
                    return base64String;
                }
            }
            return String.Empty;
        }
توجه داشته باشید که خروجی این متد قرار است توسط callBack یک متد جاوا اسکریپتی مورد استفاده قرار گرفته و احیانا عکس مورد نظر در صفحه نمایش داده شود. پس بهتر است که از قالب تصویر به شکل Base64 استفاده گردد. ضمن اینکه پلاگین‌های Jquery مرتبط با ویرایش تصویر هم از این قالب پشتیبانی می‌کنند. (اینجا )

این مثال به ساده‌ترین شکل نوشته شد. کلاس دیگری هم در اینجا وجود دارد و در صورتیکه از اسکنر نوع Feeder استفاده می‌کنید، می‌توانید از کدهای آن استفاده کنید.

کار ما تا اینجا در پروژه‌ی WCF Service تقریبا تمام است. اگر پروژه را یکبار Build نمایید برای اولین بار احتمالا پیغام خطاهای زیر ظاهر خواهند شد:


جهت رفع این خطا، در قسمت Reference‌های پروژه خود، WIA را انتخاب نموده و از Properties‌های آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل می‌شود.

به سراغ پروژه‌ی ویندوز فرم جهت هاست کردن این WCF سرویس می‌رویم. می‌توانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده می‌کنیم.
یک پروژه‌ی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژه‌ی قبلی رفته و dll‌‌های آن را به پروژه خود اضافه نمایید.
Form1.cs  را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
        private readonly Uri _baseAddress = new Uri("http://localhost:6019");
        private ServiceHost _host;
برای استفاده از کلاس ServiceHost لازم است تا ارجاعی به فضای نام System.ServiceModel داده شود. متغیر baseAddress_ نگه دارنده‌ی آدرس ثابت سرویس اسکنر در سمت کلاینت می‌باشد و به این ترتیب ما دقیقا می‌دانیم باید سرویس را با کدام آدرس در کدهای جاوا اسکریپتی خود فراخوانی نماییم.
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه می‌نماییم:
        private void Form1_Load(object sender, EventArgs e)
        {
            _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress);
            _host.Open();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            _host.Close();
        }
همین چند خط برای هاست کردن سرویس روی آدرس localhost و پورت 8010 کامپیوتر کلاینت کافی است. اما یکسری تنظیمات مربوط به خود سرویس هم وجود دارد که باید در زمان پیاده سازی سرویس، در خود پروژه‌ی سرویس، ایجاد می‌گردید. اما از آنجا که ما قرار است سرویس را در یک پروژه‌ی دیگر هاست کنیم، بنابراین این تنظیمات را باید در همین پروژه‌ی WinForm قرار دهیم.
فایل App.Config پروژه‌ی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="BehaviourMetaData">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="WcfServiceScanner.ScannerService"
               behaviorConfiguration="BehaviourMetaData">
        <endpoint address=""
                  binding="basicHttpBinding"
                  contract="WcfServiceScanner.IScannerService" />
      </service>
    </services>
  </system.serviceModel>

</configuration>
اکنون پروژه‌ی هاست آماده اجرا می‌باشد. اگر آنرا اجرا کنید و در مرورگر خود آدرس ذکر شده را وارد کنید، صفحه‌ی زیر را مشاهده خواهد کرد که به معنی صحت اجرای سرویس اسکنر می‌باشد.

اگر موفق به اجرا نشدید و احیانا با خطای زیر مواجه شدید، اطمینان حاصل کنید که ویژوال استودیو Run as Administrator باشد. مشکل حل خواهد شد.


به سراغ پروژه‌ی بعدی، یعنی وب سایت خود می‌رویم. یک پروژه‌ی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه می‌کنیم.
( از آنجا که کدها به صورت جاوا اسکریپت می‌باشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحه‌ی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a>
<img src="" id="img-scanned" />
<script>
    $("#get-scan").click(function () {
        var url = 'http://localhost:6019/';
        $.get(url, function (data) {
            $("#img-scanned").attr("src","data:image/Jpeg;base64,  "+ data.GetScanResult);
        });
    });
</script>
دقت کنید در هنگام دریافت اطلاعات از سرویس، نتیجه به شکل GetScanResult خواهد بود. الان اگر پروژه را اجرا نمایید و روی لینک کلیک کنید، اسکنر شروع به دریافت اسکن خواهد کرد اما نتیجه‌ای بازگشت داده نخواهد شد و علت هم مشکل امنیتی CORS می‌باشد که به دلیل دریافت اطلاعات از یک دامین دیگر رخ می‌دهد و اگر با Firebug درخواست را بررسی کنید متوجه خطا به شکل زیر خواهید شد.


راه حل‌های زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژه‌ی ما جوابگو نمی‌باشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژه‌ی WCF Service  می‌باشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
    public class CORSSupport : IDispatchMessageInspector
    {
        Dictionary<string, string> requiredHeaders;
        public CORSSupport(Dictionary<string, string> headers)
        {
            requiredHeaders = headers ?? new Dictionary<string, string>();
        }

        public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
            var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty;
            if (httpRequest.Method.ToLower() == "options")
                instanceContext.Abort();
            return httpRequest;
        }

        public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
            var httpRequest = correlationState as HttpRequestMessageProperty;

            foreach (var item in requiredHeaders)
            {
                httpResponse.Headers.Add(item.Key, item.Value);
            }
            var origin = httpRequest.Headers["origin"];
            if (origin != null)
                httpResponse.Headers.Add("Access-Control-Allow-Origin", origin);

            var method = httpRequest.Method;
            if (method.ToLower() == "options")
                httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent;

        }
    }

    // Simply apply this attribute to a DataService-derived class to get
    // CORS support in that service
    [AttributeUsage(AttributeTargets.Class)]
    public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior
    {
        #region IServiceBehavior Members

        void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
        {
        }

        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var requiredHeaders = new Dictionary<string, string>();

            //Chrome doesn't accept wildcards when authorization flag is true
            //requiredHeaders.Add("Access-Control-Allow-Origin", "*");
            requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS");
            requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type");
            requiredHeaders.Add("Access-Control-Allow-Credentials", "true");
            foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
            {
                foreach (EndpointDispatcher ed in cd.Endpoints)
                {
                    ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders));
                }
            }
        }

        void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }

        #endregion
    }
فضاهای نام لازم برای این کلاس به شرح زیر می‌باشد:
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
کلاس ScannerService را باز کرده و آنرا به ویژگی
    [CORSSupportBehavior]
    public class ScannerService : IScannerService
    {
مزین نمایید.

کار تمام است، یکبار دیگر ابتدا پروژه‌ی WcfServiecScanner و سپس پروژه هاست را Build کرده و برنامه‌ی هاست را اجرا کنید. اکنون مشاهده می‌کنید که با زدن دکمه‌ی اسکن، اسکنر فرم تنظیمات اسکن را نمایش می‌دهد که پس از زدن دکمه‌ی Scan، پروسه آغاز شده و پس از اتمام، تصویر اسکن شده در صفحه‌ی وب سایت نمایش داده می‌شود.
نظرات مطالب
ایجاد گزارشات Crosstab در PdfReport
سلام
 نام‌ستون   در linq اعمال می‌شود ولی در خروجی گزارش به زیر نمایش داده می‌شود:
نامستون 
مطالب
Identity و مباحث مربوط به آن (قسمت اول) - آشنایی با Identity
Identity یکی از  Attributeهایی که در SQL Server به ازای Columnهای عددی می‌توان در نظر گرفت. به طور خیلی ساده هنگامی که این Attribute به ازای یک فیلد عددی تنظیم گردد. چنانچه رکوردی در جدول مربوط به Identity درج شود فیلد Identity مقداری را به طور اتوماتیک دریافت خواهد نمود. 
نحوه دریافت مقدار به ازای فیلد Identity با توجه به آخرین مقدار آن و گام افزایش است که در هنگام ایجاد identity تعریف می‌گردد.
برای ایجاد یک فیلد از نوع Identity می‌توانید زمانیکه جدول خود را ایجاد می‌کنید این Attribute را به فیلد مورد نظر خود تخصیص دهید. 
 مثال 1 : این مثال نحوه ایجاد یک فیلد از نوع Identity را نمایش می‌دهد. 
USE tempdb
GO
CREATE TABLE Customers1
(
 ID INT IDENTITY,-- ID INT IDENTITY(1,1)
 Name NVARCHAR(100),
 [Address] NVARCHAR(200)
)
GO

همانطور که در مثال 1 مشاهده می‌کنید فیلد ID از نوع Identity تعریف شده است. در این حالت (ID int IDENTITY) مقدار شروع و گام افزایش به ازای این فیلد 1 در نظر گرفته خواهد شد. در این صورت اگر چند رکورد زیر را به ازای این جدول درج کنید. مقدار Identity به صورت زیر خواهد بود. 
INSERT INTO Customers1 (Name,[Address]) VALUES
(N'مسعود',N'میانه'),
(N'فرید',N'میانه'),
(N'احمد',N'میانه')
GO
SELECT * FROM Customers1

مثال 2 : این مثال نحوه ایجاد یک فیلد از نوع Identity به همراه مقدار شروع و گام افزایش را مشخص می‌کند. 
USE tempdb
GO
CREATE TABLE Customers2
(
ID INT IDENTITY(100,2),
Name NVARCHAR(100),
[Address] NVARCHAR(200)
)
GO

همانطور که در مثال 2 مشاهده می‌کنید فیلد ID از نوع Identity تعریف شده است و مقدار شروع آن از 100 و همچنین گام افزایش 2 در نظر گرفته شده است. در این صورت اگر چند رکورد زیر را به ازای این جدول درج کنید. مقدار Identity به صورت زیر خواهد بود. 
INSERT INTO Customers2 (Name,[Address]) VALUES
(N'مسعود',N'میانه'),
(N'فرید',N'میانه'),
(N'احمد',N'میانه')
GO
SELECT * FROM Customers2

مثال 3 : این مثال نحوه تنظیم یک فیلد به صورت Identity را در محیط SQL Server Management Studio (SSMS) آموزش می‌دهد.
1- برای شروع کار همانند تصویر زیر بر روی قسمت Table کلیک راست کنید و گزینه New Table… را انتخاب کنید.


2- پس از نمایش پنجره زیر فیلدی را که می‌خواهید از نوع Identity باشد را انتخاب کرده و در قسمت Column Properties خصیصه Is Identity  را برابر Yes قرار دهید تا فیلد مورد نظر شما از نوع Identity در نظر گرفته شود. لازم به ذکر است که Identity Seed مقدار شروع و Identity Increment گام افزایش را مشخص می‌نماید. 

مطالب
کار با Docker بر روی ویندوز - قسمت ششم - کار با بانک‌های اطلاعاتی درون Containerها
تا اینجا نحوه‌ی اجرای برنامه‌ها را داخل کانتینرها بررسی کردیم؛ اما هنوز در مورد داده‌های آن‌ها بحث نکرده‌ایم. اگر بانک‌های اطلاعاتی را به درون کانتینرها منتقل کنیم، چه بر سر داده‌های آن‌ها می‌آید؟


بررسی روش اجرای MS SQL Server Express درون یک Container

اگر مخازن Imageهای رسمی مایکروسافت را در داکرهاب بررسی کنیم، به مخازنی مانند mssql-server-windows-express ، mssql-server و یا mssql-server-linux نیز خواهیم رسید. در اینجا آخرین نگارش Image مربوط به SQL Server Express آن، حدود 7GB حجم دارد. برای دریافت آن ابتدا به Windows Containers سوئیچ کنید و سپس دستور زیر را صادر نمائید:
 docker pull microsoft/mssql-server-windows-express
پس از دریافت آن، اگر به مستندات رسمی آن در داکر هاب مراجعه کنیم، دستوری را به صورت زیر برای اجرای آن عنوان کرده‌است:
 docker run -d -p 1433:1433 -e sa_password=<SA_PASSWORD> -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
در این دستور:
- سوئیچ d سبب می‌شود تا پس از اجرای این دستور، بلافاصله به command prompt بازگشت داده شویم و SQL Server Express در background اجرا شود.
- سپس پورت 1433 میزبان به پورت 1433 کانتینر، نگاشت شده‌است که پورت استاندارد SQL Server است.
- سوئیچ e، امکان تنظیم متغیرهای محیطی را میسر می‌کند؛ برای مثال ورود کلمه‌ی عبور کاربر SA و یا پذیرش مجوز آن. برای نمونه، این کلمه‌ی عبور را مساوی password وارد کنید؛ هرچند کار نخواهد کرد، اما بررسی خطاهای به همراه آن مفید است.
- و در آخر نام image مرتبط ذکر شده‌است.

پس از اجرای این دستور، کانتینر SQL Server Express، در پس زمینه شروع به کار خواهد کرد و بلافاصله به خط فرمان بازگشت داده می‌شویم. در اینجا ممکن است آغاز SQL Server اندکی طول بکشد. برای اینکه دریابیم در این لحظه وضعیت پروسه‌ی آن به چه صورتی است، دستور docker logs id را صادر کنید. پس از آن خطایی مانند password validation failed را مشاهده خواهیم کرد. عنوان می‌کند که پیچیدگی کلمه‌ی عبور وارد شده کافی نیست.

یک نکته: زمانیکه دستور docker run را اجرا می‌کنیم، یک هش طولانی را نمایش می‌دهد و پس از آن به خط فرمان بازگشت داده می‌شویم. این هش طولانی، همان id کانتینر در حال اجرا است. برای مثال در دستور docker logs id می‌توان 3 حرف ابتدای این هش را بجای id وارد کرد. البته این id را توسط دستور docker ps نیز می‌توان بدست آورد.

بنابراین با توجه به اینکه دستور docker logs id، خطایی را گزارش کرده‌است، توسط دستور docker stop id، این کانتینر را متوقف کرده و آن‌را مجددا با کلمه‌ی عبوری مانند pass!w0rd1 اجرا می‌کنیم:
 docker run -d -p 1433:1433 -e sa_password=pass!w0rd1 -e ACCEPT_EULA=Y microsoft/mssql-server-windows-express
اینبار نیز مجددا دستور docker logs id را بر اساس id جدید این کانتینر اجرا می‌کنیم که پیام Started SQL Server را نمایش می‌دهد. بنابراین تا به اینجا موفق شدیم پروسه‌ی SQL Server Express را بدون مشکلی آغاز کنیم.


همانطور که در قسمت سوم نیز عنوان شد، اگر این کانتینر را بر روی ویندوز سرور، در حالت Windows Containers اجرا کنیم (و نه در حالت Hyper-V)، پروسه‌های اجرای شده‌ی داخل یک Container را می‌توان با Job Object Idهای یکسانی که دارند، در Task Manager ویندوز، در کنار سایر پروسه‌های سیستم، شناسایی کرد.


اتصال به SQL Server Express اجرا شده‌ی داخل یک Container توسط SQL Server Management Studio

پس از اجرای SQL Server Express دخل کانتینر، مطابق تنظیمات آن، چه در سیستم میزبان و چه در داخل کانتینر، به پورت 1433 گوش فرا داده می‌شود. به همین منظور نیاز است IP این کانتینر را نیز بدست آوریم. برای اینکار دستور ipconfig را در سیستم میزبان صادر کنید تا بر اساس مشخصات کارت شبکه‌ی مجازی آن، بتوان IP آن‌را بدست آورد (دستور docker inspect id نیز چنین اطلاعاتی را به همراه دارد). اکنون می‌توان از داخل سیستم راه دور دیگری که SQL Server Management Studio بر روی آن نصب است، توسط این IP و پورت، به SQL Server Express متصل شد.


البته در اینجا نیازی به ذکر پورت نیست؛ چون پورت 1433، شماره پورت پیش‌فرض است. بعد از اتصال، می‌توان کارهای متداولی مانند ایجاد یک بانک اطلاعاتی جدید را انجام داد.
برای آزمایش، یکبار دستور docker ps را صادر کنید تا id این کانتینر مشخص شود. سپس دستور docker stop id را صادر کنید تا پروسه SQL Server Express خاتمه یابد. اکنون اگر در SQL Server Management Studio قصد کار با آن‌را داشته باشیم، پیام عدم اتصال مشاهده می‌شود. اکنون برای اجرای مجدد کانتینر، دستور docker start id را صادر کنید.


بررسی روش اجرای MySQL داخل یک Container

برای اجرای MySQL نیاز است به Linux Containers سوئیچ کنیم. حجم tag ویژه‌ی latest آن نیز حدود 138MB است که نسبت به SQL Server Express هفت گیگابایتی، بسیار کمتر است!
در همان صفحه‌ی مستندات آن در داکرهاب، دستور اجرایی آن نیز ذکر شده‌است:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
در اینجا نیز توسط سوئیچ e که مخفف environment است، یکسری از متغیرهای محیطی MySQL، مانند کلمه‌ی عبور آن قابل تنظیم هستند. همچنین سوئیچ d نیز برای اجرای آن در پس زمینه، ذکر شده‌است. همین دستور را به همین شکل، صرفا با حذف tag آن، جهت اشاره‌ی به آخرین نگارش موجود این image، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql
با اجرای این دستور، در ابتدا MySQL از داکرهاب دریافت شده و سپس در پس زمینه اجرا خواهد شد. پیش از بازگشت به command prompt، یک هش طولانی نیز نمایش داده می‌شود که همان id کانتینر در حال اجرای آن است. برای اینکه بتوانیم ریز جزئیات رخ داده را بهتر مشاهده کنیم، می‌توان از دستور docker logs id استفاده کرد.

یک نکته: می‌توان یک command prompt جدید را باز کرد و سپس دستور docker logs -f id را در آن صادر کرد. به این صورت لاگ‌های لحظه‌ای یک کانتینر را نیز می‌توان مشاهده کرد (f در اینجا به معنای follow است).

اکنون می‌خواهیم MySQL Client موجود در همین Container در حال اجرا را، اجرا کنیم (اجرای پروسه‌ای درون یک کانتینر در حال اجرا). برای اینکار از دستور docker exec استفاده می‌شود:
docker ps
docker exec -it id mysql --user=root --password=my-secret-pw
ابتدا توسط دستور docker ps مقدار id این کانتینر را بدست می‌آوریم و سپس در دستور بعدی، از آن استفاده خواهیم کرد.
در اینجا توسط دستور docker exec ابتدا یک interactive shell را درخواست کرده‌ایم (اجرای foreground یک برنامه‌ی شل). سپس id این کانتینر باید ذکر شود. پس از آن نام فایل اجرایی MySQL Client قید شده و در پایان، نام کاربری و کلمه‌ی عبور اتصال به آن که در دستور docker run تنظیم شده‌اند، ذکر می‌شوند.
با اجرای این دستور، به خط فرمان MySQL Client داخل این کانتینر دسترسی پیدا می‌کنیم. در اینجا می‌توان دستورات مختلفی را برای کار با پروسه‌ی mysql اجرا کرد؛ مانند اجرای دستور show databases که لیست بانک‌های اطلاعاتی موجود را نمایش می‌دهد:
mysql> show databases;
use mysql;
show tables;
select * from user;
exit;


روش مدیریت داده‌های بانک‌های اطلاعاتی توسط Docker

در قسمت قبل دریافتیم که لایه‌ی رویی یک container، دارای قابلیت read/write است و برای مثال می‌توان فایل‌های یک وب سایت استاتیک را در آنجا کپی و سپس هاست کرد. اما این لایه، لایه‌ی مناسبی برای ذخیره سازی داده‌های یک بانک اطلاعاتی نیست. در اینجا برای مدیریت بهتر این نوع داده‌ها، از مفهومی به نام volume استفاده می‌شود.
برای درک روش مدیریت داده‌ها توسط داکر، دستور docker volume ls را اجرا کنید. مشاهده خواهید کرد که docker یک volume پیش‌فرض را نیز ایجاد کرده‌است. البته با volumes پیشتر در قسمت چهارم، در بخش «روش به اشتراک گذاری فایل سیستم میزبان با کانتینرها» نیز آشنا شده‌ایم. این volume پیش‌فرض، کار ذخیره سازی اطلاعات را حتی اگر کانتینری در حال اجرا نباشد نیز انجام می‌دهد. وجود یک چنین قابلیتی جهت از دست نرفتن اطلاعات ارزشمند ذخیره شده‌ی در بانک‌های اطلاعاتی بسیار ضروری است.
البته لازم به ذکر است، این volume ای را که در اینجا مشاهده می‌کنید، توسط Dockerfile خود mysql به صورت خودکار ایجاد می‌شود. برای مثال در داکرهاب، در قسمت full description این image، در ابتدای توضیحات قسمتی است به نام supported tags and respective dockerfile links. در اینجا هر tag نامبرده شده، در حقیقت لینکی است به یک Dockerfile. اگر یکی از آن‌ها را باز کنید، چنین سطری را در آن مشاهده خواهید کرد:
  VOLUME /var/lib/mysql
این دستور سبب می‌شود چنین مسیری (مسیر پیش‌فرض ثبت اطلاعات mysql) به صورت یک volume جدید، خارج از فایل سیستم کانتینر، بر روی سیستم میزبان، ایجاد شود. سپس این مسیر و volume جدید، توسط داکر به صورت خودکار به این کانتینر mount خواهد شد و برای این موارد نیازی نیست کار خاصی توسط ما انجام شود.
اینکار نه فقط برای بالابردن کارآیی اعمال read/write انجام شده‌ی توسط container انجام می‌شود، بلکه حتی اگر این کانتینر را توسط دستور docker rm id حذف کنیم، دستور docker volume ls، هنوز همان volume ای را که در حین نصب mysql به صورت خودکار ایجاد شده بود، نمایش می‌دهد. علت اینجا است که طول عمر این volume، وابسته‌ی به طول عمر کانتینر آن نیست. به این ترتیب حذف تصادفی یک کانتینر، سبب از دست رفتن اطلاعات ارزشمند داخل بانک اطلاعاتی آن نمی‌شود.


روش تعیین صریح یک volume برای یک کانتینر بانک اطلاعاتی، توسط volumeهای نامدار

دستور docker run ای را که برای اجرای mysql صادر کردیم، یک volume خودکار را ایجاد کرده‌است و اگر آن‌را با دستور docker volume ls بررسی کنیم، دارای یک نام هش مانند است که به آن anonymous volume هم گفته می‌شود. در ادامه قصد داریم یک volume نامدار را ایجاد کنیم و سپس از آن جهت ذخیره سازی اطلاعات چندین وهله از کانتینر mysql استفاده نمائیم.
پیش از ادامه بحث، ابتدا توسط دستور docker rm id، کانتینر mysql ای را که پیشتر ایجاد کردیم حذف کنید؛ هرچند این دستور، volume متناظر با آن‌را حذف نمی‌کند.
سپس برای اینکه یک کانتینر جدید mysql را با ذکر صریح volume آن ایجاد و اجرا کنیم، می‌توان از دستور زیر استفاده کرد:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
در اینجا از سوئیچ v برای ایجاد یک volume نامدار استفاده شده‌است و در آن بجای ذکر قسمت مسیر پوشه‌ای در سمت میزبان، صرفا یک نام، مانند db، پیش از ذکر : قید شده‌است. پس از :، مسیری که این volume قرار است در آن کانتینر به آن نگاشت شود، ذکر شده‌است.
اکنون اگر دستور docker volume ls را صادر کنیم، در لیست خروجی آن، نام db قابل مشاهده‌است.

در ادامه پروسه‌ی MySQL Client داخل این کانتینر را اجرا کرده:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و تغییراتی را به صورت زیر اعمال می‌کنیم:
mysql> show databases;
create database pets;
show databases;
exit;
در اینجا بانک اطلاعاتی جدید pets ایجاد شده‌است.

اکنون در ابتدا این کانتینر را متوقف کرده و سپس آن‌را حذف می‌کنیم:
docker ps
docker stop id
docker rm id
هرچند اگر دستور حذف را با سوئیچ f- نیز اجرا کنیم (به معنای force)، کار stop را به صورت خودکار انجام می‌دهد.

در ادامه مجددا همان دستور قبلی را که توسط آن volume نامداری، ایجاد کردیم، اجرا می‌کنیم:
 docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -v db:/var/lib/mysql mysql
اینبار اگر دستور docker volume ls را مجددا صادر کنیم، مشاهده خواهیم کرد این کانتینر جدید، بجای ایجاد یک volume جدید، از همان volume موجود db که آن‌را پیشتر ایجاد کردیم، استفاده می‌کند؛ هرچند کانتینری که آن‌را ایجاد کرده‌است، دیگر وجود خارجی ندارد. در این حالت اگر MySQL Client این کانتینر را اجرا نمائیم:
 docker exec -it some-mysql mysql --user=root --password=my-secret-pw
و سپس دستور نمایش بانک‌های اطلاعاتی آن‌را صادر کنیم:
 mysql> show databases;
در خروجی آن هنوز بانک اطلاعاتی pets که پیشتر ایجاد شده بود، قابل مشاهده‌است. بنابراین حذف و یا ایجاد کانتینرها، تاثیری را بر روی volumeهای ایجاد شده، نخواهند داشت.


روش حذف volumes اضافی

با توجه به اینکه volumeها، طول عمر متفاوتی را نسبت به کانتینرها دارند، ممکن است پس از مدتی فضای دیسک سخت شما را پر کنند. برای مثال به ازای هربار اجرای دستور docker run مربوط با MYSQL با نامی متفاوت، یک volume جدید نیز ایجاد می‌شود.
خروجی دستور docker inspect id به همراه قسمتی است به نام mounts که خاصیت name آن، دقیقا مساوی نام volume متناظر با کانتینر بررسی شده‌است. همچنین خاصیت source آن، محل دقیق ذخیره سازی این volume را بر روی فایل سیستم میزبان مشخص می‌کند.
برای حذف آن‌ها، ابتدا نیاز است کانتینرها را متوقف کرد. دستور زیر تمام کانتینرهای در حال اجرا را متوقف می‌کند. در اینجا دستور docker ps -q، لیست id تمام کانتینرهای در حال اجرا را باز می‌گرداند (در این دستورات، افزودن پارامتر q، سبب بازگشت صرفا idها می‌شود):
 docker stop $(docker ps -q)
اگر می‌خواهید تمام کانتینرهای موجود را حذف کنید:
 docker rm $(docker ps -aq)
و یا دستور زیر ابتدا تمام کانتینرهای موجود را متوقف کرده و سپس آن‌ها را حذف می‌کند:
 docker rm -f $(docker ps -aq)
دستور زیر تمام volumes موجود را حذف می‌کند:
 docker volume rm $(docker volume ls -q)
دستور زیر یک کانتینر با id مشخص شده را به همراه volume نامگذاری نشده‌ی مرتبط با آن، متوقف و سپس حذف می‌کند:
 docker rm -fv id
دستور زیر، لیست تمام volumes غیراستفاده شده‌ی توسط کانتینرهای موجود را نمایش می‌دهد (به یک چنین volumeهای در اینجا dangling گفته می‌شود؛ volume ای که کانتینر آن حذف شده‌است):
 docker volume ls -f dangling=true
 که می‌تواند لیست مناسبی برای حذف باشند:
 docker volume rm $(docker volume ls -qf dangling=true)
مطالب
محاسبه میانگین متحرک (moving average) در SQL Server 2012
شرح مساله
میانگین متحرک یا moving average به چند دسته تقسیم می‌شود که ساده‌ترین آنها میان متحرک ساده است.
برای محاسبه میانگین متحرک باید بازه زمانی مورد نظر را مشخص کنیم. مثلا میانگین فروش در 3 روز گذشته.

به جدول زیر توجه بفرمایید:


میانگین متحرک فروش سه روز و چهار روز گذشته در جدول فوق قابل مشاهده است.
بطور مثال مقدار میانگین متحرک سه روزه برای روز چهارم برابر است با جمع فروش سه روز گذشته تقسیم بر سه. یعنی 3/(10+12+13)
و برای روز ششم میانگین متحرک 4 روزه برابر است با جمع فروش چهار روز گذشته و تقسیم آنها بر چهار. یعنی 10+12+13+16 تقسیم بر 4 که برابر است با 12.7

در نمودار زیر، خط قرم رنگ مربوط به میانگین متحرک ساده (میانگین فروش سه روز گذشته) است و خط آبی رنگ نیز میزان فروش است

 



راه حل در SQL Server 2012
توسط توابع window این مساله را به سادگی می‌توانیم حل کنیم. همانطور که مشاهده می‌شود در تصویر زیر. کافیست ما به سطرهایی در بازه‌ی سه سطر قبل تا یک سطر قبل (برای میانگین متحرک سه روزه) دسترسی پیدا کرده و میانگین آن را بگیریم.
 


ابتدا این جدول را ایجاد و تعدادی سطر برای نمونه در آن درج کنید:
CREATE TABLE Samples
(
[date] SMALLDATETIME,
selling SMALLMONEY
);
  
INSERT  Samples
VALUES
('2010-12-01 00:00:00', 10),
('2010-12-02 00:00:00', 12),
('2010-12-03 00:00:00', 13),
('2010-12-04 00:00:00', 16),
('2010-12-05 00:00:00', 19),
('2010-12-06 00:00:00', 23),
('2010-12-07 00:00:00', 26),
('2010-12-08 00:00:00', 27),
('2010-12-09 00:00:00', 20),
('2010-12-10 00:00:00', 18),
('2010-12-11 00:00:00', 19);
سپس برای محاسبه میانگین متحرک در بازه سه روز گذشته query زیر را اجرا کنید: 
SELECT [date],
       selling,
       CASE WHEN rnk < 4 THEN NULL ELSE mv END AS SimpleMovingAverage
  FROM (SELECT *,
               AVG(selling) OVER(ORDER BY [date]
                                 ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING) AS mv,
               ROW_NUMBER() OVER(ORDER BY [date]) AS rnk
          FROM Samples
       ) AS d;



قلب query دستور ROWS BETWEEN 3 PRECEDING AND 1 PRECEDING می‌باشد.
به این معنا که سطرهایی در بازه‌ی سه سطر قبل و یک سطر قبل را در Window انتخاب کرده و عمل میاگنین گیری را بر اساس مقادیر مورد نظر انجام بده.

راه حل در SQL Server 2005
به درخواست یکی از کاربران من راه حلی را پیشنهاد می‌کنم که جایگزین مناسبی برای روش قبلی است در صورت عدم استفاده از نسخه 2012. توابع window در اینگونه مسائل بهترین عملکرد را خواهند داشت.
SELECT S.[date], S.selling, CASE WHEN COUNT(*) < 3 THEN NULL ELSE AVG(s) END AS SimpleMovingAverage
  FROM Samples AS S
       OUTER APPLY (SELECT TOP(3) selling
                      FROM Samples
                     WHERE [date] < S.[date]
                     ORDER BY [date] DESC) AS D(s)
 GROUP BY S.[date], S.selling
 ORDER BY S.[date];



FOR FUN
توسط توابع Analytical ای چون LAG نیز می‌توان اینگونه مسائل را حل نمود. بطور مثال توسط تابع LAG به یک مقدار قبلی، دو مقدار قبلی و سه مقدار قبلی دسترسی پیدا کرده و آنها را با یکدیگر جمع نموده و تقسیم بر تعدادشان می‌کنیم یعنی:
select [date],
       selling,
       (
       lag(selling, 1) over(order by [date]) +
       lag(selling, 2) over(order by [date]) +
       lag(selling, 3) over(order by [date])
       ) / 3
  from Samples;


مطالب
تعریف قالب‌های جداول سفارشی و کار با منابع داده‌ای از نوع Anonymous در PdfReport
تعدادی قالب جدول پیش فرض در PdfReport تعریف شده‌اند، مانند BasicTemplate.RainyDayTemplate ،BasicTemplate.SilverTemplate و غیره. نحوه تعریف این قالب‌ها بر اساس پیاده سازی اینترفیس ITableTemplate است. برای نمونه اگر یک قالب جدید را بخواهیم ایجاد کنیم، تنها کافی است اینترفیس یاد شده را به نحو زیر پیاده سازی نمائیم:
using System.Collections.Generic;
using System.Drawing;
using iTextSharp.text;
using PdfRpt.Core.Contracts;

namespace PdfReportSamples.HexDump
{
    public class GrayTemplate : ITableTemplate
    {
        public HorizontalAlignment HeaderHorizontalAlignment
        {
            get { return HorizontalAlignment.Center; }
        }

        public BaseColor AlternatingRowBackgroundColor
        {
            get { return new BaseColor(Color.WhiteSmoke); }
        }

        public BaseColor CellBorderColor
        {
            get { return new BaseColor(Color.LightGray); }
        }

        public IList<BaseColor> HeaderBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(ColorTranslator.FromHtml("#990000")), new BaseColor(ColorTranslator.FromHtml("#e80000")) }; }
        }

        public BaseColor RowBackgroundColor
        {
            get { return null; }
        }

        public IList<BaseColor> PreviousPageSummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.LightSkyBlue) }; }
        }

        public IList<BaseColor> SummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.LightSteelBlue) }; }
        }

        public IList<BaseColor> PageSummaryRowBackgroundColor
        {
            get { return new List<BaseColor> { new BaseColor(Color.Yellow) }; }
        }

        public BaseColor AlternatingRowFontColor
        {
            get { return new BaseColor(ColorTranslator.FromHtml("#333333")); }
        }

        public BaseColor HeaderFontColor
        {
            get { return new BaseColor(Color.White); }
        }

        public BaseColor RowFontColor
        {
            get { return new BaseColor(ColorTranslator.FromHtml("#333333")); }
        }

        public BaseColor PreviousPageSummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }

        public BaseColor SummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }

        public BaseColor PageSummaryRowFontColor
        {
            get { return new BaseColor(Color.Black); }
        }

        public bool ShowGridLines
        {
            get { return true; }
        }
    }
}
و برای استفاده از آن خواهیم داشت:
.MainTableTemplate(template =>
{
      template.CustomTemplate(new GrayTemplate());
})
چند نکته:
- در کتابخانه iTextSharp، کلاس رنگ توسط BaseColor تعریف شده است. به همین جهت خروجی رنگ‌ها را در اینجا نیز بر اساس BaseColor مشاهده می‌کنید. اگر نیاز داشتید رنگ‌های تعریف شده در فضای نام استاندارد System.Drawing را به BaseColor تبدیل کنید، فقط کافی است آن‌را به سازنده کلاس BaseColor ارسال نمائید.
- اگر علاقمند هستید که معادل رنگ‌های HTML ایی را در اینجا داشته باشید، می‌توان از متد توکار ColorTranslator.FromHtml استفاده کرد.
- برای تعریف رنگی به صورت شفاف (transparent) آن‌را مساوی null قرار دهید.
- در اینترفیس فوق، تعدادی از خروجی‌ها به صورت IList است. در این موارد می‌توان یک یا دو رنگ را حداکثر معرفی کرد. اگر دو رنگ را معرفی کنید یک گرادیان خودکار از این دو رنگ، تشکیل خواهد شد.
- اگر قالب جدید زیبایی را طراحی کردید، لطفا در این پروژه مشارکت کرده و آن‌را به صورت یک وصله ارائه دهید!


تهیه یک منبع داده ناشناس

مثال زیر را در نظر بگیرید. در اینجا قصد داریم معادل Ascii اطلاعات Hex را تهیه کنیم:
using System;
using System.Collections;
using System.Linq;

namespace PdfReportSamples.HexDump
{
    public static class PrintHex
    {
        public static char ToSafeAscii(this int b)
        {
            if (b >= 32 && b <= 126)
            {
                return (char)b;
            }
            return '_';
        }

        public static IEnumerable HexDump(this byte[] data)
        {
            int bytesPerLine = 16;
            return data
                        .Select((c, i) => new { Char = c, Chunk = i / bytesPerLine })
                        .GroupBy(c => c.Chunk)
                        .Select(g =>
                                  new
                                  {
                                      Hex = g.Select(c => String.Format("{0:X2} ", c.Char)).Aggregate((s, i) => s + i),
                                      Chars = g.Select(c => ToSafeAscii(c.Char).ToString()).Aggregate((s, i) => s + i)
                                  })
                        .Select((s, i) =>
                                        new
                                        {
                                            Offset = String.Format("{0:d6}", i * bytesPerLine),
                                            Hex = s.Hex,
                                            Chars = s.Chars
                                        });
        }
    }
}
نکته مهم این منبع داده، خروجی IEnumerable آن و Select نهایی عبارت LINQ ایی است که مشاهده می‌کنید. در اینجا اطلاعات به یک شیء ناشناس با اعضای Offset، Hex و Chars نگاشت شده‌اند.
مفهوم فوق از دات نت 3 به بعد تحت عنوان anonymous types در دسترس است. توسط این قابلیت می‌توان یک شیء را بدون نیاز به تعریف ابتدایی آن ایجاد کرد. این نوع‌های ناشناس توسط واژه‌های کلیدی new و var تولید می‌شوند. کامپایلر به صورت خودکار برای هر anonymous type یک کلاس ایجاد می‌کند.

نکته‌ای مهم حین کار با کلاس‌های ناشناس:
کلاس‌های ناشناس به صورت خودکار توسط کامپایلر تولید می‌شوند و ... از نوع internal هم تعریف خواهند شد. به عبارتی در اسمبلی‌های دیگر قابل استفاده نیستند. البته می‌توان توسط ویژگی assembly:InternalsVisibleTo ، تعاریف internal یک اسمبلی را دراختیار اسمبلی دیگری نیز گذاشت. ولی درکل باید به این موضوع دقت داشت و اگر قرار است منبع داده‌ای به این نحو تعریف شود، بهتر است داخل همان اسمبلی تعاریف گزارش باشد.

برای نمایش این نوع اطلاعات حاصل از کوئری‌های LINQ می‌توان از منبع داده پیش فرض AnonymousTypeList به نحو زیر استفاده کرد:
using System;
using System.Text;
using PdfRpt.Core.Contracts;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.HexDump
{
    public class HexDumpPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.LeftToRight);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\COUR.ttf",
                    Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\tahoma.TTF");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("Hex Dump");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new GrayTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var data = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog.");
                var list = data.HexDump();
                dataSource.AnonymousTypeList(list);
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("Offset");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(0.5f);
                    column.HeaderCell("Offset");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("Hex");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2.5f);
                    column.HeaderCell("Hex");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName("Chars");
                    column.CellsHorizontalAlignment(HorizontalAlignment.Left);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(1f);
                    column.HeaderCell("Chars");
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\HexDumpSampleRpt.pdf"));
        }
    }
}

توضیحات:
در اینجا منبع داده بر اساس کلاس‌های کمکی که تعریف کردیم، به نحو زیر مشخص شده است:
            .MainTableDataSource(dataSource =>
            {
                var data = Encoding.UTF8.GetBytes("The quick brown fox jumps over the lazy dog.");
                var list = data.HexDump();
                dataSource.AnonymousTypeList(list);
            })
و سپس برای معرفی ستون‌های متناظر با این منبع داده ناشناس، فقط کافی است آن‌ها را به صورت رشته‌ای معرفی کنیم:
column.PropertyName("Offset");
//...
column.PropertyName("Hex");
//...
column.PropertyName("Chars");



نکته‌ای در مورد خواص تودرتو:
در حین استفاده از AnonymousTypeList امکان تعریف خواص تو در تو نیز وجود دارد. برای مثال فرض کنید که Select نهایی به شکل زیر تعریف شده است و در اینجا OrderInfoData نیز خود یک شیء است:
.Select(x => new
{
   OrderInfo = x.OrderInfoData
})
برای استفاده از یک چنین منبع داده‌ای، ذکر مسیر خاصیت تودرتوی مورد نظر نیز مجاز است:
column.PropertyName("OrderInfo.Price");

 
مطالب
مروری بر کدهای کلاس SqlHelper

قسمتی از یک پروژه به همراه کلاس SqlHelper آن در کامنت‌های مطلب «اهمیت Code review» توسط یکی از خوانندگان بلاگ جهت Code review مطرح شده که بهتر است در یک مطلب جدید و مجزا به آن پرداخته شود. قسمت مهم آن کلاس SqlHelper است و مابقی در اینجا ندید گرفته می‌شوند:

//It's only for code review purpose!  
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;


public sealed class SqlHelper
{
private SqlHelper() { }


// Send Connection String
//---------------------------------------------------------------------------------------
public static string GetCntString()
{
return WebConfigurationManager.ConnectionStrings["db_ConnectionString"].ConnectionString;
}


// Connect to Data Base SqlServer
//---------------------------------------------------------------------------------------
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
{
try
{
if (sqlCnt == null) sqlCnt = new SqlConnection();
sqlCnt.ConnectionString = cntString;
if (sqlCnt.State != ConnectionState.Open) sqlCnt.Open();
return sqlCnt;
}
catch (SqlException)
{
return null;
}
}


// Run ExecuteScalar Command
//---------------------------------------------------------------------------------------
public static string RunExecuteScalarCmd(ref SqlConnection sqlCnt, string strCmd, bool blnClose)
{
Connect2Db(ref sqlCnt, GetCntString());
using (sqlCnt)
{
using(SqlCommand sqlCmd = sqlCnt.CreateCommand())
{
sqlCmd.CommandText = strCmd;
object objResult = sqlCmd.ExecuteScalar();
if (blnClose) CloseCnt(ref sqlCnt, true);
return (objResult == null) ? string.Empty : objResult.ToString();
}
}
}

// Close SqlServer Connection
//---------------------------------------------------------------------------------------
public static bool CloseCnt(ref SqlConnection sqlCnt, bool nullSqlCnt)
{
try
{
if (sqlCnt == null) return true;
if (sqlCnt.State == ConnectionState.Open)
{
sqlCnt.Close();
sqlCnt.Dispose();
}
if (nullSqlCnt) sqlCnt = null;
return true;
}
catch (SqlException)
{
return false;
}
}
}


مثالی از نحوه استفاده ارائه شده:

protected void BtnTest_Click(object sender, EventArgs e)
{
SqlConnection sqlCnt = new SqlConnection();
string strQuery = "SELECT COUNT(UnitPrice) AS PriceCount FROM [Order Details]";


// در این مرحله پارامتر سوم یعنی کانکشن باز نگه داشته شود
string strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, false);



strQuery = "SELECT LastName + N'-' + FirstName AS FullName FROM Employees WHERE (EmployeeID = 9)";
// در این مرحله پارامتر سوم یعنی کانکشن بسته شود
strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, true);
}


مروری بر این کد:

1) نحوه کامنت نوشتن
بین سی شارپ و زبان سی++ تفاوت وجود دارد. این نحوه کامنت نویسی بیشتر در سی++ متداول است. اگر از ویژوال استودیو استفاده می‌کنید، مکان نما را به سطر قبل از یک متد منتقل کرده و سه بار پشت سر هم forward slash را تایپ کنید. به صورت خودکار ساختار خالی زیر تشکیل خواهد شد:
/// <summary>
///
/// </summary>
/// <param name="sqlCnt"></param>
/// <param name="cntString"></param>
/// <returns></returns>
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)

این روش مرسوم کامنت نویسی کدهای سی شارپ است. خصوصا اینکه ابزارهایی وجود دارند که به صورت خودکار از این نوع کامنت‌ها، فایل CHM‌ درست می‌کنند.

2) وجود سازنده private
احتمالا هدف این بوده که نه شخصی و نه حتی کامپایلر، وهله‌ای از این کلاس را ایجاد نکند. بنابراین بهتر است کلاسی را که تمام متدهای آن static است (که به این هم خواهیم رسید!) ، راسا static معرفی کنید. به این ترتیب نیازی به سازنده private نخواهد بود.

3) وجود try/catch
یک اصل کلی وجود دارد: اگر در حال طراحی یک کتابخانه پایه‌ای هستید، try/catch را در هیچ متدی از آن لحاظ نکنید. بله؛ درست خوندید! لطفا try/catch ننویسید! کرش کردن برنامه خوب است! لا‌یه‌های بالاتر برنامه که در حال استفاده از کدهای شما هستند متوجه خواهند شد که مشکلی رخ داده و این مشکل توسط کتابخانه مورد استفاده «خفه» نشده. برای مثال اگر هم اکنون SQL Server در دسترس نیست، لایه‌های بالاتر برنامه باید این مشکل را متوجه شوند. Exception اصلا چیز بدی نیست! کرش برنامه اصلا بد نیست!
فرض کنید که دچار بیماری شده‌اید. اگر مثلا تبی رخ ندهد، از کجا باید متوجه شد که نیاز به مراقبت پزشکی وجود دارد؟ اگر هیچ علامتی بروز داده نشود که تا الان نسل بشر منقرض شده بود!

4) وجود ref و out
دوستان گرامی! این ref و out فقط جهت سازگاری با زبان C در سی شارپ وجود دارد. لطفا تا حد ممکن از آن استفاده نکنید! مثلا استفاده از توابع API‌ ویندوز که با C نوشته شده‌اند.
یکی از مهم‌ترین کاربردهای pointers در زبان سی، دریافت بیش از یک خروجی از یک تابع است. برای مثال یک متد API ویندوز را فراخوانی می‌کنید؛ خروجی آن یک ساختار است که به کمک pointers به عنوان یکی از پارامترهای همان متد معرفی شده. این روش به وفور در طراحی ویندوز بکار رفته. ولی خوب در سی شارپ که از این نوع مشکلات وجود ندارد. یک کلاس ساده را طراحی کنید که چندین خاصیت دارد. هر کدام از این خاصیت‌ها می‌توانند نمایانگر یک خروجی باشند. خروجی متد را از نوع این کلاس تعریف کنید. یا برای مثال در دات نت 4، امکان دیگری به نام Tuples معرفی شده برای کسانی که سریع می‌خواهند چند خروجی از یک تابع دریافت کنند و نمی‌خواهند برای اینکار یک کلاس بنویسند.
ضمن اینکه برای مثال در متد Connect2Db، هم کانکشن یکبار به صورت ref معرفی شده و یکبار به صورت خروجی متد. اصلا نیازی به استفاده از ref در اینجا نبوده. حتی نیازی به خروجی کانکشن هم در این متد وجود نداشته. کلیه تغییرات شما در شیء کانکشنی که به عنوان پارامتر ارسال شده، در خارج از آن متد هم منعکس می‌شود (شبیه به همان بحث pointers در زبان سی). بنابراین وجود ref غیرضروری است؛ وجود خروجی متد هم به همین صورت.

5) استفاده از using در متد RunExecuteScalarCmd
استفاده از using خیلی خوب است؛ همیشه اینکار را انجام دهید!
اما اگر اینکار را انجام دادید، بدانید که شیء sqlCnt در پایان بدنه using ، توسط GC نابوده شده است. بنابراین اینجا bool blnClose دیگر چه کاربردی دارد؟! تصمیم شما دیگر اهمیتی نخواهد داشت؛ چون کار تخریبی پیشتر انجام شده.

6) متد CloseCnt
این متد زاید است؛ به دلیلی که در قسمت (5) عنوان شد. using های استفاده شده، کار را تمام کرده‌اند. بنابراین بستن اشیاء dispose شده معنا نخواهد داشت.

7) در مورد نحوه استفاده
اگر SqlHelper را در اینجا مثلا یک DAL ساده فرض کنیم (data access layer)، جای قسمت BLL (business logic layer) در اینجا خالی است. عموما هم چون توضیحات این موارد را خیلی بد ارائه داده‌اند، افراد از شنیدن اسم آن‌ها هم وحشت می‌کنند. BLL یعنی کمی دست به Refactoring بزنید و این پیاده سازی منطق تجاری ارائه شده در متد BtnTest_Click را به یک کلاس مجزا خارج از code behind پروژه منتقل کنید. Code behind فقط محل استفاده نهایی از آن باشد. همین! فعلا با همین مختصر شروع کنید.
مورد دیگری که در اینجا باز هم مشهود است، عدم استفاده از پارامتر در کوئری‌ها است. چون از پارامتر استفاده نکرده‌اید، SQL Server مجبور است برای حالت EmployeeID = 9 یکبار execution plan را محاسبه کند، برای کوئری بعدی مثلا EmployeeID = 19، اینکار را تکرار کند و الی آخر. این یعنی مصرف حافظه بالا و همچنین سرعت پایین انجام کوئری‌ها. بنابراین اینقدر در قید و بند باز نگه داشتن یک کانکشن نباشید؛ مشکل اصلی جای دیگری است!

8) برنامه وب و اطلاعات استاتیک!
این پروژه، یک پروژه ASP.NET است. دیدن تعاریف استاتیک در این نوع پروژه‌ها یک علامت خطر است! در این مورد قبلا مطلب نوشتم:
متغیرهای استاتیک و برنامه‌های ASP.NET


یک درخواست عمومی!
لطف کنید در پروژ‌های «جدید» خودتون این نوع کلاس‌های SqlHelper رو «دور بریزید». یاد گرفتن کار با یک ORM جدید اصلا سخت نیست. مثلا طراحی Entity framework مایکروسافت به حدی ساده است که هر شخصی با داشتن بهره هوشی در حد یک عنکبوت آبی یا حتی جلبک دریایی هم می‌تونه با اون کار کنه! فقط NHibernate هست که کمی مرد افکن است و گرنه مابقی به عمد ساده طراحی شده‌اند.
مزایای کار کردن با ORM ها این است:
- کوئری‌های حاصل از آن‌ها «پارامتری» است؛ که این دو مزیت عمده را به همراه دارد:
امنیت: مقاومت در برابر SQL Injection
سرعت و همچنین مصرف حافظه کمتر: با کوئری‌های پارامتری در SQL Server همانند رویه‌های ذخیره شده رفتار می‌شود.
- عدم نیاز به نوشتن DAL شخصی پر از باگ. چون ORM یعنی همان DAL که توسط یک سری حرفه‌ای طراحی شده.
- یک دست شدن کدها در یک تیم. چون همه بر اساس یک اینترفیس مشخص کار خواهند کرد.
- امکان استفاده از امکانات جدید زبان‌های دات نتی مانند LINQ و نوشتن کوئری‌های strongly typed تحت کنترل کامپایلر.
- پایین آوردن هزینه‌های آموزشی افراد در یک تیم. مثلا EF را می‌شود به عنوان یک پیشنیاز در نظر گرفت؛ عمومی است و همه گیر. کسی هم از شنیدن نام آن تعجب نخواهد کرد. کتاب(های) آموزشی هم در مورد آن زیاد هست.
و ...