نظرات مطالب
نحوه صحیح تولید Url در ASP.NET MVC
در View مورد نظر یک section تعریف کنید به نحو زیر (که در آن می‌شود متغیرها را تعریف کرد):
@Section scriptVariables {
    <script type="text/javascript">
        var variableA = '@MyVarA';
    </script>
}
سپس در فایل layout سایت، پیش از الحاق اسکریپت اصلی آن‌را فراخوانی کنید:
<head>
    <meta charset="utf-8" />
    @RenderSection("scriptVariables", false)

    <script src="@Url.Content("~/Scripts/custom.js")" type="text/javascript"></script>
</head>

نظرات مطالب
نحوه ایجاد یک اسلایدشو به صورت داینامیک
با سلام ..
من همه چیز را همان طوری که شما گفتید انجام دادم ولی نمی‌دونم چرا اجرا نی کنه ..بعد از لود صفحه یک لحظه نشون میده بعد فقط فلش که برای حرکت تصویر است نشون میده و اون هم در گوشه بالای صفحه :
این هم کد :::
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Web;


public class GallerySite
{
    int iD;

    public int ID
    {
        get { return iD; }
        set { iD = value; }
    }
    string imagePath;

    public string ImagePath
    {
        get { return imagePath; }
        set { imagePath = value; }
    }
    string imageText;

    public string ImageText
    {
        get { return imageText; }
        set { imageText = value; }
    }
    public List<GallerySite> GetImage()
    {
        List<GallerySite> _Gallery = new List<GallerySite>() { };
        SqlConnection cnn = new SqlConnection("Data Source=.;Initial Catalog=RoshanZamirDataBase;Integrated Security=True");
        cnn.Open();
        SqlCommand cmd = new SqlCommand("Select * From Gallery", cnn);
        SqlDataReader datareadfewr = cmd.ExecuteReader();
        if (datareadfewr.HasRows)
        {
            while (datareadfewr.Read())
            {
                _Gallery.Add(new GallerySite()
                {
                    ID = Convert.ToInt32(datareadfewr["ID"]),
                    ImagePath = (string)datareadfewr["ImagePath"],
                    ImageText=(string)datareadfewr["ImageText"]
                });
            }
        }
        return _Gallery;
    }
}

.................................................................................................................................




    <link href="orbit-1.2.3.css" rel="stylesheet" />
    <script src="jquery-1.8.3.min.js"></script>
    <script src="jquery.orbit-1.2.3.min.js"></script>
    <script type="text/javascript">
        $(function () {
            $.ajax({
                url: "Handler.ashx",
                contentType: "application/json; charset=utf-8",
                success: function (data) {
                    $.each(data, function (i, b) {
                        var str = '<img src="' + b.ImagePath + '" alt="' + b.ImageText + '"/>';
                        $("#featured").append(str);
                    });
                    $('#featured').orbit();
                },
                dataType: "json"
            });
        });
    </script>




...........................................................................................................................

Handler :



public class Handler : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {
        var _Gallery = new GallerySite();
        var List = _Gallery.GetImage();
        string str = JsonConvert.SerializeObject(List);
        context.Response.Write(str);          
    } 
    public bool IsReusable {
        get {
            return false;
        }
    }
}


مطالب
یکی کردن اسمبلی‌های ارجاعی یک برنامه 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 را نیز نباید حذف کنید.

نظرات مطالب
بررسی چک لیست امنیتی web.config
آیا شما هم برای اجرا نشدن فایل‌های aspx از این وبکانفیگ در پوشه آپلودتون استفاده میکنید؟ آیا روش من درست است؟
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
</configuration>
اما نسخه اصلی(از OrchardCMS)
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Enabled" value="false" />
  </appSettings>
  <system.web>
    <httpHandlers>
      <!-- iis6 - for any request in this location, return via managed static file handler -->
      <add path="*" verb="*" type="System.Web.StaticFileHandler" />
    </httpHandlers>
  </system.web>
  <system.webServer>
    <staticContent>
      <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
    </staticContent>

    <handlers accessPolicy="Script,Read">
      <!--
      iis7 - for any request to a file exists on disk, return it via native http module.
      accessPolicy 'Script' is to allow for a managed 404 page.
      -->
      <add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
    </handlers>
  </system.webServer>
</configuration>
مطالب
Long Polling در WCF
به صورت پیش فرض سرویس‌های WCF به صورت Sync اجرا خواهند شد، یعنی هر گاه درخواستی از سمت کلاینت به سرور ارسال شود سرور بعد از پردازش درخواست پاسخ مورد نظر را به کلاینت باز می‌گرداند. اما حالتی را در نظر بگیرید که بعد از دریافت Request از کلاینت بنا به دلایلی امکان پاسخ گویی سمت سرور در آن لحظه وجود ندارد. خوب چه اتفاقی خواهد افتاد؟
در این حالت thread جاری سمت کلاینت نیز در حالت wait است و برنامه سمت کلاینت از کار می‌افتد تا زمانی که پاسخ از سرور دریافت نماید. اما در WCF به صورت پیش فرض هر درخواست ارسالی باید در طی یک دقیقه در اختیار سرور قرار گیرد و سرور نیز باید در طی یک دقیقه پاسخ مورد نظر را برگرداند(مقادیر خواص SendTimeout و ReceiveTimeout برای مدیریت این موارد به کار می‌روند). افزایش مقادیر این خواص کمک خاصی به این حالت نمی‌کند زیرا هم چنان کلاینت در حالت wait است و سرور نیز پاسخ خاصی ارسال نمی‌کند. حتی اگر کل عملیات را به صورت Async پیاده سازی نماییم باز ممکن است بعد از منقضی شدن زمان پردازش با یک TimeoutException برنامه از کار بیفتد. برای حل اینگونه موارد پیاده سازی سرویس‌ها به صورت Long Polling به ما کمک خوبی خواهد کرد.
حال سناریو زیر را در نظر بگیرید:
سمت سرور:
»یک درخواست دریافت می‌شود؛
»سرور در حالت wait (البته توسط یک thread دیگر) منتظر تامین منابع برای پاسخ به کلاینت است؛
»در نهایت پاسخ مورد نظر ارسال خواهد شد.
سمت کلاینت:
»درخواست مورد نظر به سرور ارسال می‌شود؛
»کلاینت منتظر پاسخ از سمت سرور است(البته توسط یک Thread دیگر)؛
»اگر در حین انتظار برای پاسخ از سمت سرور، با یک TimeoutException روبرو شدیم به جای توقف برنامه و نمایش پیغام خطای  Server is not available، باید عملیات به صورت خودکار restart شود.
»در نهایت پاسخ مورد نظر دریافت خواهد شد.
پیاده سازی این سناریو در WCF کار پیچیده ای نیست. بدین منظور می‌توانید از کلاس زیر استفاده کنید( لینک دانلود ). سورس آن به صورت زیر است:
    public abstract class LongPollingAsyncResult<TResult> : IAsyncResult where TResult : class
    {
        #region - Fields -

        private AsyncCallback _callback;
        private TimeSpan _timoutSpan;
        private TimeSpan _intervalWaitSpan;

        #endregion

        #region - Properties -
        public Exception Exception { get; private set; }
    
        public TResult Result { get; private set; }
     
        public object SyncRoot { get; private set; }

        #endregion

        #region - Ctor -
      
        public LongPollingAsyncResult(AsyncCallback callback, object asyncState, int timeoutSeconds = 300, int intervalWaitMilliseconds = 500)
        {
            SyncRoot = new object();
            _callback = callback;
            AsyncState = asyncState;
            AsyncWaitHandle = new ManualResetEvent(IsCompleted);
            _timoutSpan = TimeSpan.FromSeconds(timeoutSeconds);
            _intervalWaitSpan = TimeSpan.FromMilliseconds(intervalWaitMilliseconds);

            ThreadPool.QueueUserWorkItem(new WaitCallback(LoopWithIntervalAndTimeout));
        }

        #endregion

        #region - Private Helper Methods -

        private void LoopWithIntervalAndTimeout(object input)
        {
            try
            {
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                while (!IsCompleted)
                {
                    if (stopwatch.Elapsed > _timoutSpan)
                        throw new TimeoutException();
                    
                    DoWork();

                    if (!IsCompleted)
                        Thread.Sleep(_intervalWaitSpan);
                }
            }
            catch (Exception e)
            {
                Complete(null, e);
            }
        }

        #endregion

        #region - Protected/Abstract Methods -
        protected void Complete(TResult result, Exception e = null, bool completedSynchronously = false)
        {
            lock (SyncRoot)
            {
                CompletedSynchronously = completedSynchronously;
                Result = result;
                Exception = e;
                IsCompleted = true;

                if (_callback != null)
                    _callback(this);
            }
        }
       
        protected abstract void DoWork();

        #endregion

        #region - Public Methods -
        
        public TResult WaitForResult()
        {
            if (!IsCompleted)
                AsyncWaitHandle.WaitOne();

            if (Exception != null)
            {
                if (Exception is TimeoutException && WebOperationContext.Current != null)
                    WebOperationContext.Current.OutgoingResponse.StatusCode = HttpStatusCode.RequestTimeout;

                throw Exception;
            }

            return Result;
        }

        #endregion

        #region - IAsyncResult Implementation -

        public object AsyncState { get; private set; }
        
        public WaitHandle AsyncWaitHandle { get; private set; }

        public bool CompletedSynchronously { get; private set; }
       
        public bool IsCompleted { get; private set; }

        #endregion
    }
