اشتراک‌ها
14 قابلیت بهبود یافته برتر در VB 14

1. 50 درصد سریعتر شدن زمان کامپایل. نسخه فعلی کامپایلر VB در ++C نوشته شده است, ولی تیم VB کامپایلر جدید VB.NET 14 را کاملا در VB.NET بازنویسی کرده اند.نتایج دو مقایسه :

   * زمان Build یک Solution بزرگ(Huge) با 1.3 میلیون خط کد, از 68 ثانیه رسید به 41 ثانیه.

   * زمان Load یک Cold Solution (یک پروژه Win Store) از 6.7 ثانیه رسید به 4.6 ثانیه.

2. به اشتراک گذاری یک پروژه(یک سری ار سورس کد ها) بین چند تا Application

3. نمایش Expression‌های Lambda و LINQ در پنجره Watch و Immediate در زمان Debug

4. قرار دادن کامنت در لا به لای عبارات LINQ

5. استفاده از اپراتور جدید ?. جهت بررسی Null بودن برای تسریع در حین کد نویسی

و ...

14 قابلیت بهبود یافته برتر در VB 14
مطالب
غیرمعتبر کردن توکن و یا کوکی سرقت شده در برنامه‌های مبتنی بر ASP.NET Core
چند روز قبل، یکی از کانال‌های فنی معروف یوتیوب با بیش از 15 میلیون مشترک، هک و پاک شد! که داستان آن‌را در اینجا می‌توانید پیگیری کنید. در این هک، مهاجم در سعی اول، پیشنهاد پشتیبانی مالی از شبکه را داده و در ایمیل دوم، پس از جلب اعتماد اولیه، یک فایل به ظاهر PDF مفاد قرارداد را ارسال کرده که با کلیک بر روی آن، تمام کوکی‌های یوتیوب مالک کانال، سرقت و مورد سوء استفاده قرار گرفته! در یک چنین حالتی، مهم نیست که شما اعتبارسنجی دو مرحله‌ای را فعال کرده‌اید و یا از بهترین روش‌های رمزنگاری برای امن کردن اطلاعات کوکی و یا توکن خود استفاده کرده‌اید، همینقدر که اصل محتوای کوکی و یا توکن شما در اختیار شخص دیگری قرار گیرد، می‌تواند بدون نیاز به لاگین و دانستن کلمه‌ی عبور شما، بجای شما وارد سیستم شده و تغییرات دلخواهی را اعمال کند!
بنابراین سؤال اینجاست که ما (توسعه دهندگان) چگونه می‌توانیم یک چنین حملاتی را مشکل‌تر کنیم؟ در این مطلب روشی را در جهت سعی در غیرمعتبر کردن توکن‌ها و یا کوکی‌های سرقت شده، در برنامه‌های مبتنی بر ASP.NET Core بررسی خواهیم کرد.


توسعه‌ی یک سرویس تشخیص مرورگر و سیستم عامل شخص وارد شده‌ی به سیستم

یکی از روش‌های غیرممکن کردن یک چنین حملاتی، درج مشخصات سیستم عامل و مرورگر شخص وارد شده‌ی به سیستم، در کوکی و همچنین توکن صادر شده‌ی حاصل از اعتبارسنجی موفق است. سپس زمانیکه قرار است از اطلاعات این کوکی و یا توکن در برنامه استفاده شود، این اطلاعات را با اطلاعات درخواست جاری کاربر مقایسه کرده و در صورت عدم تطابق، درخواست او را برگشت می‌زنیم. برای مثال اگر عملیات لاگین، در ویندوز انجام شده و اکنون توکن و یا کوکی حاصل، در سیستم عامل اندروید در حاصل استفاده‌است، یعنی ... این عملیات مشکوک است و باید خاتمه یابد و کاربر باید مجبور به لاگین مجدد شود و نه اعتبارسنجی خودکار بدون زحمت!
برای این منظور می‌توان از کتابخانه‌ی UA-Parser استفاده کرد و توسط آن سرویس زیر را توسعه داد:
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
using UAParser;

namespace ASPNETCore2JwtAuthentication.Services;

/// <summary>
///     To invalidate an old user's token from a new device
/// </summary>
public class DeviceDetectionService : IDeviceDetectionService
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ISecurityService _securityService;

    public DeviceDetectionService(ISecurityService securityService, IHttpContextAccessor httpContextAccessor)
    {
        _securityService = securityService ?? throw new ArgumentNullException(nameof(securityService));
        _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
    }

    public string GetCurrentRequestDeviceDetails() => GetDeviceDetails(_httpContextAccessor.HttpContext);

    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }

    public string GetDeviceDetailsHash(HttpContext context) =>
        _securityService.GetSha256Hash(GetDeviceDetails(context));

    public string GetCurrentRequestDeviceDetailsHash() => GetDeviceDetailsHash(_httpContextAccessor.HttpContext);

    public string GetCurrentUserTokenDeviceDetailsHash() =>
        GetUserTokenDeviceDetailsHash(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public string GetUserTokenDeviceDetailsHash(ClaimsIdentity claimsIdentity)
    {
        if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
        {
            return null;
        }

        return claimsIdentity.FindFirst(ClaimTypes.System)?.Value;
    }

    public bool HasCurrentUserTokenValidDeviceDetails() =>
        HasUserTokenValidDeviceDetails(_httpContextAccessor.HttpContext?.User.Identity as ClaimsIdentity);

    public bool HasUserTokenValidDeviceDetails(ClaimsIdentity claimsIdentity) =>
        string.Equals(GetCurrentRequestDeviceDetailsHash(), GetUserTokenDeviceDetailsHash(claimsIdentity),
                      StringComparison.Ordinal);

    private static string GetUserAgent(HttpContext context)
    {
        if (context is null)
        {
            return null;
        }

        return context.Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgent)
                   ? userAgent.ToString()
                   : null;
    }
}
توضیحات:
اصل کار این سرویس در متد زیر رخ می‌دهد:
    public string GetDeviceDetails(HttpContext context)
    {
        var ua = GetUserAgent(context);
        if (ua is null)
        {
            return "unknown";
        }

        var client = Parser.GetDefault().Parse(ua);
        var deviceInfo = client.Device.Family;
        var browserInfo = $"{client.UA.Family}, {client.UA.Major}.{client.UA.Minor}";
        var osInfo = $"{client.OS.Family}, {client.OS.Major}.{client.OS.Minor}";
        //TODO: Add the user's IP address here, if it's a banking system.
        return $"{deviceInfo}, {browserInfo}, {osInfo}";
    }
