مطالب
استفاده از افزونه‌ی jQuery Autocomplete در ASP.NET

با استفاده از AutoComplete TextBoxes می‌توان گوشه‌ای از زندگی روزمره‌ی کاربران یک برنامه را ساده‌تر کرد. مشکل مهم dropDownList ها دریک برنامه‌ی وب، عدم امکان تایپ قسمتی از متن مورد نظر و سپس نمایان شدن آیتم‌های متناظر با آن در اسرع وقت می‌باشد. همچنین با تعداد بالای آیتم‌ها هم حجم صفحه و زمان بارگذاری را افزایش می‌دهند. راه حل‌های بسیار زیادی برای حل این مشکل وجود دارند و یکی از آن‌ها ایجاد AutoComplete TextBoxes است. پلاگین‌های متعددی هم جهت پیاده سازی این قابلیت نوشته‌ شده‌اند منجمله jQuery Autocomplete . این پلاگین دیگر توسط نویسنده‌ی اصلی آن نگهداری نمی‌شود اما توسط برنامه نویسی دیگر در github ادامه یافته است. در ادامه نحوه‌ی استفاده از این افزونه‌ را در ASP.NET Webforms بررسی خواهیم کرد.

الف) دریافت افزونه

لطفا به آدرس GitHub ذکر شده مراجعه نمائید.

سپس برای مثال پوشه‌ی js را به پروژه افزوده و فایل‌های jquery-1.5.min.js ، jquery.autocomplete.js ، jquery.autocomplete.css و indicator.gif را در آن کپی کنید. فایل indicator.gif به همراه مجموعه‌ی دریافتی ارائه نمی‌شود و یک آیکن loading معروف می‌تواند باشد.
علاوه بر آن یک فایل جدید custom.js را نیز جهت تعاریف سفارشی خودمان اضافه خواهیم کرد.


ب) افزودن تعاریف افزونه به صفحه

در ذیل نحوه‌ی افزودن فایل‌های فوق به یک master page نمایش داده شده است.
در اینجا از قابلیت‌های جدید ScriptManager (موجود در سرویس پک یک دات نت سه و نیم و یا دات نت چهار) جهت یکی کردن اسکریپت‌ها کمک گرفته شده است. به این صورت تعداد رفت و برگشت‌ها به سرور به‌جای سه مورد (تعداد فایل‌های اسکریپت مورد استفاده)، یک مورد (نهایی یکی شده) خواهد بود و همچنین حاصل نهایی به صورت خودکار به شکلی فشرده شده به مرورگر تحویل داده شده، سرآیندهای کش شدن اطلاعات به آن اضافه می‌گردد (که در سایر حالات متداول اینگونه نیست)؛ به علاوه Url نهایی آن هم بر اساس hash فایل‌ها تولید می‌شود. یعنی اگر محتوای یکی از این فایل‌ها تغییر کرد، چون Url نهایی تغییر می‌کند، دیگر لازم نیست نگران کش شدن و به روز نشدن اسکریپت‌ها در سمت کاربر باشیم.

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="AspNetjQueryAutocompleteTest.Site" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<asp:PlaceHolder Runat="server">
<link href="<%= ResolveClientUrl("~/js/jquery.autocomplete.css")%>" rel="stylesheet" type="text/css" />
</asp:PlaceHolder>
<asp:ContentPlaceHolder ID="head" runat="server">
</asp:ContentPlaceHolder>
</head>
<body>
<form id="form1" runat="server">
<asp:ScriptManager ID="ScriptManager1" runat="server">
<CompositeScript>
<Scripts>
<asp:ScriptReference Path="~/js/jquery-1.5.min.js" />
<asp:ScriptReference Path="~/js/jquery.autocomplete.js" />
<asp:ScriptReference Path="~/js/custom.js" />
</Scripts>
</CompositeScript>
</asp:ScriptManager>
<div>
<asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
</asp:ContentPlaceHolder>
</div>
</form>
</body>
</html>
علت استفاده از ResolveClientUrl در حین تعریف فایل css در اینجا به عدم مجاز بودن استفاده از ~ جهت مسیر دهی فایل‌های css در header صفحه بر می‌گردد.


ج) افزودن یک صفحه‌ی ساده به برنامه
<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" AutoEventWireup="true"
CodeBehind="default.aspx.cs" Inherits="AspNetjQueryAutocompleteTest._default" %>

<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<asp:TextBox ID="txtShenas" runat="server" />
</asp:Content>

فرض کنید می‌خواهیم افزونه‌ی ذکر شده را به TextBox استاندارد فوق اعمال کنیم. ID این TextBox در نهایت به شکل ContentPlaceHolder1_txtShenas رندر خواهد شد. البته در ASP.NET 4.0 با تنظیم ClientIDMode=Static می‌توان ID انتخابی خود را به جای این ID خودکار درنظر گرفت و اعمال کرد. اهمیت این مساله در قسمت (ه) مشخص می‌گردد.


د) فراهم آوردن اطلاعات مورد استفاده توسط افزونه‌ی AutoComplete به صورت پویا

مهم‌ترین قسمت استفاده از این افزونه، تهیه‌ی اطلاعاتی است که باید نمایش دهد. این اطلاعات باید به صورت فایلی که هر سطر آن حاوی یکی از آیتم‌های مورد نظر است، تهیه گردد. برای این منظور می‌توان از فایل‌های ASHX یا همان Generic handlers استفاده کرد:

using System;
using System.Data.SqlClient;
using System.Text;
using System.Web;

namespace AspNetjQueryAutocompleteTest
{
public class AutoComplete : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string prefixText = context.Request.QueryString["q"];
var sb = new StringBuilder();

using (var conn = new SqlConnection())
{
//todo: این مورد باید از فایل کانفیگ خوانده شود
conn.ConnectionString = "Data Source=(local);Initial Catalog=MyDB;Integrated Security = true";
using (var cmd = new SqlCommand())
{
cmd.CommandText = @" select Field1 ,Field2 from tblData where Field1 like @SearchText + '%' ";
cmd.Parameters.AddWithValue("@SearchText", prefixText);
cmd.Connection = conn;
conn.Open();
using (var sdr = cmd.ExecuteReader())
{
if (sdr != null)
while (sdr.Read())
{
string field1 = sdr.GetValue(0) == DBNull.Value ? string.Empty : sdr.GetValue(0).ToString().Trim();
string field2 = sdr.GetValue(1) == DBNull.Value ? string.Empty : sdr.GetValue(1).ToString().Trim();
sb.AppendLine(field1 + "|" + field2);
}
}
}
}

context.Response.Write(sb.ToString());
}

public bool IsReusable
{
get
{
return false;
}
}
}
}