در این حالت شما می‌توانید حداکثر زمان مورد نیاز برای درخواست را به عنوان پارامتر از طریق سازنده کلاس بالا تعیین نمایید. اگر این زمان بیش از زمان تعیین شده در خواص SendTimeout و ReceiveTimeout بود بعد از منقضی شدن زمان پردازش درخواست، به جای دریافت TimeoutException عملیات پردازش به کار خود ادامه خواهد داد.
برای استفاده از کلاس تهیه شده ابتدا باید عملیات خود را به صورت Async پیاده سازی نمایید که در این مقاله به صورت کامل شرح داده شده است.
یک مثال
قصد داریم Operation زیر را به صورت Long Polling پیاده سازی نماییم:
[OperationContract]
public string GetNotification();
ابتدا متد زیر باید به صورت Async تبدیل شود:
[OperationContract(AsyncPattern = true)]
public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state);
 
public string EndWaitNotification(IAsyncResult result);
حال نوع بازگشتی سرویس مورد نظر را با استفاده از کلاس LongPollingAsyncResult به صورت زیر ایجاد خواهیم کرد:
public class MyNotificationResult : LongPollingAsyncResult<string>
{
   protected override DoWork()
    {
        کد‌های مورد نظر را اینجا قرار دهید
        base.Complete(...)
    }
}
در نهایت پیاده سازی متد‌های Begin و End همانند ذیل خواهد بود:
public IAsyncResult BeginWaitNotification(AsyncCallback callback, object state)
{
    return new MyNotificationResult(callback, state);
}
 
public string EndWaitNotification(IAsyncResult result)
{
    MyNotificationResult myResult = result as MyNotificationResult;
    if(myResult == null)
        throw new ArgumentException("result was of the wrong type!");
 
    myResult.WaitForResult();
    return myResult.Result;
}
در این حالت کلاینت می‌تواند یک درخواست به صورت LongPolling به سرور ارسال نماید و البته مدیریت این درخواست در یک thread دیگر انجام می‌گیرد که نتیجه آن از عدم تداخل پردازش این درخواست با سایر قسمت‌های برنامه است.

مطالب
چگونه تشخیص دهیم اسمبلی دات نت ما وصله شده است؟

یکی از روش‌هایی که برای بررسی یکپارچگی فایل‌ها مورد استفاده قرار می‌گیرد و عموما در دنیای سخت افزار و firmware های نوشته شده برای آن‌ها مرسوم است، قرار دادن CRC32 فایل در قسمتی از فایل و بررسی آن حین Boot سیستم است. اگر CRC32 جدید با CRC32 اصلی یکسان نباشد به این معنا است که فایل در حال اجرا پیش تر دستکاری شده است.
اما در دات نت فریم ورک روش متداول اینکار چیست؟ برای این منظور اضافه کردن امضای دیجیتال به فایل و اسمبلی نهایی تولیدی (فایل exe یا dll تولیدی) توصیه می‌شود (مراجعه به قسمت خواص پروژه و افزودن امضای دیجیتال جدید فقط با چند کلیک، +).
این مورد خوب است (با توجه به اینکه از الگوریتم‌های RSA و SHA1 استفاده می‌کند)، لازم است، اما کافی نیست زیرا ابزارهای حذف آن وجود دارند. به عبارتی برای وصله کردن این فایل‌ها فقط کافی است این امضای دیجیتال حذف شود و زمانی هم که نباشد، بررسی خاصی در مورد یکپارچگی فایل صورت نخواهد گرفت.
اما اگر باز هم نگران patch یا وصله شدن اسمبلی دات نت خود هستید این مورد افزودن امضای دیجیتال را حتما انجام دهید. مهم‌ترین خاصیت آن این است که یک سری تابع native در دات نت فریم ورک برای بررسی نبود آن وجود دارند (+):
[DllImport("mscoree.dll", CharSet=CharSet.Unicode)]
public static extern bool StrongNameSignatureVerificationEx(string wszFilePath, bool fForceVerification, ref bool pfWasVerified);

wszFilePath مسیر فایلی است که باید بررسی شود.
fForceVerification آیا متغیر pfWasVerified نیز مقدار دهی گردد؟
خروجی تابع مشخص می‌سازد که آیا strong name موجود و معتبر است یا خیر؟

و مثالی از استفاده‌ی آن (که بهتر است در یک تایمر نیم ساعت پس از اجرای برنامه رخ دهد):
using System;
using System.Reflection;
using System.Runtime.InteropServices;