در اینجا با استفاده از کتابخانه‌ی UA-Parser، سعی می‌کنیم تا جزئیات مرورگر و سیستم عامل شخص را تهیه کنیم. سپس در قسمت دیگری از این سرویس، این اطلاعات را هش می‌کنیم. از این جهت که هم حجم آن کاهش یابد و بی‌جهت کوکی و یا توکن ما را حجیم نکند و هم بررسی محتوای آن جهت شبیه سازی آن، غیرممکن شود. هر مشخصات دریافتی در حین لاگین، همواره یک هش مشخص و یکتا را دارد. به همین جهت متدهای هش کردن اطلاعات را هم در اینجا مشاهده می‌کنید. به علاوه‌ی متد HasUserTokenValidDeviceDetails که کار آن، دریافت Claim مرتبط با این اطلاعات، از کوکی و یا توکن جاری و مقایسه‌ی آن با اطلاعات Http Request جاری است. اگر این دو یکی نبودند، یعنی احتمال سوء استفاده‌ی از اطلاعات شخص، وجود دارد.


اضافه کردن اطلاعات مشخصات دستگاه کاربر به کوکی و یا توکن او

همانطور که عنوان شد، در متد HasUserTokenValidDeviceDetails، ابتدا مشخصات دستگاه موجود در کوکی و یا توکن دریافتی، استخراج می‌شود. به همین جهت نیاز است این مشخصات را دقیقا در حین لاگین موفق، به صورت یک Claim جدید، برای مثال از نوع ClaimTypes.System به مجموعه‌ی Claims کاربر اضافه کرد:
new(ClaimTypes.System, _deviceDetectionService.GetCurrentRequestDeviceDetailsHash(),
ClaimValueTypes.String, _configuration.Value.Issuer),


یکپارچه کردن DeviceDetectionService با اعتبارسنج‌های کوکی‌ها و توکن‌ها

پس از افزودن مشخصات سیستم کاربر وارد شده‌ی به سیستم، به صورت یک Claim جدید به توکن‌ها، روش اعتبارسنجی اطلاعات موجود در توکن رسیده، در رخ‌داد گردان OnTokenValidated است که امکان دسترسی به HttpContext و محتوای توکن را میسر می‌کند:
.AddJwtBearer(cfg =>
{
      cfg.Events = new JwtBearerEvents
      {
           OnTokenValidated = context =>
           {
               var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenValidatorService>();
              return tokenValidatorService.ValidateAsync(context);
           },
       };
  });
و یا اگر از کوکی‌ها استفاده می‌کنید، معادل آن به صورت زیر است:
.AddCookie(options =>
{
    options.Events = new CookieAuthenticationEvents
    {
       OnValidatePrincipal = context =>
       {
         var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
         return cookieValidatorService.ValidateAsync(context);
       }
    };
});

در کل تمام تغییرات مورد نیاز مرتبط را جهت یک برنامه‌ی تولید کننده‌ی JWT در اینجا و برای یک برنامه‌ی مبتنی بر کوکی‌ها در اینجا می‌توانید مشاهده کنید.
مطالب دوره‌ها
آشنایی با مدل برنامه نویسی TAP
تاریخچه‌ی اعمال غیر همزمان در دات نت فریم ورک

دات نت فریم ورک، از زمان ارائه نگارش یک آن، از اعمال غیرهمزمان و API خاص آن پشتیبانی می‌کرده‌است. همچنین این مورد یکی از ویژگی‌های Win32 نیز می‌باشد. نوشتن کدهای همزمان متداول بسیار ساده است. در این نوع کدها هر عملیات خاص، پس از پایان عملیات قبلی انجام می‌شود.
        public string TestNoneAsync()
        {
            var webClient = new WebClient();
            return webClient.DownloadString("http://www.google.com");
        }
در این مثال متداول، متد DownloadString به صورت همزمان یا synchronous عمل می‌کند. به این معنا که تا پایان عملیات دریافت اطلاعات از وب، منتظر مانده و ترد جاری را قفل می‌کند. مشکل از جایی آغاز می‌شود که مدت زمان دریافت اطلاعات، طولانی باشد. چون این عملیات در ترد UI در حال انجام است، کل رابط کاربری برنامه تا پایان عملیات نیز قفل شده و دیگر پاسخگوی سایر اعمال رسیده نخواهد بود. در این حالت عموما ویندوز در نوار عنوان برنامه، واژه‌های Not responding را نمایش می‌دهد.
این مورد همچنین در برنامه‌های سمت سرور نیز حائز اهمیت است. با قفل شدن تعداد زیادی ترد در حال اجرا، عملا قدرت پاسخ‌دهی سرور نیز کاهش می‌یابد. بنابراین در این نوع موارد، برنامه‌های چند ریسمانی هرچند در سمت کلاینت ممکن است مفید واقع شوند و برای مثال ترد UI را آزاد کنند، اما اثر آنچنانی بر روی برنامه‌های سمت سرور ندارند. زیرا در آن‌ها می‌توان هزاران ترد را ایجاد کرد که همگی دارای کدهای اصطلاحا blocking باشند. برای حل این مساله استفاده از API غیرهمزمان توصیه می‌شود.
برای نمونه کلاس WebClient توکار دات نت، دارای متدی به نام DownloadStringAsync نیز می‌باشد. این متد به محض فراخوانی، ترد جاری را آزاد می‌کند. به این معنا که فراخوانی آن سبب توقف ترد جاری برای دریافت نتیجه‌ی دریافت اطلاعات از وب نمی‌شود. به این نوع API، یک Asynchronous API گفته می‌شود؛ زیرا با سایر کدهای نوشته شده، هماهنگ و همزمان اجرا نمی‌شود.
هر چند این کد جدید مشکل عدم پاسخ دهی برنامه را برطرف می‌کند، اما مشکل دیگری را به همراه دارد؛ چگونه باید حاصل عملیات آن‌را پس از پایان کار دریافت کرد؟ چگونه باید خطاها و مشکلات احتمالی را مدیریت کرد؟
برای مدیریت این مساله، رخدادی به نام DownloadStringCompleted تعریف شده‌است. روال رویدادگردان آن پس از پایان کار دریافت اطلاعات از وب، فراخوانی می‌گردد.
        public void TestAsync()
        {
            var webClient = new WebClient();
            webClient.DownloadStringAsync(new Uri("http://www.google.com"));
            webClient.DownloadStringCompleted += webClientDownloadStringCompleted;
        }

        void webClientDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            // use e.Result
        }