در این مثال از ADO.NET کلاسیک استفاده شده است تا به عمد نحوه‌ی تعریف پارامترها یکبار دیگر مرور گردند. اگر از LINQ to SQL یا Entity framework یا NHibernate و موارد مشابه استفاده می‌کنید، جای نگرانی نیست؛ زیرا کوئری‌های SQL تولیدی توسط این ORMs به صورت پیش فرض از نوع پارامتری هستند (+).
در این مثال اطلاعات دو فیلد یک و دوی فرضی از جدولی با توجه به استفاده از like تعریف شده دریافت می‌گردد. به عبارتی همان متد StartsWith معروف LINQ بکارگرفته شده است.
به صورت خلاصه افزونه، کوئری استرینگ q را به این فایل ashx ارسال می‌کند. سپس کلیه آیتم‌های شروع شده با مقدار دریافتی، از بانک اطلاعاتی دریافت شده و هر کدام قرارگرفته در یک سطر جدید بازگشت داده می‌شوند.
اگر دقت کرده باشید در قسمت sb.AppendLine ، با استفاده از "|" دو مقدار دریافتی از هم جدا شده‌اند. عموما یک مقدار کفایت می‌کند (در 98 درصد موارد) ولی اگر نیاز بود تا توضیحاتی نیز نمایش داده شود از این روش نیز می‌توان استفاده کرد. برای مثال یک مقدار خاص به همراه توضیحات آن به عنوان یک آیتم نمایش داده شده مد نظر است.


ه) اعمال نهایی افزونه به TextBox

در ادامه پیاده سازی فایل custom.js برای استفاده از امکانات فراهم شده در قسمت‌های قبل ارائه گردیده است:

function formatItem(row) {
return row[0] + "<br/><span style='text-align:justify;' dir='rtl'>" + row[1] + "</span>";
}

$(document).ready(function () {
$("#ContentPlaceHolder1_txtShenas").autocomplete('AutoComplete.ashx', {
//Minimum number of characters a user has to type before the autocompleter activates
minChars: 0,
delay: 5,
//Only suggested values are valid
mustMatch: true,
//The number of items in the select box
max: 20,
//Fill the input while still selecting a value
autoFill: false,
//The comparison doesn't looks inside
matchContains: false,
formatItem: formatItem
});
});

پس از این مقدمات، اعمال افزونه‌ی autocomplete به textBox ایی با id مساوی ContentPlaceHolder1_txtShenas ساده است. اطلاعات از فایل AutoComplete.ashx دریافت می‌گردد و تعدادی از خواص پیش فرض این افزونه در اینجا مقدار دهی شده‌اند. لیست کامل آن‌ها را در فایل jquery.autocomplete.js می‌توان مشاهده کرد.
تنها نکته‌ی مهم آن استفاده از پارامتر اختیاری formatItem است. اگر در حین تهیه‌ی AutoComplete.ashx خود تنها یک آیتم را در هر سطر نمایش می‌دهید و از "|" استفاده نکرده‌اید، نیازی به ذکر آن نیست. در این مثال ویژه، فیلد یک در یک سطر و فیلد دو در سطر دوم یک آیتم نمایش داده می‌شوند:



مطالب
یکی کردن اسمبلی‌های ارجاعی یک برنامه WPF با فایل خروجی آن
ممکن است برای شما هم پیش آمده باشد که بخواهید پس از پابلیش برنامه‌ای که نوشته‌اید، تمامی فایل‌های اسمبلی استفاده شده در برنامه را نیز با فایل خروجی آن ادغام کنید و به اصلاح تنها یک فایل، برای اجرا داشته باشید. مایکروسافت ابزاری را به نام ILMerge، برای اینکار معرفی کرده است که به وسیله آن، امکان ادغام اسمبلی‌ها با فایل اصلی برنامه وجود دارد؛ بجز اسمبلی‌های مربوط به WPF، به خاطر داشتن فایل‌های XAML.
برای حل این مسئله می‌توان از دو راه استفاده کرد:
  • اضافه کردن اسمبلی‌ها به صورت دستی به پروژه و تنظیم Build Action آن‌ها به Embedded Resource
  • تنظیم فایل csproj پروژه برای Embed کردن خودکار رفرنس‌های پروژه در زمان Build


روش اول

بعد از این که ارجاع اسمبلی مورد نظر را به پروژه اضافه کردید، نیاز است مقدار Copy Local آن‌ها را نیز در پنجره Properties به False تغییر دهید و سپس با استفاده از گزینه Add -> Existing Item فایل اسمبلی مورد نظر را به پروژه اضافه کرده و مقدار Build Action را در پنجره Properties به Embedded Resource تغییر دهید.
نکته: در صورتی که فایل اسمبلی به صورت unmanaged / native داشتید و امکان افزودن ارجاعی به آن وجود نداشت، تنها کافیست آن را به صورت Embedded Resource اضافه کنید.
تا به اینجا کار ادغام اسمبلی‌ها با فایل خروجی برنامه با موفقیت انجام شد و به علت یکسان بودن کد مربوط به بارگذاری اسمبلی‌ها، بعد از روش دوم، توضیح داده خواهد شد.


روش دوم

در این روش باید فایل csproj و یا vbproj برنامه را در یک ادیتور باز کرده ( یا با استفاده از گزینه Unload Project و انتخاب گزینه Edit projectName.csproj ) و در قسمت انتهای فایل، قبل از تگ Project، این کد را اضافه می‌کنیم:
<Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences">
  <ItemGroup>
    <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" />
    <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="'%(AssembliesToEmbed.Extension)' == '.dll'">
      <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName>
    </EmbeddedResource>
  </ItemGroup>
  <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" />
</Target>
<Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build">
  <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" />
</Target>
بعد از اضافه کردن این کد به فایل پروژه و بارگذاری مجدد پروژه، با اجرای برنامه یا Build کردن آن، در پوشه bin (پوشه خروجی برنامه) مشاهده می‌کنید که فایل‌های اسمبلی ارجاعی برنامه در این پوشه وجود ندارند و حجم فایل خروجی افزایش یافته است.

همانطور که در تصویر بالا نیز مشاهده می‌کنید، اسمبلی‌های ارجاعی برنامه TestApp به صورت Resource به آن اضافه شده‌اند.


نحوه بارگذاری اسمبلی‌های Embed شده

در پروژه‌های WPF، در OnStartup event کلاس App و در پروژه‌های WinForm در متد Main کلاس Program، قطعه کد زیر را وارد می‌کنیم:

private void App_OnStartup( object sender, StartupEventArgs e )
{
    AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
    var assembly = Assembly.GetExecutingAssembly();
    foreach (var name in assembly.GetManifestResourceNames())
    {
        if ( name.ToLower()
                 .EndsWith( ".resources" ) ||
             !name.ToLower()
                  .EndsWith( ".dll" ) )
            continue;
        EmbeddedAssembly.Load( name,
                               name );
    }
}