namespace SigCheck
{
public class Validation
{
[DllImport("mscoree.dll", CharSet = CharSet.Unicode)]
public static extern bool StrongNameSignatureVerificationEx(
string wszFilePath, bool fForceVerification, ref bool pfWasVerified);

public static void SigCheck()
{
var assembly = Assembly.GetExecutingAssembly();
bool pfWasVerified = false;
if (!StrongNameSignatureVerificationEx(assembly.Location, true, ref pfWasVerified))
{
//خاتمه برنامه در صورت عدم وجود امضای دیجیتال معتبر
throw new Exception();
}
}
}

class Program
{
static void Main(string[] args)
{
Validation.SigCheck();
}
}
}
خوب، شاید پس از حذف و وصله شدن اسمبلی، مجددا strong name به آن اضافه شود! ، آن وقت چه باید کرد؟
زمانیکه به اسمبلی خود امضای دیجیتال اضافه می‌کنید، هش رمزنگاری شده فایل با الگوریتم RSA ، به همراه public key مورد نیاز در اسمبلی ذخیره می‌شوند. از آنجائیکه private key الگوریتم RSA را منتشر نکرده‌اید، شکستن الگوریتم RSA کار ساده‌ای نیست، مگر اینکه جفت کلید خودشان را تولید کنند و public key جدید را در فایل نهایی قرار دهند. بدیهی است این public key جدید با کلید عمومی ما که متناظر است با کلید خصوصی منتشر نشده‌ی اصلی، تطابق نخواهد داشد. برای آشنایی با تابعی که این بررسی را انجام می‌دهد به مقاله ذکر شده رجوع کنید:



مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت ششم
در قسمت قبل رویکرد‌های مختلف برای حذف موجودیت‌های منفصل را بررسی کردیم. در این قسمت مدیریت همزمانی یا Concurrency را بررسی خواهیم کرد.


فرض کنید می‌خواهیم مطمئن شویم که موجودیتی که توسط یک کلاینت WCF تغییر کرده است، تنها در صورتی بروز رسانی شود که شناسه (token) همزمانی آن تغییر نکرده باشد. به بیان دیگر شناسه ای که هنگام دریافت موجودیت بدست می‌آید، هنگام بروز رسانی باید مقداری یکسان داشته باشد.

مدل زیر را در نظر بگیرید.


می‌خواهیم یک سفارش (order) را توسط یک سرویس WCF بروز رسانی کنیم در حالی که اطمینان حاصل می‌کنیم موجودیت سفارش از زمانی که دریافت شده تغییری نکرده است. برای مدیریت این وضعیت دو رویکرد تقریبا متفاوت را بررسی می‌کنیم. در هر دو رویکرد از یک ستون همزمانی استفاده می‌کنیم، در این مثال فیلد TimeStamp.

  • در ویژوال استودیو پروژه جدیدی از نوع WCF Service Library بسازید و نام آن را به Recipe6 تغییر دهید.
  • روی نام پروژه کلیک راست کنید و گزینه Add New Item را انتخاب کنید. سپس گزینه‌های Data -> Entity Data Model را برگزینید. از ویزارد ویژوال استودیو برای اضافه کردن مدل جاری و جدول Orders استفاده کنید. در EF Designer روی فیلد TimeStamp کلیک راست کنید و گزینه Properties را انتخاب کنید. سپس مقدار CuncurrencyMode آنرا به Fixed تغییر دهید.
  • فایل IService1.cs را باز کنید و تعریف سرویس را مطابق لیست زیر بروز رسانی کنید.
[ServiceContract]
public interface IService1
{
    [OperationContract]
    Order InsertOrder();
    [OperationContract]
    void UpdateOrderWithoutRetrieving(Order order);
    [OperationContract]
    void UpdateOrderByRetrieving(Order order);
}

  • فایل Service1.cs را باز کنید و پیاده سازی سرویس را مطابق لیست زیر تکمیل کنید.
public class Service1 : IService1
{
    public Order InsertOrder()
    {
        using (var context = new EFRecipesEntities())
        {
            // remove previous test data
            context.Database.ExecuteSqlCommand("delete from [orders]");
            var order = new Order
            {
                Product = "Camping Tent",
                Quantity = 3,
                Status = "Received"
            };
            context.Orders.Add(order);
            context.SaveChanges();
            return order;
        }
    }

    public void UpdateOrderWithoutRetrieving(Order order)
    {
        using (var context = new EFRecipesEntities())
        {
            try
            {
                context.Orders.Attach(order);
                if (order.Status == "Received")
                {
                    context.Entry(order).Property(x => x.Quantity).IsModified = true;
                    context.SaveChanges();
                }
            }
            catch (OptimisticConcurrencyException ex)
            {
                // Handle OptimisticConcurrencyException
            }
        }
    }

    public void UpdateOrderByRetrieving(Order order)
    {
        using (var context = new EFRecipesEntities())
        {
            // fetch current entity from database
            var dbOrder = context.Orders
            .Single(o => o.OrderId == order.OrderId);
            if (dbOrder != null &&
                // execute concurrency check
                StructuralComparisons.StructuralEqualityComparer.Equals(order.TimeStamp, dbOrder.TimeStamp))
            {
                dbOrder.Quantity = order.Quantity;
                context.SaveChanges();
            }
            else
            {
                // Add code to handle concurrency issue
            }
        }
    }
}


  • برای تست این سرویس به یک کلاینت نیاز داریم. پروژه جدیدی از نوع Console Application به راه حل جاری اضافه کنید و کد آن را مطابق لیست زیر تکمیل کنید. با کلیک راست روی نام پروژه و انتخاب گزینه Add Service Reference سرویس پروژه را هم ارجاع کنید. دقت کنید که ممکن است پیش از آنکه بتوانید سرویس را ارجاع کنید نیاز باشد روی آن کلیک راست کرده و از منوی Debug گزینه Start Instance را انتخاب کنید تا وهله از سرویس به اجرا در بیاید.
class Program
{
    static void Main(string[] args)
    {
        var service = new Service1Client();
        var order = service.InsertOrder();
        order.Quantity = 5;
        service.UpdateOrderWithoutRetrieving(order);
        order = service.InsertOrder();
        order.Quantity = 3;
        service.UpdateOrderByRetrieving(order);
    }
}
اگر به خط اول متد ()Main یک breakpoint اضافه کنید و اپلیکیشن را اجرا کنید می‌توانید افزودن و بروز رسانی یک Order با هر دو رویکرد را بررسی کنید.


شرح مثال جاری

متد ()InsertOrder داده‌های پیشین را حذف می‌کند، سفارش جدیدی می‌سازد و آن را در دیتابیس ثبت می‌کند. در آخر موجودیت جدید به کلاینت باز می‌گردد. موجودیت بازگشتی هر دو مقدار OrderId و TimeStamp را دارا است که توسط دیتابیس تولید شده اند. سپس در کلاینت از دو رویکرد نسبتا متفاوت برای بروز رسانی موجودیت استفاده می‌کنیم.