در اینجا همچنین توسط آرگومان DownloadStringCompletedEventArgs، موفقیت یا شکست عملیات نیز گزارش می‌شود و مقدار e.Result حاصل عملیات است.

مشکل!
ما سادگی یک عملیات همزمان را از دست دادیم. متد TestNoneAsync از لحاظ پیاده سازی و همچنین خواندن و نگهداری آن در طول زمان، بسیار ساده‌تر است از نمونه‌ی TestAsync نوشته شده. در کدهای غیرهمزمان فوق، یک متد ساده، به دو متد مجزا خرد شده‌است و نتیجه‌ی نهایی، درون یک روال رخدادگردان بدست می‌آید.
به این مدل، EAP یا Event based asynchronous pattern نیز گفته می‌شود. EAP در دات نت 2 معرفی شد. روال‌های رخدادگردان در این حالت، در ترد اصلی برنامه اجرا می‌شوند. اما اگر به حالت اصلی اعمال غیرهمزمان موجود از دات نت یک کوچ کنیم، اینطور نیست. در WinForms و WPF برای به روز رسانی رابط کاربری نیاز است اطلاعات دریافت شده در همان تردی که رابط کاربری ایجاد شده است، تحویل گرفته شده و استفاده شوند. در غیراینصورت استثنایی صادر شده و برنامه خاتمه می‌یابد.


آشنایی با Synchronization Context

ابتدا یک برنامه‌ی WinForms ساده را آغاز کرده و یک دکمه‌ی جدید را به نام btnGetInfo و یک تکست باکس را به نام txtResults، به آن اضافه کنید. سپس کدهای فرم اصلی آن‌را به نحو ذیل تغییر دهید:
using System;
using System.Linq;
using System.Net;
using System.Windows.Forms;

namespace Async02
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnGetInfo_Click(object sender, EventArgs e)
        {
            var req = (HttpWebRequest)WebRequest.Create("http://www.google.com");
            req.Method = "HEAD";
            req.BeginGetResponse(
                asyncResult =>
                {
                    var resp = (HttpWebResponse)req.EndGetResponse(asyncResult);
                    var headersText = formatHeaders(resp.Headers);
                    txtResults.Text = headersText;
                }, null);
        }

        private string formatHeaders(WebHeaderCollection headers)
        {
            var headerString = headers.Keys.Cast<string>()
                                      .Select(header => string.Format("{0}:{1}", header, headers[header]));
            return string.Join(Environment.NewLine, headerString.ToArray());
        }
    }
}
در اینجا از روش دیگری برای دریافت اطلاعات از وب استفاده کرده‌ایم. با استفاده از امکانات HttpWebRequest، کوئری‌های پیشرفته‌تری را می‌توان تهیه کرد. برای مثال می‌توان نوع متد را به HEAD تنظیم نمود؛ تا صرفا مقادیر هدر آدرس درخواستی از سرور، دریافت شوند.
همچنین در این مثال از متد غیرهمزمان BeginGetResponse نیز استفاده شده‌است. در این نوع API خاص، کار با BeginGetResponse آغاز شده و سپس در callback نهایی توسط EndGetResponse، نتیجه‌ی عملیات به دست می‌آید.
اگر برنامه را اجرا کنید، با استثنای زیر مواجه خواهید شد:
 An exception of type 'System.InvalidOperationException' occurred in System.Windows.Forms.dll but was not handled in user code
Additional information: Cross-thread operation not valid: Control 'txtResults' accessed from a thread other than the thread it was created on.
علت اینجا است که asyncResult دریافتی، در تردی دیگر نسبت به ترد اصلی برنامه که UI را اداره می‌کند، اجرا می‌شود. یکی از راه حل‌های این مشکل و انتقال اطلاعات به ترد اصلی برنامه، استفاده از Synchronization Context است:
        private void btnGetInfo_Click(object sender, EventArgs e)
        {
            var sync = SynchronizationContext.Current;
            var req = (HttpWebRequest)WebRequest.Create("http://www.google.com");
            req.Method = "HEAD";
            req.BeginGetResponse(
                asyncResult =>
                {
                    var resp = (HttpWebResponse)req.EndGetResponse(asyncResult);
                    var headersText = formatHeaders(resp.Headers);
                    sync.Post(delegate { txtResults.Text = headersText; }, null);
                }, null);
        }
SynchronizationContext.Current در اینجا چون در ابتدای متد دریافت اطلاعات اجرا می‌شود، به ترد UI، یا ترد اصلی برنامه اشاره می‌کند. به همین جهت این زمینه را نباید داخل Async callback دریافت کرد؛ زیرا ترد جاری آن، ترد UI مدنظر ما نیست. سپس همانطور که ملاحظه می‌کنید، توسط متد Post آن می‌توان اطلاعات را در زمینه‌ی تردی که SynchronizationContext به آن اشاره می‌کند اجرا کرد.