static Assembly OnResolveAssembly( object sender, ResolveEventArgs args )
{
    var fields = args.Name.Split( ',' );
    var name = fields[0];
    var culture = fields[2];
    if ( name.EndsWith( ".resources" ) &&
         !culture.EndsWith( "neutral" ) )
        return null;

    return EmbeddedAssembly.Get( args.Name );
}

با استفاده از رویداد AssemblyResolve می توان اسمبلی Embed شده را در زمانیکه نیاز به آن است، بارگذاری کرد. کد مربوط به کلاس EmbeddedAssembly نیز به این صورت می‌باشد:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Security.Cryptography;

public static class EmbeddedAssembly
{
    static Dictionary< string, Assembly > _dic;

    public static void Load( string embeddedResource,
                                string fileName )
    {
        if ( _dic == null )
            _dic = new Dictionary< string, Assembly >();

        byte[] ba;
        Assembly asm;
        var curAsm = Assembly.GetExecutingAssembly();

        using ( var stm = curAsm.GetManifestResourceStream( embeddedResource ) )
        {
            if ( stm == null )
                return;

            ba = new byte[(int)stm.Length];
            stm.Read( ba,
                      0,
                      (int)stm.Length );
            try
            {
                asm = Assembly.Load( ba );

                _dic.Add( asm.GetName().Name,
                            asm );
                return;
            }
            catch
            {
            }
        }

        bool fileOk;
        string tempFile;

        using ( var sha1 = new SHA1CryptoServiceProvider() )
        {
            var fileHash = BitConverter.ToString( sha1.ComputeHash( ba ) )
                                        .Replace( "-",
                                                    string.Empty );

            tempFile = Path.GetTempPath() + fileName;

            if ( File.Exists( tempFile ) )
            {
                var bb = File.ReadAllBytes( tempFile );
                var fileHash2 = BitConverter.ToString( sha1.ComputeHash( bb ) )
                                            .Replace( "-",
                                                        string.Empty );

                fileOk = fileHash == fileHash2;
            }
            else
            {
                fileOk = false;
            }
        }

        if ( !fileOk )
        {
            File.WriteAllBytes( tempFile,
                                ba );
        }

        asm = Assembly.LoadFile( tempFile );

        _dic.Add( asm.GetName().Name,
                    asm );
    }

    public static Assembly Get( string assemblyFullName )
    {
        if ( _dic == null ||
                _dic.Count == 0 )
            return null;

        var name = new AssemblyName( assemblyFullName ).Name;
        return _dic.ContainsKey( name )
            ? _dic[name]
            : null;
    }
}

با استفاده از متد Load کلاس بالا، کل اسمبلی‌هایی که بارگذاری شده‌اند در یک دیکشنری استاتیک نگهداری می‌شوند. ابتدا اسمبلی‌ها را با استفاده از []byte بارگذاری می‌کنیم و در صورتیکه بارگذاری اسمبلی با خطایی مواجه شود، بارگذاری را با استفاده از فایل temp انجام می‌دهیم (که معمولا برای فایل‌های unmanaged این مورد اتفاق می‌افتد).

با استفاده از متد Get که در زمان نیاز به یک اسمبلی توسط AssemblyResolve فراخوانی می‌شود، اسمبلی مربوطه از دیکشنری پیدا شده و برگشت داده می‌شود.


نکته ها

  • در صورتیکه بخواهید فایلی را از Embed کردن خودکار (روش دوم) استثناء کنید، باید از Condition استفاده کنید:
  <Target Name="EmbedReferencedAssemblies" AfterTargets="ResolveAssemblyReferences">
    <ItemGroup>
      <AssembliesToEmbed Include="@(ReferenceCopyLocalPaths)" />
      <EmbeddedResource Include="@(AssembliesToEmbed)" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(AssembliesToEmbed.Filename)', '^((?!Microsoft).)*$')) And '%(AssembliesToEmbed.Extension)' == '.dll'">
        <LogicalName>%(AssembliesToEmbed.DestinationSubDirectory)%(AssembliesToEmbed.Filename)%(AssembliesToEmbed.Extension)</LogicalName>
      </EmbeddedResource>
    </ItemGroup>
    <Message Importance="high" Text="Embedding: @(AssembliesToEmbed->'%(DestinationSubDirectory)%(Filename)%(Extension)', ', ')" />
  </Target>
  <Target Name="DeleteAllReferenceCopyLocalPaths" AfterTargets="Build">
    <Delete Files="@(ReferenceCopyLocalPaths->'$(OutDir)%(DestinationSubDirectory)%(Filename)%(Extension)')" Condition="$([System.Text.RegularExpressions.Regex]::IsMatch('%(Filename)', '^((?!Microsoft).)*$')) Or '%(Extension)' == '.xml'" />
  </Target>

برای نمونه در اینجا با استفاده از Regex، تمامی فایل‌هایی که شروع نام آنها با Microsoft است، استثناء شده‌اند. فقط توجه داشته باشید در صورتیکه شرطی را برای Embed کردن تعریف می‌کنید، حتما در هر دو قسمت، شرط را وارد کنید.
  • در صورتیکه بعد از اجرای برنامه و یا اجرای به صورت دیباگ با خطای Stackoverflow مواجه شدید که به خاطر ارجاعات زیاد Resource‌های برنامه پیش می‌آید، کد زیر را به فایل AssemblyInfo، در پوشه Properties اضافه کنید:
[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.MainAssembly)]


  • در صورتیکه پروژه شما از نوع Office Add-Ins باشد، باید در کد مربوط به AssemblyResolve را در فایل ThisAddIn.Designer.cs (در صورت عدم تغییر نام) به متد Initialize اضافه کنید و دستور بارگذاری را در متد ThisAddIn_Startup اضافه کنید. نکته خیلی مهم:  در فایل csproj حتما در قسمت Condition باید اسمبلی‌هایی را که با نام Microsoft شروع می‌شوند، از Embed شدن استثناء کنید و در قسمت DeleteAllReferenceCopyLocalPaths مقدار "AfterTargets="VisualStudioForApplicationsBuild را قرار دهید (تا امکان Build پروژه برای شما باشد) و همچنین پسوند vsto را نیز نباید حذف کنید.

نظرات مطالب
AngularJS #2
راه دیگه برای بارگزاری صفحات تعریف مسیر یاب است فرض کنید مسیر زیر را تعریف کرده ایم :
var PostApp = angular.module('PostApp', []).config(['$routeProvider',
  function ($routeProvider) {
      $routeProvider.
          when('/list', {
              templateUrl: '/Administrator/Post/Index',
              controller: 'PostController'
          });
  }]);
در قسمت templateUrl مسیر یک اکشن Index در کنترلری به نام Post است که یک partial view بر میگرداند به شکل زیر :
public virtual ActionResult Index(int? id)
{
      var model = new PostViewModels
      {
          //......
      };
      return PartialView(viewName: "_Index", model: model);
}
در این اکشن ما مدل را به partial view ارسال میکنیم و ویو توسط razor رندر میشود و نتیجه که یک فایل html است بازگشت داده میشود و ما میتوانیم داخل این html از امکانات Angular استفاده کنیم یعنی:
 "قبل از اینکه این فایل‌های cshtml تبدیل به html شوند و به کلاینت برگردانده شوند، من با razor عملیات دلخواه خود را انجام می‌دهم. "