در رویکرد نخست، متد ()UpdateOrderWithoutRetrieving موجودیت دریافت شده از کلاینت را Attach می‌کند و چک می‌کند که مقدار فیلد Status چیست. اگر مقدار این فیلد "Received" باشد، فیلد Quantity را با EntityState.Modified علامت گذاری می‌کنیم و متد ()SaveChanges را فراخوانی می‌کنیم. EF دستورات لازم برای بروز رسانی را تولید می‌کند، که فیلد quantity را مقدار دهی کرده و یک عبارت where هم دارد که فیلدهای OrderId و TimeStamp را چک می‌کند. اگر مقدار TimeStamp توسط یک دستور بروز رسانی تغییر کرده باشد، بروز رسانی جاری با خطا مواجه خواهد شد. برای مدیریت این خطا ما بدنه کد را در یک بلاک try/catch قرار می‌دهیم، و استثنای OptimisticConcurrencyException را مهار می‌کنیم. این کار باعث می‌شود اطمینان داشته باشیم که موجودیت Order دریافت شده از متد ()InsertOrder تاکنون تغییری نکرده است. دقت کنید که در مثال جاری تمام خواص موجودیت بروز رسانی می‌شوند، صرفنظر از اینکه تغییر کرده باشند یا خیر.

رویکرد دوم نشان می‌دهد که چگونه می‌توان وضعیت همزمانی موجودیت را پیش از بروز رسانی مشخصا دریافت و بررسی کرد. در اینجا می‌توانید مقدار TimeStamp موجودیت را از دیتابیس بگیرید و آن را با مقدار موجودیت کلاینت مقایسه کنید تا وجود تغییرات مشخص شود. این رویکرد در متد ()UpdateOrderByRetrieving نمایش داده شده است. گرچه این رویکرد برای تشخیص تغییرات خواص موجودیت‌ها و یا روابط شان مفید و کارآمد است، اما بهترین روش هم نیست. مثلا ممکن است از زمانی که موجودیت را از دیتابیس دریافت می‌کنید، تا زمانی که مقدار TimeStamp آن را مقایسه می‌کنید و نهایتا متد ()SaveChanges را صدا میزنید، موجودیت شما توسط کلاینت دیگری بروز رسانی شده باشد.

مسلما رویکرد دوم هزینه بر‌تر از رویکرد اولی است، چرا که برای مقایسه مقادیر همزمانی موجودیت ها، یکبار موجودیت را از دیتابیس دریافت می‌کنید. اما این رویکرد در مواقعی که Object graph‌‌های بزرگ یا پیچیده (complex) دارید بهتر است، چون پیش از ارسال موجودیت‌ها به context در صورت برابر نبودن مقادیر همزمانی پروسس را لغو می‌کنید.

مطالب
Minimal API's در دات نت 6 - قسمت دوم - ایجاد مدل‌ها و Context برنامه
در قسمت قبل، ساختار ابتدایی یک پروژه‌ی Minimal API's مبتنی بر معماری Vertical slices را تشکیل دادیم. در ادامه موجودیت‌ها و DbContext آن‌را تشکیل می‌دهیم.


ایجاد مدل‌ها و موجودیت‌های برنامه‌ی Minimal Blog

در مثال این سری، هر نویسنده می‌تواند بلاگ خاص خودش را داشته باشد و در هر بلاگ، تعدادی مقاله منتشر کند. هر مقاله هم می‌تواند تعدادی تگ یا گروه مرتبط را داشته باشد.

ساختار ابتدایی موجودیت نویسندگان مطالب بلاگ

این موجودیت‌ها در پوشه‌ی جدیدی به نام Model پروژه‌ی MinimalBlog.Domain اضافه خواهند شد:
namespace MinimalBlog.Domain.Model;

public class Author
{
    public int Id { get; set; }
    public string FirstName { get; set; } = default!;
    public string LastName { get; set; } = default!;

    public string FullName => FirstName + " " + LastName;
    public DateTime DateOfBirth { get; set; }
    public string? Bio { get; set; }
}
در اینجا تعاریفی مانند !default را هم مشاهده می‌کنید که مرتبط است با فعال بودن nullable reference types در این پروژه که در فایل Directory.Build.props به صورت سراسری به تمام پروژه‌ها اعمال می‌شود. با تعریف !default به کامپایلر اعلام می‌کنیم که این خواص هیچگاه نال نخواهند بود. هدف اصلی از nullable reference types، بالا بردن قابلیت پیش بینی نال بودن، یا نبودن آن‌ها است؛ تا برنامه نویس در قسمت‌های مختلف برنامه بداند که آیا واقعال نیاز است هنگام کار با خاصیت FirstName، نال بودن آن‌را پیش از استفاده بررسی کند یا خیر؟