برای درک بهتر آن، سه break point را پیش از متد BeginGetResponse، داخل  Async calback و داخل delegate متد Post قرار دهید. پس از اجرای برنامه، از منوی دیباگ در VS.NET گزینه‌ی Windows و سپس Threads را انتخاب کنید.
در اینجا همانطور که مشخص است، کد داخل delegate تعریف شده، در ترد اصلی برنامه اجرا می‌شود و نه یکی از Worker threadهای ثانویه.
هر چند استفاده از متدهای تو در تو و lambda syntax، نیاز به تعریف چندین متد جداگانه را برطرف کرده‌است، اما باز هم کد ساده‌ای به نظر نمی‌رسد. در سی شارپ 5، برای مدیریت بهتر تمام مشکلات یاد شده، پشتیبانی توکاری از اعمال غیرهمزمان، به هسته‌ی زبان اضافه شده‌است.


Syntax ابتدایی یک متد Async

در ابتدا کلاس و متد Async زیر را در نظر بگیرید:
using System;
using System.Threading.Tasks;

namespace Async01
{
    public class AsyncExample
    {
        public async Task DoWorkAsync(int parameter)
        {
            await Task.Delay(parameter);
            Console.WriteLine(parameter);
        }
    }
}
شیوه‌ی نگارش آن بر اساس راهنمای نوشتن برنامه‌های Async یا Task asynchronous programming model یا به اختصار TAP است:
- در مدل برنامه نویسی TAP، متدهای غیرهمزمان باید یک Task را بازگشت دهند؛ یا نمونه‌ی جنریک آن‌را. البته کامپایلر، async void را نیز پشتیبانی می‌کند ولی در قسمت‌های بعدی بررسی خواهیم کرد که چرا استفاده از آن مشکل‌زا است و باید از آن پرهیز شود.
- همچنین مطابق TAP، اینگونه متدها باید به پسوند Async ختم شوند تا استفاده کننده در حین کار با Intellisense، بتواند آ‌ن‌ها را از متدهای معمولی سریعتر تشخیص دهد.
- از واژه‌ی کلیدی async نیز استفاده می‌گردد تا کامپایلر از وجود اعمال غیر همزمان مطلع گردد.
- await به کامپایلر می‌گوید، عبارت پس از من، یک وظیفه‌ی غیرهمزمان است و ادامه‌ی کدهای نوشته شده، تنها زمانی باید اجرا شوند که عملیات غیرهمزمان معرفی شده، تکمیل گردد.

در متد DoWorkAsync، ابتدا به اندازه‌‌ای مشخص توقف حاصل شده و سپس سطر بعدی یعنی Console.WriteLine اجرا می‌شود.


یک اشتباه عمومی! استفاده از واژه‌های کلیدی async و await متد شما را async نمی‌کنند.

برخلاف تصور ابتدایی از بکارگیری واژه‌های کلیدی async و await، این کلمات نحوه‌ی اجرای متد شما را async نمی‌کنند. این کلمات صرفا برای تشکیل متدهایی که هم اکنون غیرهمزمان هستند، مفید می‌باشند. برای توضیح بیشتر آن به مثال ذیل دقت کنید:
        public async Task<double> GetNumberAsync()
        {
            var generator = new Random();
            await Task.Delay(generator.Next(1000));

            return generator.NextDouble();
        }
در این متد با استفاده از Task.Delay، انجام یک عملیات طولانی شبیه سازی شده‌است؛ مثلا دریافت یک عدد یا نتیجه از یک وب سرویس. سپس در نهایت، عددی را بازگشت داده است. برای بازگشت یک خروجی double، در اینجا از نمونه‌ی جنریک Task استفاده شده‌است.
در ادامه برای استفاده از آن خواهیم داشت:
        public async Task<double> GetSumAsync()
        {
            var leftOperand = await GetNumberAsync();
            var rightOperand = await GetNumberAsync();

            return leftOperand + rightOperand;
        }
خروجی این متد تنها زمانی بازگشت داده می‌شود که نتایج leftOperand و rightOperand از وب سرویس فرضی، دریافت شده باشند و در اختیار مصرف کننده قرارگیرند. بنابراین همانطور که ملاحظه می‌کنید از واژه‌ی کلیدی await جهت تشکیل یک عملیات غیرهمزمان و مدیریت ساده‌تر کدهای نهایی، شبیه به کدهای معمولی همزمان استفاده شده‌است.
در کدهای همزمان متداول، سطر اول ابتدا انجام می‌شود و بعد سطر دوم و الی آخر. با استفاده از واژه‌ی کلیدی await یک چنین عملکردی را با اعمال غیرهمزمان خواهیم داشت. پیش از این برای مدیریت اینگونه اعمال از یک سری callback و یا رخداد استفاده می‌شد. برای مثال ابتدا عملیات همزمانی شروع شده و سپس نتیجه‌ی آن در یک روال رخ‌داد گردان جایی در کدهای برنامه دریافت می‌شد (مانند مثال ابتدای بحث). اکنون تصور کنید که قصد داشتید جمع نهایی حاصل دو عملیات غیرهمزمان را از دو روال رخدادگردان جدا از هم، جمع آوری کرده و بازگشت دهید. هرچند اینکار غیرممکن نیست، اما حاصل کار به طور قطع آنچنان زیبا نبوده و قابلیت نگهداری پایینی دارد. واژه‌ی کلیدی await، انجام اینگونه امور غیرهمزمان را طبیعی و همزمان جلوه می‌دهد. به این ترتیب بهتر می‌توان بر روی منطق و الگوریتم‌های مورد استفاده تمرکز داشت، تا اینکه مدام درگیر مکانیک اعمال غیرهمزمان بود.

امکان استفاده از واژه‌ی کلیدی await در هر جایی از کدها وجود دارد. برای نمونه در مثال زیر، برای ترکیب دو عملیات غیرهمزمان، از await در حین تشکیل عملیات ضرب نهایی، دقیقا در جایی که مقدار متد باید بازگشت داده شود، استفاده شده‌است:
        public async Task<double> GetProductOfSumAsync()
        {
            var leftOperand = GetSumAsync();
            var rightOperand = GetSumAsync();

            return await leftOperand * await rightOperand;
        }
اگر await را از این مثال حذف کنیم، خطای کامپایل زیر را دریافت خواهیم کرد:
 Operator '*' cannot be applied to operands of type 'System.Threading.Tasks.Task<double>' and 'System.Threading.Tasks.Task<double>'