@using ViewModels.Administrator.Post

//استفاده از امکانات Razor
@(Html.EnumDropDownListMenu<PostPermition, AppViewPostResource>("permition-", "{{item.id}}"))

//استفاده از امکانات Angular
<div  ng-controller="PostController">
     <ul>
           <li ng-repeat="item in ListOfItems">
                  {{item.Title}}
            </li>
    </ul>
</div>

مطالب
انجام اعمال ریاضی بر روی Generics
کامپایلر سی‌شارپ اگر نتواند نوع‌های عملوندها را در حین بکارگیری عملگرها تشخیص دهد، اجازه‌ی استفاده از عملگر را نخواهد داد و کار کامپایل، با یک خطا خاتمه می‌یابد. برای نمونه مثال زیر را در نظر بگیرید:
    public interface ICalculator<T>
    {
        T Add(T operand1, T operand2);
    }

    public class Calculator<T> : ICalculator<T>
    {
        public T Add(T operand1, T operand2)
        {
            return operand1 + operand2;
        }
    }
در اینجا چون کامپایلر نمی‌داند که عملگر + بر روی چه نوع‌هایی قرار است اعمال شود (به علت جنریک تعریف شدن این نوع‌ها و مشخص نبودن اینکه آیا این نوع، اصلا عملگر + دارد یا خیر)، با صدور خطای زیر، عملیات کامپایل را متوقف می‌کند:
 Operator '+' cannot be applied to operands of type 'T' and 'T'
برای حل این مساله، چندین روش مطرح شده‌است که در ادامه تعدادی از آن‌ها را مرور خواهیم کرد.


روش اول: واگذار کردن استراتژی عملیات ریاضی به یک کلاس خارجی

این راه حلی است که توسط اعضای تیم سی‌شارپ در روزهای ابتدایی معرفی جنریک‌ها مطرح شده‌است. فرض کنید می‌خواهیم لیستی از جنریک‌ها را با هم جمع بزنیم:
    public class Calculator2<T>
    {
        public T Sum(List<T> list)
        {
            T sum = 0;
            for (int i = 0; i < list.Count; i++)
                sum += list[i];
            return sum;
        }
    }
این کد نیز قابل کامپایل نبوده و امکان اعمال عملگر + بر روی نوع ناشناخته‌ی T میسر نیست.
    public interface ICalculator<T>
    {
        T Add(T operand1, T operand2);
    }

    public class Int32Calculator : ICalculator<int>
    {
        public int Add(int operand1, int operand2)
        {
            return operand1 + operand2;
        }
    }

    public class AlgorithmLibrary<T> where T : new() 
    {
        private readonly ICalculator<T> _calculator;
        public AlgorithmLibrary(ICalculator<T> calculator)
        {
            _calculator = calculator;
        }

        public T Sum(List<T> items)
        {
            var sum = new T();
            for (var i = 0; i < items.Count; i++)
            {
                sum = _calculator.Add(sum, items[i]);
            }
            return sum;
        }
    }
در راه حل ارائه شده، یک اینترفیس عمومی که متد جمع را تعریف کرده‌است، مشاهده می‌کنیم. سپس این اینترفیس در سازنده‌ی کتابخانه‌ی الگوریتم‌‌های برنامه تزریق شده‌است. اکنون کدهای AlgorithmLibrary بدون مشکل کامپایل می‌شوند. هر زمان که نیاز به استفاده از آن بود، بر اساس نوع T، پیاده سازی خاصی را باید ارائه داد. برای مثال در اینجا Int32Calculator پیاده سازی نوع int را انجام داده‌است. برای استفاده از آن نیز خواهیم داشت:
 var result = new AlgorithmLibrary<int>(new Int32Calculator()).Sum(new List<int> { 1, 2, 3 });

البته این نوع پیاده سازی را که کار اصلی آن واگذاری عملیات جمع، به یک کلاس خارجی است، توسط Func نیز می‌توان خلاصه‌تر کرد:
    public class Algorithms<T> where T : new() 
    {
        public T Calculate(Func<T, T, T> add, IEnumerable<T> numbers)
        {
            var sum = new T();
            foreach (var number in numbers)
            {
                sum = add(sum, number);
            }
            return sum;
        }
    }
استفاده از Action و Func نیز یکی دیگر از روش‌های تزریق وابستگی‌ها است که در اینجا بکار گرفته شده‌است. برای استفاده از آن خواهیم داشت:
 var result = new Algorithms<int>().Calculate((a, b) => a + b, new[] { 1, 2, 3 });
آرگومان اول روش جمع زدن را مشخص می‌کند و آرگومان دوم، لیستی است که باید اعضای آن جمع زده شوند.


روش دوم: استفاده از واژه‌ی کلیدی dynamic

با استفاده از واژه‌ی کلیدی dynamic می‌توان بررسی نوع داده‌ها را به زمان اجرا موکول کرد. به این ترتیب دیگر کامپایلر مشکلی با کامپایل قطعه کد ذیل نخواهد داشت:
    public class Calculator<T> : ICalculator<T>
    {
        public T Add(T operand1, T operand2)
        {
            return (dynamic)operand1 + operand2;
        }
    }
و مثال زیر نیز به خوبی کار می‌کند:
 var test = new Calculator<int>().Add(1, 2);
البته بدیهی است که نوع تعریف شده در اینجا باید دارای عملگر + باشد. در غیر اینصورت در زمان اجرا برنامه با یک خطا خاتمه خواهد یافت.
روش فوق نسبت به حالتی که بر اساس نوع T تصمیم‌گیری شود و از عملگر + متناظری استفاده گردد، خوانایی بهتری دارد:
public T Add(T t1, T t2)
{
    if (typeof(T) == typeof(double))
    {
        var d1 = (double)t1;
        var d2 = (double)t2;
        return (T)(d1 + d2);
    }
    else if (typeof(T) == typeof(int)){
        var i1 = (int)t1;
        var i2 = (int)t2;
        return (T)(i1 + i2);
    }
    else ...
}


روش سوم: استفاده از Expression Trees

روش زیر بسیار شبیه است به حالتیکه از Func در روش اول استفاده شد. در اینجا این Func به صورت پویا تولید و سپس صدا زده می‌شود:
using System;
using System.Linq.Expressions;