ساختار ابتدایی موجودیت مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Article
{
    public Article()
    {
        Categories = new List<Category>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = default!;
    public string? Subtitle { get; set; }
    public string Body { get; set; } = default!;

    public int AuthorId { get; set; }
    public Author Author { get; set; } = default!;
    public DateTime DateCreated { get; set; }
    public DateTime LastUpdated { get; set; }
    public int NumberOfLikes { get; set; }
    public int NumberOfShares { get; set; }
    public ICollection<Category> Categories { get; set; }
}

ساختار ابتدایی بلاگ‌های اختصاصی قابل تعریف
namespace MinimalBlog.Domain.Model;

public class Blog
{
    public Blog()
    {
        Contributors = new List<Author>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public DateTime CreatedDate { get; set; }

    public int AuthorId { get; set; }
    public Author Owner { get; set; } = default!;

    public ICollection<Author> Contributors { get; }
}

ساختار ابتدایی گروه‌های مقالات بلاگ
namespace MinimalBlog.Domain.Model;

public class Category
{
    public Category()
    {
        Articles = new List<Article>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = default!;
    public string Description { get; set; } = default!;
    public ICollection<Article> Articles { get; set; }
}


معرفی موجودیت‌های تعریف شده به لایه‌ی Dal

لایه‌ی Dal جایی است که DbContext برنامه تعریف می‌شود و موجودیت‌ها فوق توسط DbSetها در معرض دید EF-Core قرار می‌گیرند. به همین جهت ابتدا فایل MinimalBlog.Dal.csproj را به صورت زیر تغییر می‌دهیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
</Project>
در اینجا در ابتدا ارجاعی به پروژه‌ی MinimalBlog.Domain.csproj اضافه شده‌است. سپس بسته‌های مورد نیاز از EF-Core نیز جهت تعریف DbSetها و اجرای Migrations، اضافه شده‌اند.
به علاوه تعاریفی مانند ImplicitUsings و Nullable را هم مشاهده می‌کنید که با توجه به استفاده‌ی از فایل Directory.Build.props غیرضروری و تکراری هستند؛ چون این فایل مخصوص به همراه این تعاریف سراسری نیز هست.

سپس به پروژه‌ی Dal، کلاس جدید MinimalBlogDbContext را به صورت زیر اضافه می‌کنیم تا مدل‌های برنامه را در معرض دید EF-Core قرار دهد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Domain.Model;

namespace MinimalBlog.Dal;

public class MinimalBlogDbContext : DbContext
{
    public MinimalBlogDbContext(DbContextOptions options) : base(options)
    {
    }

    public DbSet<Article> Articles { get; set; } = default!;
    public DbSet<Category> Categories { get; set; } = default!;
    public DbSet<Author> Authors { get; set; } = default!;
    public DbSet<Blog> Blogs { get; set; } = default!;
}
در اینجا نیز تعاریف !default را مشاهده می‌کنید که خاصیت راهنمای کامپایلر را در حالت فعال بودن <Nullable>enable</Nullable> دارند و هدف از آن، اعلام نال نبودن این خواص، در حین استفاده‌ی از آن‌ها در قسمت‌های مختلف برنامه است.


ایجاد و اجرای Migrations

پس از تعریف MinimalBlogDbContext، اکنون نوبت به ایجاد کلاس‌های Migrations و اجرای آن‌ها جهت ایجاد بانک اطلاعاتی متناظر با مدل‌های برنامه است. برای این منظور در ابتدا به پروژه‌ی Api مراجعه کرده و فایل appsettings.json را به صورت زیر تکمیل می‌کنیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=MinimalBlog;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False"
  }
}
در اینجا یک رشته‌ی اتصالی جدید را از نوع SQL Server LocalDB، تعریف کرده‌ایم. سپس نیاز است تا این رشته‌ی اتصالی را به پروایدر SQL Server مخصوص EF-Core اضافه کنیم. برای اینکار ابتدا باید تغییرات زیر را به فایل MinimalBlog.Api.csproj اعمال کنیم:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="6.0.2" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\MinimalBlog.Domain\MinimalBlog.Domain.csproj" />
    <ProjectReference Include="..\MinimalBlog.Dal\MinimalBlog.Dal.csproj" />
  </ItemGroup>
</Project>
که در آن ارجاعاتی به پروژه‌های Domain و Dal اضافه شده‌است و همچنین بسته‌های نیوگت EF-Core نیز افزوده شده‌اند تا بتوان در فایل Program.cs، تنظیم زیر را اضافه کرد:
using Microsoft.EntityFrameworkCore;
using MinimalBlog.Dal;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var connectionString = builder.Configuration.GetConnectionString("Default");
builder.Services.AddDbContext<MinimalBlogDbContext>(opt => opt.UseSqlServer(connectionString));
در اینجا با استفاده از متد الحاقی AddDbContext، کلاس Context برنامه به مجموعه‌ی سرویس‌های آن اضافه شده و همچنین رشته‌ی اتصالی با کلید Default هم از تنظیمات برنامه، دریافت و به متد UseSqlServer جهت استفاده ارسال شده‌است.

پس از این تنظیمات به پوشه‌ی MinimalBlog.Dal وارد شده و فایل _01-add_migrations.cmd را اجرا می‌کنیم (این فایل به همراه پیوست قسمت اول، ارائه شده‌است). محتوای این فایل به صورت زیر است:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b)
For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b)
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef migrations --startup-project ../MinimalBlog.Api/ add V%mydate%_%mytime% --context MinimalBlogDbContext
pause
ابتدا، آخرین نگارش ابزار dotnet-ef را نصب کرده و سپس یکبار هم پروژه را build می‌کند تا اگر مشکلی باشد، در همینجا با اطلاعات بیشتری نمایش داده شود. سپس با اجرای دستور dotnet ef migrations، کلاس‌های Migrations در پوشه‌ای به همین نام تشکیل می‌شوند.
البته چون کدهای تولید شده‌ی به صورت خودکار الزاما با Roslyn analyzers برنامه سازگاری ندارند، آن‌ها را توسط فایل مخصوص editorconfig. قرار گرفته‌ی در پوشه‌ی MinimalBlog.Dal\Migrations، از لیست آنالیزهای برنامه خارج می‌کنیم. محتوای این فایل ویژه به صورت زیر است:
[*.cs]
generated_code = true
که سبب خواهد شد تا این پوشه‌ی ویژه به عنوان کدهای به صورت خودکار تولید شده تشخیص داده شود و دیگر آنالیز نشود؛ چون کیفیت آن، مشکل ما نیست!

پس از ایجاد کلاس‌های Migration، اکنون نوبت به اجرای آن‌ها بر روی بانک اطلاعاتی است که در رشته‌ی اتصالی مشخص کردیم. برای اینکار فایل MinimalBlog.Dal\_02-update_db.cmd را اجرا می‌کنیم که چنین محتویاتی دارد:
dotnet tool update --global dotnet-ef --version 6.0.2
dotnet build
dotnet ef --startup-project ../MinimalBlog.Api/ database update --context MinimalBlogDbContext
pause
در اینجا نیز ابتدا از نصب آخرین نگارش ابزارهای EF اطمینان حاصل می‌شود. یکبار دیگر نیز پروژه بر اساس فایل‌های جدیدی که به آن اضافه شده، کامپایل شده و سپس کلاس‌های مهاجرت‌ها، اجرا و اعمال می‌شوند.
نظرات مطالب
صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC
columnChooser رویداد done هم دارد که از آن می‌شود برای آنالیز ستون‌ها استفاده کرد:
            .jqGrid('navButtonAdd', '#pager',
            {
                caption: "تنظیم نمایش ستون‌ها", title: "Reorder Columns",
                onClickButton: function () {
                    jQuery("#list").jqGrid('columnChooser', {
                        done: function (perm) {
                            if (perm) {
                                $("#list").jqGrid("remapColumns", perm, true, false);
                            }

                            var colModel = $("#list").jqGrid('getGridParam', 'colModel');
                            var hiddenColumns = new Array();
                            for (var i = 0; i < colModel.length; i++) {
                                if (colModel[i].hidden) {
                                    hiddenColumns.push(colModel[i].name);
                                }
                            }

                            $.ajax({
                                type: "POST",
                                url: "@Url.Action("HiddenColumns","Home")",
                                dataType: "json",
                                traditional: true,
                                data: { hiddenColumns: hiddenColumns }
                            });
                        }
                    });
                }
            })
متد getGridParam با پارامتر colModel، آرایه‌ای از خواص ستون‌ها را بر می‌گرداند.
var colModel = $("#list").jqGrid('getGridParam', 'colModel');
در این خواص اگر hidden مساوی true بود، یعنی مخفی شده‌است. در نهایت از این‌ها می‌شود یک آرایه را تشکیل داد و به سرور ارسال کرد:
        public ActionResult HiddenColumns(string[] hiddenColumns)
        {
            //todo: save it in the DB or cookies or session ....

            return Content("OK");
        }