خروجی متد GetSumAsync صرفا یک Task است و نه یک عدد. پس از استفاده از await، عملیات آن انجام شده و بازگشت داده می‌شود.


اگر متد DownloadString همزمان ابتدای بحث را نیز بخواهیم تبدیل به نمونه‌ی async سی‌شارپ 5 کنیم، می‌توان از متد الحاقی جدید آن به نام DownloadStringTaskAsync کمک گرفت:
        public async Task<string> DownloadAsync()
        {
            var webClient = new WebClient();
            return await webClient.DownloadStringTaskAsync("http://www.google.com");
        }
نکته‌ی مهم این کد علاوه بر ساده سازی اعمال غیر همزمان، برای استفاده از نتیجه‌ی نهایی آن، نیازی به SynchronizationContext معرفی شده در تاریخچه‌ی ابتدای بحث نیست. نتیجه‌ی دریافتی از آن در ترد اصلی برنامه تحویل داده شده و به سادگی قابل استفاده است.


سؤال: آیا استفاده از await نیز ترد جاری را قفل می‌کند؟

اگر به کدها دقت کنید، استفاده از await به معنای صبر کردن تا پایان عملیات async است. پس اینطور به نظر می‌رسد که در اینجا نیز ترد اصلی، همانند قبل قفل شده‌است.
        public void TestDownloadAsync()
        {
            Debug.WriteLine("Before DownloadAsync");
            DownloadAsync();
            Debug.WriteLine("After DownloadAsync");
        }
اگر این متد را اجرا کنید (در آن await بکار نرفته)، بلافاصله خروجی ذیل را مشاهده خواهید کرد:
 Before DownloadAsync
After DownloadAsync
به این معنا که در اصل، همانند سایر روش‌های async موجود از دات نت یک، در اینجا نیز فراخوانی متد async ترد اصلی را بلافاصله آزاد می‌کند و ترد آن‌را قفل نخواهد کرد. استفاده از await نیز عملکرد کدها را تغییر نمی‌دهد. تنها کامپایلر در پشت صحنه همان کدهای لازم جهت مدیریت روال‌های رخدادگردان و callbackها را تولید می‌کند، به نحوی که صرفا نحوه‌ی کدنویسی ما همزمان به نظر می‌رسد، اما در پشت صحنه، نحوه‌ی اجرای آن غیرهمزمان است.


برنامه‌های Async و نگارش‌های مختلف دات نت

شاید در ابتدا به نظر برسد که قابلیت‌های جدید async و await صرفا متعلق هستند به دات نت 4.5 به بعد؛ اما خیر. اگر کامپایلری را داشته باشید که از این واژه‌های کلیدی را پشتیبانی کند، امکان استفاده از آن‌ها را با دات نت 4 نیز خواهید داشت. برای این منظور تنها کافی است از VS 2012 به بعد استفاده نمائید. سپس در کنسول پاورشل نیوگت دستور ذیل را اجرا نمائید (فقط برای برنامه‌های دات نت 4 البته):
 PM> Install-Package Microsoft.Bcl.Async
این روال متداول VS.NET بوده است تا به امروز. برای مثال اگر VS 2010 را نصب کنید و سپس یک برنامه‌ی دات نت 3.5 را ایجاد کنید، امکان استفاده‌ی کامل از تمام امکانات سی‌شارپ 4، مانند آرگومان‌های نامدار و یا مقادیر پیش فرض آرگومان‌ها را در یک برنامه‌ی دات نت 3.5 نیز خواهید داشت. همین نکته در مورد async نیز صادق است. VS 2012 (یا نگارش‌های جدیدتر) را نصب کنید و سپس یک پروژه‌ی دات نت 4 را آغاز کنید. امکان استفاده از async و await را خواهید داشت. البته در این حالت دسترسی به متدهای الحاقی جدید را مانند DownloadStringTaskAsync نخواهید داشت. برای رفع این مشکل باید بسته‌ی  Microsoft.Bcl.Async را نیز توسط نیوگت نصب کنید.
مطالب
محدود کردن دسترسی به اس کیوال سرور بر اساس IP

عموما محدود کردن دسترسی بر اساس IP بهتر است بر اساس راه حل‌هایی مانند فایروال، IPSec و یا RRAS IP Filter صورت گیرد که جزو بهینه‌ترین و امن‌ترین راه حل‌های ممکن هستند.
در ادامه قصد داریم این محدودیت را با استفاده از امکانات خود اس کیوال سرور انجام دهیم (بلاک کردن کاربران بر اساس IP های غیرمجاز). مواردی که در ادامه ذکر خواهند شد در مورد اس کیوال سرور 2005 ، سرویس پک 2 به بعد و یا اس کیوال سرور 2008 صادق است.
اس کیوال سرور این قابلیت را دارد که می‌توان بر روی کلیه لاگین‌های صورت گرفته در سطح سرور تریگر تعریف کرد. به این صورت می‌توان تمامی لاگین‌ها را برای مثال لاگ کرد (جهت بررسی مسایل امنیتی) و یا می‌توان هر لاگینی را که صلاح ندانستیم rollback نمائیم (ایجاد محدودیت روی لاگین در سطح سرور).

لاگ کردن کلیه لاگین‌های صورت گرفته به سرور

ایجاد جدولی برای ذخیره سازی اطلاعات لاگین‌ها:

USE [master]
GO

SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

SET ANSI_PADDING ON
GO