namespace GenericsArithmetic
{
    public class Solution3
    {
        public T Add<T>(T a, T b)
        {
            var paramA = Expression.Parameter(typeof(T), "a");
            var paramB = Expression.Parameter(typeof(T), "b");

            var body = Expression.Add(paramA, paramB);
            var add = Expression.Lambda<Func<T, T, T>>(body, paramA, paramB).Compile();
            return add(a, b);
        }
    }
}
البته این مثال، یک مثال ابتدایی در این مورد است. بر همین مبنا و ایده، یک کتابخانه‌ی با کارآیی بالا، تحت عنوان Generic Operators که جزو Misc utils می‌باشد، تهیه شده‌است.
به کمک کتابخانه‌ی Generic Operators، کدهای جمع زدن اعضای یک لیست جنریک به صورت ذیل خلاصه می‌شوند:
public static T Sum<T>(this IEnumerable<T> source)
{
    T sum = Operator<T>.Zero;
    foreach (T value in source)
    {
            sum = Operator.Add(sum, value);
    }
    return sum;
}
مطالب
نمایش درصد پیشرفت، Watermark و گزارشات چند ستونی در PdfReport
گزارشی را در نظر بگیرید با این نیازها:
می‌خواهیم
الف) یک Watermark قطری را بر روی تمام صفحات گزارش ظاهر کنیم.
ب) عدد‌های درصد پیشرفت یک ستون را به صورت میله‌ای نمایش دهیم.
ج) در هر صفحه بجای اینکه یک جدول، اطلاعات را نمایش دهد و تمام صفحه را پر کند، دو جدول در دو ستون کنار هم اینکار را انجام دهند تا در حین چاپ گزارش، در میزان تعداد صفحات مصرفی صرفه جویی صورت گیرد.
د) مقادیر true با چک مارک و موارد false با علامت ضربدر نمایش داده شوند.

یک چنین شکلی در نهایت مد نظر است:


روش انجام کار را توسط کتابخانه PdfReport در ادامه بررسی خواهیم کرد.
ابتدا کلاس مدل زیر را در نظر بگیرید:
namespace PdfReportSamples.Models
{
    public class Task
    {
        public int Id { set; get; }
        public string Name { set; get; }
        public int PercentCompleted { set; get; }
        public bool IsActive { set; get; }
    }
}
به این ترتیب یک کلاس فعالیت تعریف شده است که در آن نام فعالیت، درصد پیشرفت و همچنین درجریان بودن آن قابل تنظیم است. از این کلاس جهت تهیه منبع داده گزارش استفاده می‌شود:
using System;
using System.Collections.Generic;
using System.Drawing;
using iTextSharp.text;
using PdfReportSamples.Models;
using PdfRpt;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.ProgressReport
{
    public class ProgressReportPdfReport
    {
        private IPdfFont getWatermarkFont()
        {
            var watermarkFont = new GenericFontProvider(
                                        AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                        Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            watermarkFont.Color = BaseColor.LIGHT_GRAY;
            watermarkFont.Size = 50;
            return watermarkFont;
        }

        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
                doc.DiagonalWatermark(new DiagonalWatermark
                {
                    Text = "نمایش درصد پیشرفت",
                    RunDirection = PdfRunDirection.RightToLeft,
                    Font = getWatermarkFont()
                });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(PersianDate.ToPersianDateTime(DateTime.Now, "/", true));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.Message("گزارش جدید ما");
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                });
            })
            .MainTableTemplate(template =>
            {
                template.BasicTemplate(BasicTemplate.SilverTemplate);
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
                table.MultipleColumnsPerPage(new MultipleColumnsPerPage
                {
                    ColumnsGap = 20,
                    ColumnsPerPage = 2,
                    ColumnsWidth = 250,
                    IsRightToLeft = true,
                    TopMargin = 7
                });
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<Task>();
                var rnd = new Random();
                for (int i = 0; i < 400; i++)
                {
                    listOfRows.Add(new Task
                    {
                        Id = rnd.Next(1000, 10000),
                        Name = "Task" + i,
                        PercentCompleted = rnd.Next(1, 100),
                        IsActive = rnd.Next(0, 2) == 1 ? true : false
                    });
                }
                dataSource.StronglyTypedList<Task>(listOfRows);
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف", captionRotation: 90);
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره فعالیت");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("فعالیت");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.PercentCompleted);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("درصد پیشرفت");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.ProgressBar(progressBarColor: Color.SkyBlue, showPercentText: true);
                        template.DisplayFormatFormula(obj =>
                        {
                            if (obj == null) return "% 0";
                            return "% " + obj.ToString();
                        });
                    });
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<Task>(x => x.IsActive);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("در جریان");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed);
                    });
                });
            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\ProgressReportSample.pdf"));
        }
    }
}

توضیحات:

- همانطور که در کدهای فوق ملاحظه می‌کنید، برای تعریف یک watermark قطری در سراسر سند تولیدی، نیاز است در متد DocumentPreferences، تنظیمات DiagonalWatermark را مشخص کرد:
doc.DiagonalWatermark(new DiagonalWatermark
                {
                    Text = "نمایش درصد پیشرفت",
                    RunDirection = PdfRunDirection.RightToLeft,
                    Font = getWatermarkFont()
                });