امضای اکشن متدی است که لیست نام ستون‌های مخفی را دریافت می‌کند.
مطالب
ارسال خطاهای رخ‌داده‌ی در برنامه‌های سمت کلاینت Blazor WASM، به تلگرام
هر زمانیکه در سمت کلاینت، استثناء یا خطایی رخ می‌دهد، کاربر با نوار زرد رنگی در پایین صفحه، از آن مطلع می‌شود؛ اما برنامه نویس چطور؟! به همین جهت در این مطلب قصد داریم تمام خطاهای رخ داده‌ی در برنامه‌ی سمت کلاینت را لاگ کرده و به سرور تلگرام ارسال کنیم. مزیت کار کردن با تلگرام، دسترسی به سروری است که تقریبا همواره در دسترس است و برخلاف بانک اطلاعاتی برنامه که ممکن است در لحظه‌ی بروز خطا، خودش سبب ساز اصلی باشد و قادر به ثبت اطلاعات خطاهای رسیده‌ی از سمت کلاینت نباشد، چنین مشکلی را با تلگرام نداریم (مانند همان جمله‌ی معروف: «بک‌آپ سروری که روی همان سرور گرفته می‌شود، بک آپ نام ندارد!»). همچنین بررسی و حذف گزارش‌های رسیده‌ی به آن نیز بسیار ساده‌است و می‌توان این گزارش‌ها را مستقل از سرور برنامه و از طریق وسایل مختلفی مانند گوشی‌های همراه، تبلت‌ها و غیره نیز بررسی کرد.




نحوه‌ی نمایش خطاها در برنامه‌های Blazor

در حین توسعه‌ی برنامه‌های Blazor، اگر استثنائی رخ دهد، نوار زرد رنگی در پایین صفحه، ظاهر می‌شود که امکان هدایت توسعه دهنده را به کنسول مرورگر، برای مشاهده‌ی جزئیات بیشتر آن خطا را دارد. در حالت توزیع برنامه، این نوار زرد رنگ تنها به ذکر خطایی رخ داده‌است اکتفا کرده و گزینه‌ی راه اندازی مجدد برنامه را با ریفرش کردن مرورگر، پیشنهاد می‌دهد. سفارشی سازی آن هم در فایل wwwroot/index.html در قسمت زیر صورت می‌گیرد:
<div id="blazor-error-ui">
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>
که شیوه نامه‌های پیش‌فرض آن در فایل wwwroot/css/app.css قرار دارند. در حالت عادی المان blazor-error-ui به همراه یک display: none است که از نمایش آن جلوگیری می‌کند. اما در زمان بروز خطایی، فریم‌ورک آن‌را به صورت display: block نمایش می‌دهد.


نحوه‌ی مدیریت استثناءها در برنامه‌های Blazor

توصیه شده‌است که کار مدیریت استثناءها باید توسط توسعه دهنده صورت گیرد و بهتر است جزئیات آن‌ها و یا stack-trace آن‌ها را به کاربر نمایش نداد؛ تا مبادا اطلاعات حساسی فاش شوند و یا کاربر مهاجم بتواند توسط آن‌ها اطلاعات ارزشمندی را از نحوه‌ی عملکرد برنامه بدست آورد.
برخلاف برنامه‌های ASP.NET Core که دارای یک middleware pipeline هستند و برای مثال توسط آن‌ها می‌توان مدیریت سراسری خطاهای رخ‌داده را انجام داد، چنین ویژگی در برنامه‌های Blazor وجود ندارد؛ چون در اینجا مرورگر است که هاست برنامه بوده و processing pipeline آن‌را تشکیل می‌دهد.
اما ... اگر استثنائی مدیریت نشده در یک برنامه‌ی Blazor رخ‌دهد، این استثناء در ابتدا توسط یک ILogger، لاگ شده و سپس در کنسول مرورگر نمایش داده می‌شود. در اینجا Console Logging Provider، تامین کننده‌ی پیش‌فرض سیستم ثبت وقایع برنامه‌های Blazor است. به همین جهت استثناءهای مدیریت نشده‌ی برنامه را می‌توان در کنسول توسعه دهندگان مرورگر نیز مشاهده کرد. برای مثال اگر سطح لاگ ارائه شده LogLevel.Error باشد، به صورت خودکار به معادل console.error ترجمه می‌شود.
بنابراین اگر در برنامه‌ی Blazor جاری یک ILoggerProvider سفارشی را تهیه و آن‌را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، می‌توان از تمام وقایع سیستم (هر قسمتی از آن که از ILogger استفاده می‌کند)، منجمله تمام خطاهای رخ‌داده (و مدیریت نشده) مطلع شد و برای مثال آن‌ها را به سمت Web API برنامه، جهت ثبت در بانک اطلاعاتی و یا نمایش در برنامه‌ی تلگرام، ارسال کرد و این دقیقا همان کاری است که قصد داریم در ادامه انجام دهیم.


نوشتن یک ILoggerProvider سفارشی جهت ارسال رخ‌دادها برنامه‌ی سمت کلاینت، به یک Web API

برای ارسال تمام وقایع برنامه‌ی کلاینت به سمت سرور، نیاز است یک ILoggerProvider سفارشی را تهیه کنیم که شروع آن به صورت زیر است:
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class ClientLoggerProvider : ILoggerProvider
    {
        private readonly HttpClient _httpClient;
        private readonly WebApiLoggerOptions _options;
        private readonly NavigationManager _navigationManager;

        public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
        {
            if (serviceProvider is null)
            {
                throw new ArgumentNullException(nameof(serviceProvider));
            }

            if (options is null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            _httpClient = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<HttpClient>();
            _options = options.Value;
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new WebApiLogger(_httpClient, _options, _navigationManager);
        }

        public void Dispose()
        {
        }
    }
}
توضیحات:
زمانیکه قرار است یک لاگر سفارشی را به سیستم تزریق وابستگی‌های برنامه معرفی کنیم، روش آن به صورت زیر است:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public static class ClientLoggerProviderExtensions
    {
        public static ILoggingBuilder AddWebApiLogger(this ILoggingBuilder builder)
        {
            if (builder == null)
            {
                throw new ArgumentNullException(nameof(builder));
            }

            builder.Services.AddSingleton<ILoggerProvider, ClientLoggerProvider>();
            return builder;
        }
    }
}
باید کلاسی را داشته باشیم مانند ClientLoggerProvider که یک ILoggerProvider را پیاده سازی می‌کند و نحوه‌ی ثبت آن نیز باید حتما Singleton باشد. مزیت معرفی ILoggerProvider به این نحو، امکان دسترسی به سرویس‌های برنامه در سازنده‌ی کلاس ClientLoggerProvider است و در این حالت دیگر نیاز به نوشتن new ClientLoggerProvider نبوده و خود سیستم تزریق وابستگی‌ها، سازنده‌های ClientLoggerProvider را تامین می‌کند.
در کلاس ClientLoggerProvider فوق، سه وابستگی تزریق شده را مشاهده می‌کنید:
public ClientLoggerProvider(
                IServiceProvider serviceProvider,
                IOptions<WebApiLoggerOptions> options,
                NavigationManager navigationManager)