CREATE TABLE [dbo].[Logging](
[id] [int] IDENTITY(1,1) NOT NULL,
[LogonTime] [datetime] NULL,
[LoginName] [nvarchar](max) NULL,
[ClientHost] [varchar](50) NULL,
[LoginType] [varchar](100) NULL,
[AppName] [nvarchar](500) NULL,
[FullLog] [xml] NULL,
CONSTRAINT [PK_IP_Log] PRIMARY KEY CLUSTERED
(
[id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

SET ANSI_PADDING OFF
GO

ALTER TABLE [dbo].[Logging] ADD CONSTRAINT [DF_IP_Log_LogonTime] DEFAULT (getdate()) FOR [LogonTime]
GO

در ادامه یک تریگر لاگین را جهت ذخیره سازی اطلاعات کلیه لاگین‌ها به سرور ایجاد می‌نمائیم:
USE [master]
GO

CREATE TRIGGER LogonTrigger
ON ALL SERVER
FOR LOGON
AS
BEGIN
DECLARE @data XML
SET @data = EVENTDATA()

INSERT INTO [Logging]
(
[LoginName],
[ClientHost],
[LoginType],
[AppName],
[FullLog]
)
VALUES
(
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'nvarchar(max)'),
@data.value('(/EVENT_INSTANCE/ClientHost)[1]', 'varchar(50)'),
@data.value('(/EVENT_INSTANCE/LoginType)[1]', 'varchar(100)'),
APP_NAME(),
@data
)
END
اکنون برای مثال از آخرین 100 لاگین انجام شده، به صورت زیر می‌توان گزارشگیری کرد:

SELECT TOP 100 * FROM [master].[dbo].[Logging] ORDER BY id desc
و بدیهی است در تریگر فوق می‌توان روی هر کدام از آیتم‌های دریافتی مانند ClientHost و غیره فیلتر ایجاد کرد و تنها موارد مورد نظر را ثبت نمود.

محدود کردن کاربران بر اساس IP

ClientHost ایی که در رخ‌داد لاگین فوق بازگشت داده می‌شود همان IP کاربر راه دور است. برای فیلتر کردن IP های غیرمجاز، ابتدا در دیتابیس مستر یک جدول برای ذخیره سازی IP های مجاز ایجاد می‌کنیم و IP های کلیه کلاینت‌های معتبر خود را در آن وارد می‌کنیم:

USE [master]
GO
CREATE TABLE [IP_RESTRICTION](
[ValidIP] [varchar](15) NOT NULL,
CONSTRAINT [PK_IP_RESTRICTION] PRIMARY KEY CLUSTERED
(
[ValidIP] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

سپس تریگر لاگین ما برای منع کاربران غیرمجاز بر اساس IP ، به صورت زیر خواهد بود:

USE [master]
GO

CREATE TRIGGER [LOGIN_IP_RESTRICTION]

ON ALL SERVER
FOR LOGON
AS
BEGIN
DECLARE @host NVARCHAR(255);
SET @host = EVENTDATA().value('(/EVENT_INSTANCE/ClientHost)[1]', 'nvarchar(max)');

IF (
NOT EXISTS(
SELECT *
FROM MASTER.dbo.IP_RESTRICTION
WHERE ValidIP = @host
)
)
BEGIN
ROLLBACK;
END
END;
اخطار مهم!
تریگر فوق خطرناک است! ممکن است خودتان هم دیگر نتوانید لاگین کنید!! (حتی با اکانت ادمین)
بنابراین قبل از لاگین حتما IP لوکال و یا ClientHost لوکال را هم وارد کنید.
اگر گیر افتادید به صورت زیر می‌شود رفع مشکل کرد:
تنها حالتی که تریگر لاگین را فعال نمی‌کند Dedicated Administrator Connection است یا DAC هم به آن گفته می‌شود. به صورت پیش فرض برای ایجاد این اتصال اختصاصی باید به کامپیوتری که اس کیوال سرور بر روی آن نصب است به صورت لوکال لاگین کرد و سپس در خط فرمان دستور زیر را صادر کنید (حرف A آن باید بزرگ باشد):

C:\>sqlcmd -A -d master -q "insert into IP_RESTRICTION(validip) values('<local machine>')"
به این صورت local machine به جدول IP های مجاز اضافه شده و می‌توانید لاگین کنید!
این نوع تریگرها در قسمت server objects در management studio ظاهر می‌شوند.

نظرات مطالب
بدست آوردن برگهای یک درخت توسط Recursive CTE
من دقیقا متوجه نشدم نتیجه مورد نظر شما چیست.
آیا نتیجه مورد نظر شما به صورت الحاق یافته (concatenated)  هست یا نه؟
در هر صورت باید یکی از دو query زیر نتیجه مورد نظر شما را تولید کند.
declare @t table
(id char(1) primary key,
parent char(1));
 
insert @t values
('A',null),                                   --Level 1
('B', 'A'), ('C', 'A'),                       --Level 2
('D', 'B'), ('E', 'B'),('R','B'), ('F', 'C'), --Level 3
('G', 'D'),                                   --Level 4
('H', 'G'), ('I', 'G');                       --Level 5

;with cte as
(
select id, rnk=0, 
       concats = cast(id as varchar(10))
from @t
where parent is null
 
union all
 
select t.id, rnk+1,
       cast(cte.concats + t.id as varchar(10))
from cte join @t t
on cte.id = t.parent
)
select * from cte
/*
id   rnk         concats
---- ----------- ----------
A    0           A
B    1           AB
C    1           AC
F    2           ACF
D    2           ABD
E    2           ABE
R    2           ABR
G    3           ABDG
H    4           ABDGH
I    4           ABDGI
*/
;with cte as
(
select id, rnk=0, 
       concats = cast(id as varchar(10))
from @t
where parent is null
 
union all
 
select t.id, rnk+1,
       cast(cte.concats + t.id as varchar(10))
from cte join @t t
on cte.id = t.parent
)
select stuff(d.list,1,1,'') as concats
from (select ','+concats
      from cte
  for xml path(''))d(list)
/*
concats
----------------------------------------
A,AB,AC,ACF,ABD,ABE,ABR,ABDG,ABDGH,ABDGI
*/

موفق باشید
نظرات مطالب
شروع به کار با DNTFrameworkCore - قسمت 4 - پیاده‌سازی CRUD API موجودیت‌ها
بررسی شد، مشکلی یافت نشد. برای تست، پروژه DNTFrameworkCore.TestAPI را اجرا کرده و با ابزار Postman درخواست زیر را انجام دهید.

درخواست PUT (حذف Blogs_Edit، ویرایش Blogs_Create و افزودن Blogs_UseCase)
{
    "name": "Administrators",
    "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
    "permissions": [
        {
            "name": "Blogs_View1",
            "id": 1,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Edit",
            "id": 2,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Create",
            "id": 3,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_View",
            "id": 4,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Delete",
            "id": 5,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Edit",
            "id": 6,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Create",
            "id": 7,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Delete",
            "id": 8,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_View",
            "id": 9,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Edit",
            "id": 10,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Create",
            "id": 11,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_View",
            "id": 12,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Delete",
            "id": 13,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Create1",
            "id": 15,
            "trackingState": "Modified",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_UseCase",
            "trackingState": "Added"
        },
        {
            "name": "Blogs_Edit",
            "id": 14,
            "trackingState": "Deleted"
        },
    ],
    "rowVersion": "AAAAAAAAF3M=",
    "id": 1
}

پاسخ سرور:
{
    "name": "Administrators",
    "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
    "permissions": [
        {
            "name": "Blogs_View1",
            "id": 1,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Edit",
            "id": 2,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Create",
            "id": 3,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_View",
            "id": 4,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Delete",
            "id": 5,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Edit",
            "id": 6,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_Create",
            "id": 7,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Tasks_Delete",
            "id": 8,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Users_View",
            "id": 9,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Edit",
            "id": 10,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_Create",
            "id": 11,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Roles_View",
            "id": 12,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Delete",
            "id": 13,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_Create1",
            "id": 15,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        },
        {
            "name": "Blogs_UseCase",
            "id": 17,
            "trackingState": "Unchanged",
            "modifiedProperties": null
        }
    ],
    "rowVersion": "AAAAAAAAF3Q=",
    "id": 1
}

دقت کنید که خاصیت راهبری Permissions را در متد BuildFindQuery کلاس سرویس خود، Include کرده باشد.
نکته تکمیلی: در نسخه‌های جدید این زیرساخت، خصوصیت ModifiedProperties به کلاس TrackableEntity به عنوان کلاس پایه موجودیت‎‌هایی که نیاز به TrackingState دارند، اضافه شده است. اگر قصد دارید همه خصوصیات یک رکورد ویرایش شوند، این خصوصیت را با null مقداردهی کنید.

نظرات مطالب
معرفی List Patterns Matching در C# 11
یک نکته‌ی تکمیلی: ایجاد نوع‌های سازگار با List Patterns Matching

در انتهای این مطلب در مورد «سایر نوع‌هایی که توسط List patterns قابل بررسی هستند» توضیحات مختصری عنوان شد. کامپایلر #C در جهت یافتن نوع‌های سازگار با List Patterns Matching، به دنبال اینترفیس خاصی نمی‌گردد؛ بلکه به دنبال وجود یک سری اعضای خاص، در کلاس مدنظر است و این اعضاء به شرح زیر هستند:
الف) نوع مدنظر باید به همراه یکی از خواص Length و یا Count باشد تا تعداد اعضای مجموعه را مشخص کند. اگر هر دو خاصیت با هم حضور داشته باشند، کامپایلر خاصیت Length را انتخاب می‌کند:
public int Length { get; }
public int Count { get; }

ب) نوع مجموعه‌ای باید به همراه یک ایندکسر باشد که نوع خروجی آن مهم نیست. اگر در نوع تعریف شده، هر دو امضای زیر وجود داشته باشند، کامپایلر از نمونه‌ی this[Index index] استفاده می‌کند:
public object this[int index] => throw null;
public object this[System.Index index] => throw null;

ج) نوع مجموعه‌ای باید از slice pattern، توسط یکی از امضاهای زیر که نوع خروجی آن مهم نیست، پشتیبانی کند. اگر هر دو با هم حضور داشته باشند، کامپایلر از this[System.Range index] استفاده می‌کند:
public object this[System.Range index] => throw null;
public object Slice(int start, int length) => throw null;

برای مثال با توجه به نکات فوق، نوع جدید زیر، با List Patterns Matching سازگاری دارد:
public class MyListPatternsCompatibleCollection
{
    private readonly List<int> _items = new();

    public int Length => _items.Count;

    public int this[Index index] => _items[index];

    public ReadOnlySpan<int> this[Range range]
        => CollectionsMarshal.AsSpan(_items)[range];

    public void Add(int item) => _items.Add(item);
}
و نمونه‌ای از نحوه‌ی استفاده‌ی از آن به صورت زیر است:
 var collection = new MyListPatternsCompatibleCollection();
collection.Add(1);
collection.Add(2);
collection.Add(3);

_ = collection is [var head, .. var tail];
مطالب
حذف کاراکتر های ناخواسته توسط Recursive CTE قسمت دوم
مقدمه
در قسمت پیشین نشان داده شد که چگونه کاراکتر‌های خارج از رنج حروف الفبای انگلیسی از عبارات موجود در یک جدول حذف شدند.
اکنون شرایط کمی تغییر کرده است کاراکترهای ناخواسته در قالب یک مجموعه (جدول) به ما ارائه داده می‌شوند. ما بایستی تمام کاراکترهای داده شده را از عبارات (موجود در جدول) در صورت تطابق حذف کنیم.

جدول کاراکترهای ناخواسته Unwanted و جدول داده‌ها Data نامگذاری شده اند.
CREATE TABLE Data 
(
id INTEGER NOT NULL PRIMARY KEY IDENTITY,
data VARCHAR(50) NOT NULL
);

INSERT INTO Data
VALUES ('~!hasan @#$%^&*(reza)[ali]^^^^^^^^'),
('(Ja[][][]va~!@#$*d-mohammad)'),
('Mohammad'), ('Maryam');

CREATE TABLE Unwanted
(
id INT NOT NULL PRIMARY KEY IDENTITY, 
chars CHAR(1) NOT NULL UNIQUE
);

INSERT Unwanted 
VALUES ('~'),('!'),('@'),('#'),('$'),('%'),
('['),(']'),('^'),('&'),('*');
الگوریتم
قبل از هر چیزی فرض میگیریم که داده‌ها ستون Id جدول Unwanted کاملا متوالی نیستند و gap بین مقادیر id وجود دارد. پس برای داشتن مقادیر متوالی از تابع row_number استفاده شده است.
بعد از آن باید عباراتی که حداقل با یک کاراکتر ناخواسته تطابق پیدا میکنند انتخاب شوند. و عملیات پاکسازی روی این عبارات به تعداد کاراکترهای موجود در جدول unwanted انجام می‌شود. یعنی در مرحله اول شمارنده برابر است با تعداد کاراکترهای ناخواسته سپس در هر فراخوانی یک مقدار از این شمارنده کم شده تا اینکه به 0 برسد در اینجا کار به اتمام می‌رسد.
بعد از پایان یافتن فراخوانی‌های query بازگشتی باید سطرهایی که شمارنده آنها برابر با 0 است انتخاب شده و عباراتی که در مرحله اول به دلیل عدم  تطابق انتخاب نشدن بایستی به نتیجه اضافه شوند. این کار را توسط Union all انجام شده است.
WITH pre AS
(
SELECT ROW_NUMBER() OVER(ORDER BY id) AS id, chars
  FROM Unwanted
),
 cte AS
(
SELECT  data.id,
        nbr,
        CAST(Data AS VARCHAR(50)) AS Data
  FROM Data
       CROSS JOIN (SELECT COUNT(*) AS nbr FROM Unwanted)t
 WHERE EXISTS
       (SELECT *
          FROM Unwanted
         WHERE Data LIKE '%#' + chars + '%' ESCAPE '#')
 
 UNION ALL
  
 SELECT C.id,
        nbr - 1,
        CAST(REPLACE(C.data, U.chars, '') AS VARCHAR(50))
   FROM cte AS C
        INNER JOIN pre U
        ON C.nbr = U.id
  WHERE nbr > 0
)
SELECT data
  FROM cte
 WHERE nbr = 0
 
 UNION ALL
  
SELECT data
  FROM Data
 WHERE NOT EXISTS
       (SELECT id           
          FROM cte
         WHERE cte.id = data.id);

مطالب
خواندن اطلاعات API فیدبرنر

مطلبی را در سایت رادیکال 2 در مورد نمایش تعداد خواننده یک فید دیدم که پیاده سازی آن با سی شارپ و xml serialization به صورت زیر است:

using System;
using System.Xml;
using System.Xml.Serialization;

namespace Test
{
/// <summary>
/// کلاسی جهت نمایش تعداد خواننده فید وبلاگ شما
/// <example>CFeedBurner data = new CFeedBurner { FeedID = "fhphjt61bueu08k93ehujpu234" };
/// MessageBox.Show(data.Circulation().ToString());</example>
/// </summary>
class CFeedBurner
{
/// <summary>
/// آی دی فید شما زمانیکه به فید برنر لاگین کرده‌اید در تایتل صفحه مربوطه
/// </summary>
public string FeedID { get; set; }

/// <summary>
/// نگاشت فید به یک کلاس
/// </summary>
/// <returns>کلاس متناظر با فید</returns>
/// <exception cref="Exception">لطفا شماره شناسایی فید را وارد کنید</exception>
rsp deserializeFromXML()
{
if (FeedID == null)
throw new Exception("لطفا شماره شناسایی فید را وارد کنید");

XmlSerializer deserializer =
new XmlSerializer(typeof(rsp));
using (XmlReader reader = XmlReader.Create(
string.Format("https://feedburner.google.com/api/awareness/1.0/GetFeedData?id={0}", FeedID)))
{
return (rsp)deserializer.Deserialize(reader);
}
}

/// <summary>
/// دریافت تعداد خواننده فید
/// </summary>
/// <returns>آمار فید</returns>
/// <exception cref="Exception">اطلاعات فید شما قابل دریافت نیست</exception>
public int Circulation()
{
rsp data = deserializeFromXML();
if (data == null || data.feed == null || data.feed.Length == 0)
throw new Exception("اطلاعات فید شما قابل دریافت نیست");

if (data.feed[0].entry == null || data.feed[0].entry.Length == 0)
throw new Exception("اطلاعات فید شما قابل پردازش نیست");

return int.Parse(data.feed[0].entry[0].circulation);
}
}
}

کلاس rsp از فایل xml فید استاندارد سایت فیدبرنر درست شده است. روش تولید آن‌را قبلا توضیح داده بودم. به سادگی اجرای دو سطر زیر است:

xsd.exe GetFeedData.xml
xsd.exe GetFeedData.xsd /c

دریافت فایل‌های کلاس‌های نامبرده شده.

نظرات مطالب
تنظیمات JSON در ASP.NET Web API
سلام؛ من از کد زیر استفاده کردم
                myUserApi.Id = UserId;
                return new JsonNetResult
                {
                    Data = myUserApi,
                    ContentType = "application/json"
                };
اما این خروجی تولید میشه
{
  "$id": "1",
  "Settings": {
    "$id": "2",
    "ReferenceLoopHandling": 0,
    "MissingMemberHandling": 0,
    "ObjectCreationHandling": 0,
    "NullValueHandling": 0,
    "DefaultValueHandling": 0,
    "Converters": [],
    "PreserveReferencesHandling": 0,
    "TypeNameHandling": 0,
    "MetadataPropertyHandling": 0,
    "TypeNameAssemblyFormat": 0,
    "ConstructorHandling": 0,
    "ContractResolver": null,
    "ReferenceResolver": null,
    "TraceWriter": null,
    "Binder": null,
    "Error": null,
    "Context": {
      "$id": "3",
      "m_additionalContext": null,
      "m_state": 0
    },
    "DateFormatString": "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK",
    "MaxDepth": null,
    "Formatting": 0,
    "DateFormatHandling": 0,
    "DateTimeZoneHandling": 3,
    "DateParseHandling": 1,
    "FloatFormatHandling": 0,
    "FloatParseHandling": 0,
    "StringEscapeHandling": 0,
    "Culture": "(Default)",
    "CheckAdditionalContent": false
  },
  "ContentEncoding": null,
  "ContentType": "application/json",
  "Data": "{\"Id\":2}",
  "JsonRequestBehavior": 1,
  "MaxJsonLength": null,
  "RecursionLimit": null
}