در اینجا Text، متنی است که نمایش داده خواهد شد. تنظیم PdfRunDirection.RightToLeft برای نمایش صحیح متون فارسی الزامی است. همچنین این watermark نیاز به قلم مناسب و متفاوتی نسبت به قلم‌های پیش فرض گزارش نیز دارد:
        private IPdfFont getWatermarkFont()
        {
            var watermarkFont = new GenericFontProvider(
                                        AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                        Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            watermarkFont.Color = BaseColor.LIGHT_GRAY;
            watermarkFont.Size = 50;
            return watermarkFont;
        }
قلم‌هایی از جنس IPdfFont را توسط کلاس توکار GenericFontProvider به نحوی که ملاحظه می‌کنید می‌توان ایجاد کرد.

- برای ستون بندی گزارش باید به متد MainTablePreferences رجوع نمود. در اینجا می‌توان تنظیمات دقیق ستون‌های گزارش را مشخص کرد:
table.MultipleColumnsPerPage(new MultipleColumnsPerPage
                {
                    ColumnsGap = 20,
                    ColumnsPerPage = 2,
                    ColumnsWidth = 250,
                    IsRightToLeft = true,
                    TopMargin = 7
                });
برای مثال در اینجا 2 ستون در هر صفحه تعریف شده است (ColumnsPerPage). فاصله بین این ستون‌ها 20 است (ColumnsGap). عرض هر ستون 250 درنظر گرفته شده (ColumnsWidth) و همچنین توسط تنظیم IsRightToLeft، سبب خواهیم شد تا جداول از راست به چپ شروع و در صفحه نمایش داده شوند. (اگر به شماره ردیف‌ها در شکل ابتدای بحث دقت کنید، ردیف 1 در سمت راست صفحه قرار دارد).

- برای نمایش درصد پیشرفت در یک سلول خاص تنها کافی است قالب مخصوص آن‌را انتخاب و مقدار دهی کنیم:
                    column.ColumnItemsTemplate(template =>
                    {
                        template.ProgressBar(progressBarColor: Color.SkyBlue, showPercentText: true);
                        template.DisplayFormatFormula(obj =>
                        {
                            if (obj == null) return "% 0";
                            return "% " + obj.ToString();
                        });
                    });
قالب از پیش تعریف شده ProgressBar، مقدار سلول جاری را دریافت و آن‌را تبدیل به یک میله افقی درصد پیشرفت می‌کند. همچنین در اینجا توسط DisplayFormatFormula، یک علامت درصد هم به متنی که قرار است نمایش داده شود، اضافه کرده‌ایم.

- نمایش چک مارک و علامت ضربدر نیز به همین منوال است. باید قالب مناسبی را برای آن انتخاب و اعمال کرد:
                    column.ColumnItemsTemplate(template =>
                    {
                        template.Checkmark(checkmarkFillColor: Color.Green, crossSignFillColor: Color.DarkRed);
                    });
قالب Checkmark نیز جزو قالب‌های از پیش تعریف شده PdfReport است و بر اساس گرافیک برداری کار می‌کند.
نظرات مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
با سلام و تشکر بابت همه زحمات. می‌خواستم بدونم برای لاگین کردن به پروژه فعلی از طریق Xamarin چه تنظیماتی در سمت کلاینت بایستی انجام بشه. من در فایل Config کلاینتی با مشخصات زیر اضافه کردم:
new Client
                {
                    ClientName = "Native Client (Hybrid with PKCE)",
                    ClientId = "native.hybrid",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RedirectUris = new List<string>
                    {
                        "https://notused"
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://notused"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        IdentityServerConstants.StandardScopes.Address,
                        "roles",
                        "imagegalleryapi",
                        "country",
                        "subscriptionlevel"
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowOfflineAccess = true,
                    RequireClientSecret = false,
                    RequireConsent = false,
                    RequirePkce = true,
                    UpdateAccessTokenClaimsOnRefresh = true,
                    AccessTokenType = AccessTokenType.Reference,
                    RefreshTokenUsage = TokenUsage.ReUse
                }
و در سمت کلاینت این تنظیمات رو دارم
var options = new OidcClientOptions
            {
                Authority = "https://localhost:6001",
                ClientId = "native.hybrid",
                ClientSecret = "secret",
                Scope = "openid profile email imagegalleryapi",
                RedirectUri = "xamarinformsclients://callback",
                Browser = browser,

                ResponseMode = OidcClientOptions.AuthorizeResponseMode.Redirect
            };
ولی موقع اجرا پیغام زیر رو می‌گیرم:
System.InvalidOperationException: 'Error loading discovery document: Error connecting to https://localhost:6001/.well-known/openid-configuration. Network subsystem is down.'
می‌خواستم بدونم اشکال کارم کجاست؟ ممنون 
مطالب
سازگار سازی EFTracingProvider با EF Code first
برای ثبت SQL تولیدی توسط EF، ابزارهای پروفایلر زیادی وجود دارند (+). علاوه بر این‌ها یک پروایدر سورس باز نیز برای این منظور به نام EFTracingProvider موجود می‌باشد که برای EF Database first نوشته شده است. در ادامه نحوه‌ی استفاده از این پروایدر را در برنامه‌های EF Code first مرور خواهیم کرد.

الف) دریافت کدهای EFTracingProvider اصلی: (+)
از کدهای دریافتی این مجموعه، فقط به دو پوشه EFTracingProvider و EFProviderWrapperToolkit آن نیاز است.

ب) اصلاح کوچکی در کدهای این پروایدر جهت بررسی نال بودن شیء‌ایی که باید dispose شود
در فایل DbConnectionWrapper.cs، متد Dispose را یافته و به نحو زیر اصلاح کنید (بررسی نال نبودن wrappedConnection اضافه شده است):

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.wrappedConnection != null)
                    this.wrappedConnection.Dispose();
            }

            base.Dispose(disposing);
        }

ج) ساخت یک کلاس پایه Context با قابلیت لاگ فرامین SQL صادره، جهت میسر سازی استفاده مجدد از کدهای آن
د) رفع خطای The given key was not present in the dictionary در حین استفاده از EFTracingProvider

در ادامه کدهای کامل این دو قسمت به همراه یک مثال کاربردی را ملاحظه می‌کنید:

using System;
using System.Configuration;
using System.Data;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
using System.Diagnostics;
using System.Linq;
using EFTracingProvider;

namespace Sample
{
    public class Person
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            var className = this.ContextType.FullName;
            var connectionStringData = ConfigurationManager.ConnectionStrings[className];
            if (connectionStringData == null)
                throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className));

            TargetDatabase = new DbConnectionInfo(connectionStringData.ConnectionString, connectionStringData.ProviderName);
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            for (int i = 0; i < 7; i++)
                context.Users.Add(new Person { Name = "name " + i });

            base.Seed(context);
        }
    }

    public class MyContext : MyLoggedContext
    {
        public DbSet<Person> Users { get; set; }
    }

    public abstract class MyLoggedContext : DbContext
    {
        protected MyLoggedContext()
            : base(existingConnection: createConnection(), contextOwnsConnection: true)
        {
            var ctx = ((IObjectContextAdapter)this).ObjectContext;
            ctx.GetTracingConnection().CommandExecuting += (s, e) =>
            {
                Console.WriteLine("{0}\n", e.ToTraceString());
            };
        }

        private static DbConnection createConnection()
        {
            var st = new StackTrace();
            var sf = st.GetFrame(2); // Get the derived class Type in a base class static method
            var className = sf.GetMethod().DeclaringType.FullName;
            
            var connectionStringData = ConfigurationManager.ConnectionStrings[className];
            if (connectionStringData == null)
                throw new InvalidOperationException(string.Format("ConnectionStrings[{0}] not found.", className));

            if (!isEFTracingProviderRegistered())
                EFTracingProviderConfiguration.RegisterProvider();

            EFTracingProviderConfiguration.LogToFile = "log.sql";
            var wrapperConnectionString =
                string.Format(@"wrappedProvider={0};{1}", connectionStringData.ProviderName, connectionStringData.ConnectionString);
            return new EFTracingConnection { ConnectionString = wrapperConnectionString };
        }

        private static bool isEFTracingProviderRegistered()
        {
            var data = (DataSet)ConfigurationManager.GetSection("system.data");
            var providerFactories = data.Tables["DbProviderFactories"];
            return providerFactories.Rows.Cast<DataRow>()
                                         .Select(row => (string)row.ItemArray[1])
                                         .Any(invariantName => invariantName == "EF Tracing Data Provider");
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
            using (var ctx = new MyContext())
            {
                var users = ctx.Users.AsEnumerable();
                if (users.Any())
                {
                    foreach (var user in users)
                    {
                        Console.WriteLine(user.Name);
                    }
                }

                var rnd = new Random();
                var user1 = ctx.Users.Find(1);
                user1.Name = "test user " + rnd.Next();
                ctx.SaveChanges();
            }

        }
    }
}