با استفاده از IServiceProvider می‌توان به HttpClient برنامه دسترسی یافت. از این جهت که چون HttpClient به صورت پیش‌فرض با طول عمر Scoped به سیستم معرفی شده، امکان تزریق مستقیم آن به سازنده‌ی یک ILoggerProvider از نوع Singleton وجود ندارد. به همین جهت از IServiceProvider برای تامین آن استفاده خواهیم کرد. مابقی موارد مانند IOptions که تنظیمات این لاگر را فراهم می‌کند و یا NavigationManager استاندارد برنامه که امکان دسترسی به Url جاری را میسر می‌کند، به صورت پیش‌فرض دارای طول عمر Singleton هستند و می‌توان آن‌ها را بدون مشکل، به سازنده‌ی لاگر سفارشی، تزریق کرد.
مهم‌ترین قسمت ILoggerProvider سفارشی، متد CreateLogger آن است که یک ILogger را بازگشت می‌دهد:
public ILogger CreateLogger(string categoryName)
{
   return new WebApiLogger(_httpClient, _options, _navigationManager);
}
بنابراین در ادامه نیاز است، یک ILogger سفارشی را نیز پیاده سازی کنیم:
using System;
using System.Net.Http;
using System.Net.Http.Json;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLogger : ILogger
    {
        private readonly WebApiLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly NavigationManager _navigationManager;

        public WebApiLogger(HttpClient httpClient, WebApiLoggerOptions options, NavigationManager navigationManager)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _navigationManager = navigationManager ?? throw new ArgumentNullException(nameof(navigationManager));
        }

        public IDisposable BeginScope<TState>(TState state) => default;

        public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter is null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            try
            {
                ClientLog log = new()
                {
                    LogLevel = logLevel,
                    EventId = eventId,
                    Message = formatter(state, exception),
                    Exception = exception?.Message,
                    StackTrace = exception?.StackTrace,
                    Url = _navigationManager.Uri
                };
                _httpClient.PostAsJsonAsync(_options.LoggerEndpointUrl, log);
            }
            catch
            {
                // don't throw exceptions from the logger
            }
        }
    }
}
نحوه‌ی عملکرد این ILogger سفارشی بسیار ساده‌است:
- متد IsEnabled آن مشخص می‌کند که چه سطحی از رخ‌دادهای سیستم را باید لاگ کند. این سطح را نیز از تنظیمات برنامه دریافت می‌کند:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Client.Logging
{
    public class WebApiLoggerOptions
    {
        public string LoggerEndpointUrl { set; get; }

        public LogLevel LogLevel { get; set; } = LogLevel.Information;
    }
}
در این تنظیمات مشخص می‌کنیم که Url مربوط به اکشن متد Web API ما که قرار است اطلاعات به سمت آن ارسال شوند، چیست؟ همچنین حداقل سطح لاگ مدنظر را نیز باید مشخص کنیم. اطلاعات آن توسط فایل Client\wwwroot\appsettings.json با این محتوای فرضی قابل تنظیم است:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "WebApiLogger": {
    "LogLevel": "Warning",
    "LoggerEndpointUrl": "/api/logs"
  }
}
و همچنین باید کلاس WebApiLoggerOptions را به نحو زیر در کلاس Program برنامه به سیستم تزریق وابستگی‌ها، معرفی کرد تا <IOptions<WebApiLoggerOptions قابلیت تزریق به سازنده‌ی تامین کننده‌ی لاگر را پیدا کند:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            // …
        }
    }
}
- متد لاگ این لاگر سفارشی، پیام نهایی قابل ارسال به سمت Web API را تشکیل داده و توسط متد httpClient.PostAsJsonAsync آن‌را ارسال می‌کند. به همین جهت ساختار لاگ مدنظر را در فایل Shared\ClientLog.cs به صورت زیر تعریف کرده‌ایم که بین برنامه‌ی کلاینت و سرور، مشترک است:
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Shared
{
    public class ClientLog
    {
        public LogLevel LogLevel { get; set; }

        public EventId EventId { get; set; }

        public string Message { get; set; }

        public string Exception { get; set; }

        public string StackTrace { get; set; }

        public string Url { get; set; }
    }
}
این اطلاعاتی است که کلاینت به ازای رخ‌دادی خاص، جمع آوری کرده و به سمت سرور ارسال می‌کند.

در آخر هم کار ثبت متد ()AddWebApiLogger که معرفی ILoggerProvider سفارشی ما را انجام می‌دهد، به صورت زیر خواهد بود:
namespace BlazorWasmTelegramLogger.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

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

            builder.Services.Configure<WebApiLoggerOptions>(options => builder.Configuration.GetSection("WebApiLogger").Bind(options));
            builder.Services.AddLogging(configure =>
            {
                configure.AddWebApiLogger();
            });

            await builder.Build().RunAsync();
        }
    }
}
تا اینجا اگر هر نوع استثنای مدیریت نشده‌ای در برنامه‌ی Blazor WASM رخ دهد، چون سطح لاگ آن بالاتر از Warning تنظیم شده‌ی در فایل Client\wwwroot\appsettings.json است:
public bool IsEnabled(LogLevel logLevel) => logLevel >= _options.LogLevel;
به صورت خودکار به سمت کنترلر api/logs ارسال خواهد شد. بنابراین مرحله‌ی بعدی، تکمیل کنترلر یاد شده‌است.


ایجاد سرویسی برای ارسال لاگ‌های برنامه به سمت تلگرام

پیش از اینکه کار تکمیل کنترلر api/logs را در برنامه‌ی Web API انجام دهیم، ابتدا در همان برنامه‌ی Web API، سرویسی را برای ارسال لاگ‌های رسیده به سمت تلگرام، تهیه می‌کنیم. علت اینکه این قسمت را به برنامه‌ی سمت سرور محول کرده‌ایم، شامل موارد زیر است:
- درست است که می‌توان کتابخانه‌های مرتبط با تلگرام را به برنامه‌ی سی‌شارپی Blazor خود اضافه کرد، اما هر وابستگی سمت کلاینتی، سبب حجیم‌تر شدن توزیع نهایی برنامه خواهد شد که مطلوب نیست.
- برای کار با تلگرام نیاز است توکن اتصال به آن‌را در یک محل امن، نگهداری کرد. قرار دادن این نوع اطلاعات حساس، در برنامه‌ی سمت کلاینتی که تمام اجزای آن از مرورگر قابل استخراج و بررسی است، کار اشتباهی است.
- ارسال اطلاعات لاگ برنامه‌ی سمت کلاینت به Web API، مزیت لاگ سمت سرور آن‌را مانند ثبت در یک فایل محلی، ثبت در بانک اطلاعاتی و غیره را نیز میسر می‌کند و صرفا محدود به تلگرام نیست.

برای ارسال اطلاعات به تلگرام، سرویس سمت سرور زیر را تهیه می‌کنیم:
using System;
using System.Text;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Telegram.Bot;
using Telegram.Bot.Types.Enums;