توضیحات:
تعریف TargetDatabase در Configuration سبب می‌شود تا خطای The given key was not present in the dictionary در حین استفاده از این پروایدر جدید برطرف شود. به علاوه همانطور که ملاحظه می‌کنید اطلاعات رشته اتصالی بر اساس قراردادهای توکار EF Code first به نام کلاس Context تنظیم شده است.
کلاس MyLoggedContext، کلاس پایه‌ای است که تنظیمات اصلی «EF Tracing Data Provider» در آن قرار گرفته‌اند. برای استفاده از آن باید رشته اتصالی مخصوصی تولید و در اختیار کلاس پایه DbContext قرار گیرد (توسط متد createConnection ذکر شده).
به علاوه در اینجا توسط خاصیت EFTracingProviderConfiguration.LogToFile می‌توان نام فایلی را که قرار است عبارات SQL تولیدی در آن درج شوند، ذکر نمود. همچنین یک روش دیگر دستیابی به کلیه عبارات SQL تولیدی را با مقدار دهی CommandExecuting در سازنده کلاس مشاهده می‌کنید.
اکنون که این کلاس پایه تهیه شده است، تنها کافی است Context معمولی برنامه به نحو زیر تعریف شود:
 public class MyContext : MyLoggedContext
در ادامه اگر متد RunTests را اجرا کنیم، خروجی ذیل را می‌توان در کنسول مشاهده کرد:
insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 0"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 1"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 2"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 3"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 4"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 5"

insert [dbo].[People]([Name])
values (@0)
select [Id]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = scope_identity()
-- @0 (dbtype=String, size=-1, direction=Input) = "name 6"

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name]
FROM [dbo].[People] AS [Extent1]

SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Name] AS [Name]
FROM [dbo].[People] AS [Extent1]

name 0
name 1
name 2
name 3
name 4
name 5
name 6

update [dbo].[People]
set [Name] = @0
where ([Id] = @1)
-- @0 (dbtype=String, size=-1, direction=Input) = "test user 1355460609"

-- @1 (dbtype=Int32, size=0, direction=Input) = 1

قسمتی از این خروجی مرتبط است به متد Seed تعریف شده که تعدادی رکورد را در بانک اطلاعاتی ثبت می‌کند.
دو select نیز در انتهای کار قابل مشاهده است. اولین مورد به علت فراخوانی متد Any صادر شده است و دیگری به حلقه foreach مرتبط می‌باشد (چون از AsEnumerable استفاده شده، هربار ارجاع به شیء users، یک رفت و برگشت به بانک اطلاعاتی را سبب خواهد شد. برای رفع این حالت می‌توان از متد ToList استفاده کرد.)
در پایان کار، متد update مربوط است به فراخوانی متدهای find و save changes ذکر شده. این خروجی در فایل sql.log نیز در کنار فایل اجرایی برنامه ثبت شده و قابل مشاهده می‌باشد.

کاربردها
اطلاعات این مثال می‌تواند پایه نوشتن یک برنامه entity framework profiler باشد.
 
مطالب
امکان استفاده از کتابخانه‌های native در Blazor WASM 6x
کتابخانه‌‌های بسیاری هستند که به زبان‌های C ، C++ ، Rust و امثال آن تهیه شده‌اند. دات نت 6، قابلیت جدید استفاده‌ی از این نوع کتابخانه‌ها را بدون نیاز به تبدیل کدهای آن‌ها به #C، به برنامه‌های سمت کلاینت Blazor Web Assembly اضافه کرده که در این مطلب، نمونه‌ای از آن‌را با استفاده از بانک اطلاعاتی SQLite در برنامه‌های Blazor WASM 6x، بررسی خواهیم کرد. یعنی یک برنامه‌ی SPA سمت کلاینت که بدون نیاز به سرور و Web API، تنها با استفاده از EF-Core و بانک اطلاعاتی بومی SQLite می‌تواند اطلاعات مورد نیاز خود را ثبت و یا بازیابی کند (همه چیز داخل مرورگر رخ می‌دهد).


ایجاد یک پروژه‌ی Blazor WASM جدید

یک پوشه‌ی جدید دلخواه را به نام BlazorWasmSQLite ایجاد کرده و با اجرای دستور dotnet new blazorwasm، یک پروژه‌ی Blazor Web Assembly خالی جدید را در آن آغاز می‌کنیم. همانطور که از دستور نیز مشخص است، این پروژه از نوع hosted که به همراه Web API هم هست، نمی‌باشد.


افزودن Context و مدل EF-Core به برنامه

مدل برنامه به صورت زیر در پوشه‌ی Models آن قرار می‌گیرد:
namespace BlazorWasmSQLite.Models;

public class Car
{
  public int Id { get; set; }

  public string Brand { get; set; }

  public  int Price { get; set; }
}
و Context ای که آن‌را در معرض دید قرار می‌دهد، به صورت زیر تعریف خواهد شد:
using Microsoft.EntityFrameworkCore;
using BlazorWasmSQLite.Models;

namespace BlazorWasmSQLite.Data;

public class ClientSideDbContext : DbContext
{
  public DbSet<Car> Cars { get; set; } = default!;

  public ClientSideDbContext(DbContextOptions<ClientSideDbContext> options) :
    base(options)
  {
  }
}
همچنین چون می‌خواهیم از بانک اطلاعاتی SQLite استفاده کنیم، وابستگی زیر را به فایل csproj برنامه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <!-- EF Core and Sqlite -->
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
  </ItemGroup>
</Project>
سپس این Context را به نحو زیر به فایل Program.cs معرفی می‌کنیم:
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using BlazorWasmSQLite;
using Microsoft.EntityFrameworkCore;
using BlazorWasmSQLite.Data;

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

// Sets up EF Core with Sqlite
builder.Services.AddDbContextFactory<ClientSideDbContext>(options =>
      options
        .UseSqlite($"Filename=DemoData.db")
        .EnableSensitiveDataLogging());

await builder.Build().RunAsync();
در مورد علت استفاده‌ی از AddDbContextFactory و نکات مرتبط با آن، به مطلب «نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server» مراجعه نمائید.


ثبت تعدادی رکورد در بانک اطلاعاتی

در ادامه سعی می‌کنیم در فایل Index.razor، تعدادی رکورد را به بانک اطلاعاتی اضافه کنیم:
@page "/"

@using Microsoft.Data.Sqlite
@using Microsoft.EntityFrameworkCore
@using BlazorWasmSQLite.Data
@using BlazorWasmSQLite.Models


<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

@code {
  [Inject]
  private IDbContextFactory<ClientSideDbContext> _dbContextFactory { get; set; } = default!;

  protected override async Task OnInitializedAsync()
  {
    await using var db = await _dbContextFactory.CreateDbContextAsync();
    await db.Database.EnsureCreatedAsync();

    // create seed data
    if (!db.Cars.Any())
    {
      var cars = new[]
      {
        new Car { Brand = "Audi", Price = 21000 },
        new Car { Brand = "Volvo", Price = 11000 },
        new Car { Brand = "Range Rover", Price = 135000 },
        new Car { Brand = "Ford", Price = 8995 }
      };

      await db.Cars.AddRangeAsync(cars);
      await db.SaveChangesAsync();
    }

    await base.OnInitializedAsync();
  }
}
در این مثال سعی شده‌است ساده‌ترین حالت ممکن کار با EF-Core در پیش گرفته شود؛ چون هدف اصلی آن، دسترسی به SQLite است.


اولین سعی در اجرای برنامه

در ادامه سعی می‌کنیم تا برنامه را اجرا کنیم. با خطای زیر متوقف خواهیم شد:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component:
The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. System.TypeInitializationException:
The type initializer for 'Microsoft.Data.Sqlite.SqliteConnection' threw an exception. ---> System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation. ---> System.DllNotFoundException: e_sqlite3 at
SQLitePCL.SQLite3Provider_e_sqlite3.SQLitePCL.ISQLite3Provider.sqlite3_libversion_number()
عنوان می‌کند که فایل‌های بانک اطلاعاتی SQLite به همراه EF-Core را نمی‌تواند پیدا کند. یا به عبارتی هر DLL بومی را نمی‌توان داخل مرورگر اجرا کرد.


رفع مشکل کار با SQLite با کامپایل ویژه‌ی آن

برای دسترسی به کدهای native در Blazor WASM و مرورگر، باید آن‌ها را توسط کامپایلر emcc به صورت زیر کامپایل کرد:
$ git clone https://github.com/cloudmeter/sqlite
$ cd sqlite
$ emcc sqlite3.c -shared -o e_sqlite3.o
در اینجا هر نوع فایل portable native code با فرمت‌های o. یا object files، .a و یا archive files و یا .bc یا bitcode و یا .wasm یا Standalone WebAssembly modules توسط Blazor wasm قابل استفاده هستند که در مثال فوق نمونه‌ی object files آن‌ها توسط کامپایلر تولید می‌شود.
مرحله‌ی بعد، معرفی این object file تولید شده به برنامه است. برای اینکار ابتدا باید dotnet workload install wasm-tools را نصب کرد (مهم). سپس به فایل csproj برنامه مراجعه کرده و فایل e_sqlite3.o را به آن معرفی می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
  <ItemGroup>
    <!-- EF Core and Sqlite -->
    <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.1" />
    <NativeFileReference Include="Data\e_sqlite3.o" />
  </ItemGroup>
</Project>
در اینجا فرض شده‌است که فایل o. حاصل، در پوشه‌ی data قرار دارد. این نوع فایل‌ها توسط NativeFileReferenceها به برنامه معرفی می‌شوند.


سعی در اجرای مجدد برنامه

پس از نصب wasm-tools و ذکر NativeFileReference فوق، اکنون اگر برنامه را اجرا کنیم، برنامه بدون مشکل اجرا خواهد شد:



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmSQLite.zip
نظرات اشتراک‌ها
ارتقا به dotnet core 2
با سلام و احترام
قسمت آخر مقاله یعنی قطعه کد
// Only used by EF Tooling
        public static IWebHost BuildWebHost(string[] args)
        {
            return WebHost.CreateDefaultBuilder()
                .ConfigureAppConfiguration((ctx, cfg) =>
                {
                    cfg.SetBasePath(Directory.GetCurrentDirectory())
                        .AddJsonFile("appsettings.json", true) // require the json file!
                        .AddEnvironmentVariables();
                })
                .ConfigureLogging((ctx, logging) => { }) // No logging
                .UseStartup()
                .UseSetting("DesignTime", "true")
                .Build();
        }
راه حل استانداردی نمیباشد و البته که با این حال نیاز به تکمیل شدن دارد به این شکل که باید در فایل startup.cs از تنظیم اضافه شده یعنی
UseSetting("DesignTime", "true")
جهت جلوگیری از Initial شدن دیتابیس در هنگام استفاده از Entity Framework Core Tooling به طور مثال عملیات Migration استفاده کرد
if (_config["DesignTime"] != "true")
{
  using (var scope = app.ApplicationServices.CreateScope())
  {
    var initializer = scope.ServiceProvider.GetRequiredService<YourSampleDataInitializer>();
    initializer.RunAsync().Wait();
  }
}
در ادامه روش توصیه شده توسط مایکروسافت پیاده سازی اینترفیس جدید IDesignTimeDbContextFactory میباشد که به واسطه آن EF موفق به یافتن Context برنامه در هنگام عملیات Migration میشود.قطعه کد زیر نمونه ای از این پیاده سازی است.
public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
    {
        public BloggingContext Create(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseSqlite("Data Source=blog.db");

            return new BloggingContext(optionsBuilder.Options);
        }
    }
البته همه این موارد زمانی درست کار خواهد کرد که فایل Program.cs برنامه را نیز به شکل جدید آن یعنی
public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Startup>()
                .Build();
تغییر دهیم.دلیل آن هم ... EF Core 2.0: design-time DbContext discovery
نظرات مطالب
نحوه‌ی نگاشت فیلدهای فرمول در Fluent NHibernate
از پاسخگویی شما بسیار ممنونم. من هر روز از شما مطلب جدیدی یاد میگیرم.
من قصد کشدار کردن بحث رو ندارم و اینم آخرین ارسال من در مورد این بحث است.فکر می کنم نتونستم منظورم رو واضح برسونم. فرض کنیم کلاس زیر وجود داره:
public class Project
{
public virtual int Id { get; set; }
public virtual long ProjectCode { get; set; }
public virtual string Name { get; set; }
public virtual int CreateDate { get; set; }

public virtual string SepratedDate
{
get { return myFunc(CreateDate); }
private set { ; }
}
}

من میخواهم در متد زیر لیستی از کلاس بالا رو به DataSet تبدیل کنم:

public DataSet dsGetAll(bool includeArchived)
{
using (var repository = new Repository())
{
var projects = repository.Find(x => x.IsArchive == includeArchived
);

var ds = new CollectionToDataSet>(projects.ToList());

return ds.CreateDataSet();
}
}

ولی خطا می ده که SepratedDate در جدول وجود نداره!!!
{"Invalid column name 'SepratedDate'."}
could not execute query
[ select project0_.Id as Id15_, project0_.ProjectCode as ProjectC2_15_, project0_.Name as Name15_, project0_.IsArchive as IsArchive15_, project0_.CreateDate as CreateDate15_, project0_.SepratedDate as Seprated6_15_ from tblProject project0_ where case when project0_.IsArchive=1 then 'true' else 'false' end=case when @p0='true' then 'true' else 'false' end