namespace BlazorWasmTelegramLogger.Server.Services
{
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }

    public interface ITelegramBotService
    {
        Task SendLogAsync(ClientLog log);
    }

    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }

        public async Task SendLogAsync(ClientLog log)
        {
            var text = formatMessage(log);
            if (string.IsNullOrWhiteSpace(text))
            {
                return;
            }

            await _client.SendTextMessageAsync(_chatId, text, ParseMode.Markdown);
        }

        private static string formatMessage(ClientLog log)
        {
            if (string.IsNullOrWhiteSpace(log.Message))
            {
                return string.Empty;
            }

            var sb = new StringBuilder();
            sb.Append(toEmoji(log.LogLevel))
                .Append(" *")
                .AppendFormat("{0:hh:mm:ss}", DateTime.Now)
                .Append("* ")
                .AppendLine(log.Message);

            if (!string.IsNullOrWhiteSpace(log.Exception))
            {
                sb.AppendLine()
                    .Append('`')
                    .AppendLine(log.Exception)
                    .AppendLine(log.StackTrace)
                    .AppendLine("`")
                    .AppendLine();
            }

            sb.Append("*Url:* ").AppendLine(log.Url);
            return sb.ToString();
        }

        private static string toEmoji(LogLevel level) =>
            level switch
            {
                LogLevel.Trace => "⬜️",
                LogLevel.Debug => "🟦",
                LogLevel.Information => "⬛️️️",
                LogLevel.Warning => "🟧",
                LogLevel.Error => "🟥",
                LogLevel.Critical => "❌",
                LogLevel.None => "🔳",
                _ => throw new ArgumentOutOfRangeException(nameof(level), level, null)
            };
    }
}
توضیحات:
- برای کار با API تلگرام، از کتابخانه‌ی معروف Telegram.Bot استفاده کرده‌ایم که به صورت زیر، وابستگی آن به برنامه‌ی Web API اضافه می‌شود:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <ItemGroup>
    <PackageReference Include="Telegram.Bot" Version="15.7.1" />
  </ItemGroup>
</Project>
- این سرویس برای کار کردن، نیاز به تنظیمات زیر را دارد:
    public class TelegramLoggingBotOptions
    {
        public string AccessToken { get; set; }
        public string ChatId { get; set; }
    }
- برای دریافت AccessToken، در برنامه‌ی تلگرام خود، بات مخصوصی را به نام https://t.me/botfather یافته و سپس آن‌را استارت کنید:


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

- مرحله‌ی بعد تنظیم ChatId است. نحوه‌ی کار برنامه به این صورت است که پیام‌ها را به این بات سفارشی خود ارسال کرده و این بات، آن‌ها را به کانال اختصاصی ما هدایت می‌کند. بنابراین یک کانال جدید را ایجاد کنید. ترجیحا بهتر است این کانال خصوصی باشد. سپس کاربر test_2021_logs_bot@ (همان نام منحصربفرد بات که حتما باید با @ شروع شود) را به عنوان عضو جدید کانال خود اضافه کنید. در اینجا عنوان می‌کند که این کاربر چون بات است، باید دسترسی ادمین را داشته باشد که دقیقا این دسترسی را نیز باید برقرار کنید تا بتوان توسط این بات، پیامی را به کانال اختصاصی خود ارسال کرد.
بنابراین تا اینجا یک کانال خصوصی را ایجاد کرده‌ایم که بات جدید test_2021_logs_bot@ عضو با دسترسی ادمین آن است. اکنون باید Id این کانال را بیابیم. برای اینکار بات دیگری را به نام JsonDumpBot@ یافته و استارت کنید. سپس در کانال خود یک پیام آزمایشی جدید را ارسال کنید و در ادامه این پیام را به بات JsonDumpBot@ ارسال کنید (forward کنید). همان لحظه‌ای که کار ارسال پیام به این بات صورت گرفت، Id کانال خود را در پاسخ آن می‌توانید مشاهده کنید:


در این تصویر مقدار forward_from_chat:id همان ChatId تنظیمات برنامه‌ی شما است.

در آخر این اطلاعات را در فایل Server\appsettings.json قرار می‌دهیم:
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "TelegramLoggingBot": {
    "AccessToken": "1826…",
    "ChatId": "-1001…" 
  }
}
که نحوه‌ی ثبت و معرفی آن‌ها به سیستم تزریق وابستگی‌های برنامه‌ی Web API، به صورت زیر است:
namespace BlazorWasmTelegramLogger.Server
{
    public class Startup
    {
        // ...

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<TelegramLoggingBotOptions>(options =>
                            Configuration.GetSection("TelegramLoggingBot").Bind(options));
            services.AddSingleton<ITelegramBotService, TelegramBotService>();

            // ...
        }

        // ...
    }
}
سرویس ITelegramBotService را با طول عمر Singleton معرفی کرده‌ایم. چون new TelegramBotClient ای که در سازنده‌ی آن صورت می‌گیرد:
    public class TelegramBotService : ITelegramBotService
    {
        private readonly string _chatId;
        private readonly TelegramBotClient _client;

        public TelegramBotService(IOptions<TelegramLoggingBotOptions> options)
        {
            _chatId = options.Value.ChatId;
            _client = new TelegramBotClient(options.Value.AccessToken);
        }
باید فقط یکبار در طول عمر برنامه انجام شود و از این پس، هر بار که متد client.SendTextMessageAsync_ آن فراخوانی می‌گردد، پیامی به سمت بات و سپس کانال اختصاصی ما ارسال می‌شود.


ایجاد کنترلر Logs، جهت دریافت لاگ‌های رسیده‌ی از سمت کلاینت

مرحله‌ی آخر کار بسیار ساده‌است. سرویس تکمیل شده‌ی ITelegramBotService را به سازنده‌ی کنترلر Logs تزریق کرده و سپس متد SendLogAsync آن‌را فراخوانی می‌کنیم تا لاگی را که از کلاینت دریافت کرده، به سمت تلگرام هدایت کند:
using System;
using System.Threading.Tasks;
using BlazorWasmTelegramLogger.Server.Services;
using BlazorWasmTelegramLogger.Shared;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace BlazorWasmTelegramLogger.Server.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class LogsController : ControllerBase
    {
        private readonly ILogger<LogsController> _logger;
        private readonly ITelegramBotService _telegramBotService;

        public LogsController(ILogger<LogsController> logger, ITelegramBotService telegramBotService)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _telegramBotService = telegramBotService;
        }

        [HttpPost]
        public async Task<IActionResult> PostLog(ClientLog log)
        {
            // TODO: Save the client's `log` in the database

            _logger.Log(log.LogLevel, log.EventId, log.Url + Environment.NewLine + log.Message);

            await _telegramBotService.SendLogAsync(log);

            return Ok();
        }
    }
}


آزمایش برنامه

برای آزمایش برنامه، برای مثال در فایل Client\Pages\Counter.razor یک استثنای عمدی مدیریت نشده را قرار داده‌ایم:
@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;

        throw new InvalidOperationException("This is an exception message from the client!");
    }
}
اکنون اگر برنامه را اجرا کرده و سپس بر روی دکمه‌ی شمارشگر کلیک کنیم، همان تصویر ابتدای مطلب را که حاصل از ارسال جزئیات این استثنای مدیریت نشده به سمت تلگرام است، مشاهده خواهیم کرد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: BlazorWasmTelegramLogger.zip