نظرات مطالب
React 16x - قسمت 26 - احراز هویت و اعتبارسنجی کاربران - بخش 1 - ثبت نام و ورود به سیستم
آیا زمان ثبت نام در سمت سرور نباید هم access_token و هم refresh_token رو تولید و به سمت کلاینت ارسال کرد؟
با بررسی کدهای مربوط به backend این سری و مبحث مربوط به اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity  عملیات ثبت نام رو به صورت زیر پیاده سازی کردم.
//do register action and send sms message to client
//then call activateRegisterCode action from client to server
//In the following run operation authentication for user
 [AllowAnonymous]
        [HttpPost("[action]")]
        [IgnoreAntiforgeryToken]
        public async Task<IActionResult> ActivateRegisterCode([FromBody] ActiveCodeModel model)
        {
            if (model == null)
            { return BadRequest("درخواست نامعتبر"); }

            var user = await _userService.FindUserByKeyCodeAsync(model.Keycode);
            if (user == null)
            {
                return BadRequest("کد امنیتی معتبر نیست لطفا مجددا درخواست کد امنیتی کنید");
            }

            user.IsActive = true;
            var userUpdated = await _userService.UpdateAsync(user, CancellationToken.None);
            var jwt = await _tokenFactoryService.CreateJwtTokensAsync(userUpdated);

            this.Response.Headers.Add("x-auth-token", jwt.AccessToken);
            this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
            _antiforgery.RegeneratedAntiForgeryCookie(jwt.Claims);
            return Ok();


        }
در ادامه پیاده سازی action logout  برای حذف توکن‌ها از دیتابیس
 [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout(string refreshtoken)
        {
            var claimsIdentity = this.User.Identity as ClaimsIdentity;
            var userIdValue = claimsIdentity.FindFirst(ClaimTypes.UserData)?.Value;

            if (!string.IsNullOrWhiteSpace(userIdValue) && Guid.TryParse(userIdValue, out Guid userId))
            {
                await _tokenStoreService.InvalidateUserTokensAsync(userId).ConfigureAwait(false);
            }
            await _tokenStoreService.DeleteExpiredTokensAsync().ConfigureAwait(false);
            await _uow.SaveChangesAsync().ConfigureAwait(false);
            _antiforgery.DeleteAntiForgeryCookie();
            return true;
        }
مشکلی که هست تو اکشن logout شی userIdValue خالیه و بنظر میرسه که کوکی مقدار نداره!
آیا باید زمان فراخوانی این اکشن refreshtoken رو هم بهش ارسال کنیم؟
تو اکشن لاگین هم کوکی‌ها مقدار نمیگیره یا اینکه اصلا با کوکی‌ها کار نداریم و بر اساس refreshtoken باید مقادیر claim‌های داخلش رو decode و بررسی کرد و سپس بر اساس اون در بانک اطلاعاتی عملیات خذف توکن‌های صادر شده در زمان ثبت نام و یا لاگین رو انجام داد.
مطالب
آموزش BrightStarDb (قسمت اول)
در طی این پست ها با مفاهیم NoSql آشنا شدید. همچنین در این دوره مفاهیم و مبانی RavenDb (یکی از بی نقص‌ترین دیتابیس‌های NoSql) بررسی شد. اما قرار است در طی چند پست با یکی دیگر از انواع دیتابیس‌های NoSql  طراحی شده برای دات نت به نام  BrightStarDb یا به اختصار  B*Db آشنا شویم.

*در دنیای NoSql پیاده سازی‌های متفاوتی از دیتابیس‌ها انجام شده است و هر دیتابیس، ویژگی‌ها و مزایا و معایب خاص خودش را دارد. باید قبول کرد که همیشه و همه جا نمی‌توان از یک پایگاه داده NoSql مشخص استفاده نماییم (دلایلی نظیر محدودیت‌های License، هزینه پیاده سازی و...). در نتیجه بررسی یک دیتابیس دیگر با شرایط و توانمندی‌های خاص آن خالی از سود نیست.
از ویژگی مهم این دیتابیس می‌توان به عناوین زیر اشاره کرد.
» این دیتاییس در گروه Graph databases‌ها قرار دارد و از  زبان SPARQL (بخوانید Sparkle) برای  کوئری گرفتن و کار با داده‌ها بهره می‌برد؛
» متن باز و رایگان است
» پشتیبانی از دات نت چهار به بعد؛
» قابل استفاده در Mobile Application - Windows phone 7 , 8؛
» بدون شما (Schema Less) و با قابیلت ذخیره سازی triple و به فرمت RDF
» پشتیبانی از Linq و  OData؛
» پشتیبانی از تراکنش‌ها ؛
» پیاده سازی مدل برنامه به صورت Code First؛
» سرعت بالا جهت ذخیره سازی و لود اطلاعات؛
» نیاز به پیکربندی‌های خاص جهت پیاده سازی ندارد؛
» ارائه افزونه رایگان Polaris جهت کوئری گفتن و نمایش Visual داده ها.
و غیره که در ادامه با آن‌ها آشنا خواهید شد.

در B*Db دو روش برای ذخیره سازی اطلاعات وجود دارد:
» Append Only : در این روش تمامی تغییرات (عملیات نوشتن) در انتهای فایل index اضافه خواهد شد. این روش مزایای زیر را به دنبال خواهد داشت:
  • عملیات نوشتن هیچگاه عملیات خواندن اطلاعات را block نمی‌کند. در نتیجه هر تعداد عملیات خواندن فایل (منظور اجرای کوئری‌های SPQRL است) می‌تواند به صورت موازی بر روی Store‌ها اجرا شود.
  • به دلیل اینکه عمل ویرایش واقعی هیچ گاه انجام نمی‌شود (داده‌ها فقط اضافه خواهند شد) همیشه می‌توانید وضعیت Store را به حالت‌های قبلی بازگردانید.
  • عملیات نوشتن اطلاعات بسیار سریع خواهد بود.
از معایب این روش این است که حجم Store‌ها فقط با افزایش داده‌ها زیاد نمی‌شود، بلکه با هر عملیات ویرایش نیز حجم فایل‌های Store افزایش پیاده خواهد کرد. در نتیجه از این روش فقط زمانی که از نظر فضای هارد دیسک محدودیت ندارید استفاده کنید(روش پیش فرض در B*Db نیز همین حالت است)

» Rewritable : در این روش در هنگام اجرای عملیات نوشتن، ابتدا یک کپی از اطلاعات گرفته میشود. سپس داده‌های مورد نظر به کپی گرفته شده اعمال می‌شوند. تا زمانیکه عملیات نوشتن اطلاعات به پایان نرسد، هر گونه دسترسی به اطلاعات جهت عملیات Read بر روی داده اصلی اجرا می‌شود. با استفاده از این روش عملیات Read و Write هیچ گونه تداخلی با هم نخواهند داشت. (چیزی شبیه به ^)

نکته ای که باید به آن دقت داشت این است که فقط در هنگام ساخت Store‌ها می‌توانید نوع ذخیره سازی آن را تعیین نمایید، بعد از ساخت Store امکان سوئیچ بین حالات امکان پذیر نیست.

همان طور که پیشتر گفته شد B*Db  برای ذخیره سازی اطلاعات از سند RDF بهره می‌برد. البته با RDF Syntax‌های متفاوت :

هم چنین در B*Db چهار روش برای دست یابی با داده‌ها (پیاده سازی عملیات CRUD) وجود دارد از قبیل:
» B*Db EntityFramewok
» Data Object Layer
» RDF Client Api
» Dynamic API
که هر کدام در طی پست‌های متفاوت بررسی خواهد شد.

بررسی یک مثال با روش B*Db EntityFramework

برای شروع ابتدا یک پروژه جدید از نوع Console Application ایجاد کنید. سپس از طریق Nuget اقدام به نصب Package  زیر نمایید:
pm> install-Package BirghtStarDb
پکیج بالا تمام کتابخانه‌های لازم جهت کار با B*Db را شامل می‌شود. اگر قصد ندارید از افزونه‌های مربوط به EntityFramework و Code First استفاده نمایید می‌توانید Package زیر را نصب نمایید:
PM> Install-Package BirghtStarDbLibs
این پکیج فقط شامل کتابخانه‌های لازم جهت کار با استفاده از SPRQL است.
بعد از نصب پکیج‌های بالا یک فایل Text Template با نام MyEntityContext.tt  نیز به پروژه افزوده خواهد شد. این فایل جهت تولید خودکار مدل‌های برنامه استفاده می‌شود. اما برای این کار لازم است به ازای هر مدل ابتدا یک اینترفیس ایجاد نمایید. برای مثال:
 [Entity]
    public interface IBook
    {
        public int Code { get; set; }
        public string Title { get; set; }
    }
نکته:
» نیاز به ایجاد یک خاصیت به عنوان Id وجود ندارد. به صورت پیش فرض خاصیت Id با نوع string برای هر مدل پیاده سازی می‌شود. اما اگر قصد دارید این نام خاصیت را تغییر دهید می‌توانید به صورت زیر عمل کنید:
[Entity]
    public interface IBook
    {
        [Identifier]
        public string MyId { get;  }
        public int Code { get; set; }   
        public string Title { get; set; }
    }
در مثال بالا خاصیت MyId به جای خاصیت Id در نظر گرفته می‌شود. مزین شدن با Identifier  و همچنین نداشتن متد set را فراموش نکنید. بعد از ایجاد اینترفیس مورد نظر و اجرای Run Custom Tool بر روی فایل Text Template.tt کلاسی به نام Book به صورت زیر ساخته می‌شود:

استفاده از اینترفیس برای ساخت مدل انعطاف پذیری بالایی را در اختیار ما قرار می‌دهد که بعدا مفصل بحث خواهد شد. برای عملیات درج داده می‌توان به صورت زیر عمل کنید:

 MyEntityContext context = new MyEntityContext("type=embedded;storesdirectory=c:\brightstar;storename=test");
            var book = context.Books.Create();
            book.Code = 1;
            book.Title = "Test";

            context.Books.Add(book);

            context.SaveChanges();
با یک نگاه می‌توان به شباهت مدل پیاده سازی شده بالا به EntityFramework پی برد. اما نکته مهم در مثال بالا ConnectionString پاس داده شده به Context پروژه است. در B*Db چهار روش برای دستیابی به اطلاعات ذخیره شده وجود دارد:
»embedded : در این حالت از طریق آدرس فیزیکی فایل مورد نظر می‌توان یک Connection ایجاد کرد.
»rest : یا استفاده از HTTP یا HTTPS می‌توان به سرویس B*Db دسترسی داشت.
»dotNetRdf : برای ارتباط با یک Store دیگر با استفاده از اتصال دهنده‌های DotNetRDf.
»sparql : اتصال به منبع داده ای دیگر با استفاده از پروتکل SPARQL
در هنگام ایجاد اتصال باید نوع مورد نظر را از حتما تعیین نمایید. با استفاده از storedirctory مکان فیزیکی فایل تعیین خواهد شد.
مطالب
بهبود صفحه‌‌ی بارگذاری اولیه در Blazor WASM
در بار اول اجرای برنامه‌های Blazor WASM، کار دریافت و کش شدن اسمبلی‌های NET Runtime. و برنامه انجام می‌شوند:


در این بین ... اتفاقی رخ نمی‌دهد و کاربر از پیشرفت عملیات آگاه نمی‌شود. در این مطلب قصد داریم این وضعیت را بهبود دهیم.


افزودن یک progress-bar به صفحه‌ی آغازین برنامه‌های Blazor WASM

Blazor امکان دسترسی به چرخه‌ی حیات ابتدایی آن‌را نیز میسر کرده‌است. برای اینکار ابتدا باید به آن گفت که دریافت خودکار تمام موارد مورد نیاز را انجام نده و ما اینکار را خودمان انجام خواهیم داد:
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
برای اینکار باید به تگ اسکریپتی که به blazor.webassembly.js اشاره می‌کند، ویژگی autostart را با مقدار false، افزود. از این پس باید خودمان کار آغاز Blazor را انجام دهیم که در طی دو مرحله، انجام خواهد شد:
الف) تغییر متن Loading پیش‌فرض جهت نمایش یک progress-bar
<body>
<div id="app">
    <div class="d-flex flex-column min-vh-100">
        <div class="d-flex vh-100">
<div class="d-flex w-100 justify-content-center align-self-center">
<div class="d-flex flex-column w-25">
<div>Loading <label id="progressbarLabel"></label></div>
<div class="progress mt-2" style="height: 2em;">
  <div id="progressbar" class="progress-bar progress-bar-striped"></div>  
</div>
</div>
</div>
        </div>
    </div>
</div>
به فایل index.html برنامه مراجعه کرده و بجای loading پیش‌فرض آن، یک چنین طرحی را قرار می‌دهیم که به همراه یک label و یک progressbar در وسط صفحه است:


ب) سپس فایل جدید js/blazorLoader.js را با محتوای زیر اضافه می‌کنیم. در ابتدای این فایل به المان‌های progressbar و progressbarLabel طرح فوق اشاره می‌شود:
(function () {
  let resourceIndex = 0;
  const fetchResponsePromises = [];
  const progressbar = document.getElementById("progressbar");
  const progressbarLabel = document.getElementById("progressbarLabel");
  const loadStart = new Date().getTime();

  if (!isAutostartDisabled()) {
    console.warn(
      "`blazor.webassembly.js` script tag doesn`t have the `autostart=false` attribute."
    );
    return;
  }

  Blazor.start({
    loadBootResource: function (type, filename, defaultUri, integrity) {
      if (type === "dotnetjs") {
        progressbarLabel.innerText = filename;
        return defaultUri;
      }

      const responsePromise = fetch(defaultUri, {
        cache: "no-cache",
        integrity: integrity,
      });
      fetchResponsePromises.push(responsePromise);
      responsePromise.then((response) => {
        if (!progressbar) {
          console.warn("Couldn't find the progressbar element on the page.");
          return;
        }

        if (!progressbarLabel) {
          console.warn(
            "Couldn't find the progressbarLabel element on the page."
          );
          return;
        }

        resourceIndex++;
        const totalResourceCount = fetchResponsePromises.length;
        const percentLoaded = Math.round(
          100 * (resourceIndex / totalResourceCount)
        );
        progressbar.style.width = `${percentLoaded}%`;
        progressbar.innerText = `${percentLoaded} % [${resourceIndex}/${totalResourceCount}]`;
        progressbarLabel.innerText = filename;
        if (percentLoaded >= 100) {
          var span = new Date().getTime() - loadStart;
          console.log(`All done in ${span} ms.`);
        }
      });

      return responsePromise;
    },
  });

  function isAutostartDisabled() {
    var wasmScript = document.querySelector(
      'script[src="_framework/blazor.webassembly.js"]'
    );
    if (!wasmScript) {
      return false;
    }

    var autostart = wasmScript.attributes["autostart"];
    return autostart && autostart.value === "false";
  }
})();
این فایل باید پس از تعریف مدخل blazor.webassembly.js به فایل index.html اضافه شود:
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script src="js/blazorLoader.js"></script>
</body>
توضیحات:
- محتوای blazorLoader.js، به صورت خود اجرا شونده تهیه شده‌است.
- متد Blazor.start، کار آغاز دستی Blazor WASM و دریافت فایل‌های مورد نیاز آن‌را انجام می‌دهد.
- خاصیت loadBootResource آن، به تابعی اشاره می‌کند که پیشنیازهای اجرایی Blazor WASM را دریافت می‌کند.
- در متد سفارشی loadBootResource که تهیه کرده‌ایم، responsePromise‌ها را شمارش کرده و بر اساس تعداد کلی آن‌ها و مواردی که دریافت آن‌ها به پایان رسیده‌است، یک progress-bar را تشکیل و نمایش می‌دهیم.
- تابع isAutostartDisabled، بررسی می‌کند که آیا ویژگی autostart مساوی false، به تگ اسکریپت blazor.webassembly.js اضافه شده‌است یا خیر؟


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید:  BlazorWasmLoadingBar.zip
نظرات مطالب
ASP.NET MVC #18
سلام؛
 User.Identity.IsAuthenticated  همیشه مقدار false رو بر میگردونه دلیلش چیه؟

مطالب
روش صحیح کار با HttpClient در ASP.NET Core 2x
پیشتر مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» را مطالعه کرده بودید. پس از ارائه‌ی NET Core 2.1.، این مجموعه به همراه یک IHttpClientFactory نیز ارائه می‌شود که در اینجا قصد داریم این مورد و همچنین سایر موارد مشابه را بررسی کنیم.


صورت مساله

قصد داریم اطلاعاتی را با فرمت JSON، از یک API خارجی، توسط HttpClient دریافت و سپس آن‌را به یک DTO فرضی، به نام GitHubRepositoryDto نگاشت کنیم.


راه حل 1

در این روش از وهله سازی مستقیم HttpClient به همراه استفاده‌ی از یک عبارت using کمک گرفته شده‌است. همچنین چون عملیات async است، نتیجه‌ی آن‌را به کمک خاصیت Result دریافت کرده‌ایم که پس از آن، کل اطلاعات دریافتی را به صورت یک رشته، در اختیار خواهیم داشت:
public class GitHubClient
{
    public IReadOnlyCollection<GitHubRepositoryDto> GetRepositories()
    {
        using (var httpClient = new HttpClient{BaseAddress = new Uri(GitHubConstants.ApiBaseUrl)})
        {
            var result = httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).Result;
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}
مشکلات این راه حل:
- استفاده از خاصیت Result، هیچگاه ایده‌ی خوبی نبوده است و یک عملیات async را تبدیل به عملیاتی Blocking می‌کند که حتی می‌تواند سبب بروز dead-lock نیز شود.
- HttpClient نباید Dispose شود. علت آن‌را در مطلب «روش استفاده‌ی صحیح از HttpClient در برنامه‌های دات نت» مفصل بررسی کرده‌ایم.
- دریافت کل response یک API به صورت یک رشته‌ی بزرگ، یک Large object heap را به‌وجود می‌آورد که باز هم ایده‌ی خوبی نیست.


راه حل 2

اگر خاصیت Result راه حل 1 را حذف کنیم، به راه حل 2 خواهیم رسید:
public class GitHubClient : IGitHubClient
{
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        using (var httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) })
        {
            var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath);
            return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
        }
    }
}
مزایا:
- اینبار از دسترسی asynchronous واقعی استفاده شده‌است.

معایب:
- ایجاد و تخریب یک HttpClient جدید به ازای هر فراخوانی.
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 3

در این نگارش، HttpClient از طریق وهله سازی در سازنده‌ی کلاس دریافت شده و به این ترتیب امکان استفاده‌ی مجدد را پیدا می‌کند:
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    public GitHubClient()
    {
        _httpClient = new HttpClient { BaseAddress = new Uri(GitHubConstants.ApiBaseUrl) };
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
طول عمر GitHubClient نیز Singleton معرفی می‌شود.
services.AddSingleton<GitHubClient>();
مزایا:
- دسترسی asynchronous واقعی به API مدنظر.
- استفاده‌ی مجدد از HttpClient

معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.
- چون طول عمر GitHubClient از نوع Singleton است و برای همیشه از یک وهله‌ی سراسری استفاده می‌کند، از تغییرات DNS آگاه نخواهد شد.


راه حل 4

تا اینجا همانطور که ملاحظه کردید، به سادگی می‌توان HttpClient را به نحو نادرستی مورد استفاده قرار داد. ایجاد مجدد آن به علت عدم رها شدن بلافاصله‌ی سوکت‌های لایه‌ی زرین آن توسط سیستم عامل، مشکل حادی را به نام sockets exhaustion پدید می‌آورد. به همین جهت، این کلاس باید یکبار نمونه سازی شده و در طول عمر برنامه از همین تک وهله‌ی آن استفاده شود. یک روش اینکار تعریف آن به صورت اشیاء singleton و یا static است. مشکلی که این روش به همراه دارد، عدم باخبر شدن آن از تغییرات DNS است. برای رفع این مسایل، از NET Core 2.1. به بعد، خود مایکروسافت با ارائه‌ی یک IHttpClientFactory، روش استانداری را برای مدیریت وهله‌های HttpClient ارائه کرده‌است:
public class GitHubClient : IGitHubClient
{
    private readonly IHttpClientFactory _httpClientFactory;
    public GitHubClient(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var httpClient = _httpClientFactory.CreateClient("GitHub");
        var result = await httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
با این روش ثبت
services.AddHttpClient("GitHub", x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClient>();
در این روش، IHttpClientFactory به سازنده‌ی کلاس تزریق می‌شوند و از آن برای دسترسی به یک HttpClient جدید، هربار که این متد فراخوانی خواهد شد، استفاده می‌کنیم. بله ... در این حالت نیز یک HttpClient هربار ایجاد خواهد شد؛ اما چون از IHttpClientFactory استفاده می‌کنیم، مشکلی به شمار نمی‌رود. از این جهت که مطابق مستندات آن، هر HttpClient‌ای که به این نحو تولید می‌شود، یک HttpMessageHandler را در پشت صحنه مورد استفاده قرار می‌دهد که عملیات pooling و استفاده‌ی مجدد از آن‌ها، صورت می‌گیرد. یعنی IHttpClientFactory از HttpClient خود، به نحو بهینه‌ای استفاده‌ی مجدد می‌کند و در این حالت سیستم با مشکل کمبود منابع مواجه نخواهد شد و همچنین سربار ایجاد HttpClient‌های جدید نیز به حداقل می‌رسند.

مزیت‌ها:
- استفاده‌ی از یک IHttpClientFactory توکار

معایب:
- استفاده‌ی یک از کلاینت نامدار، بجای یک کلاینت مشخص شده‌ی بر اساس نوع آن.
- دریافت و ذخیره سازی کل response به صورت یک رشته.

روش ثبت services.AddHttpClient را که در اینجا ملاحظه می‌کنید، یک روش ثبت نامدار است و بر اساس نام رشته‌ای GitHub کار می‌کند. همین نام در متد GetRepositories به صورت httpClientFactory.CreateClient("GitHub") برای دسترسی به یک HttpClient جدید استفاده شده‌است.


راه حل 5

در اینجا از یک کلاینت نوع‌دار، بجای یک کلاینت نامدار، استفاده شده‌است:
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    public GitHubClient(HttpClient httpClient)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
    }
    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var result = await _httpClient.GetStringAsync(GitHubConstants.RepositoriesPath).ConfigureAwait(false);
        return JsonConvert.DeserializeObject<List<GitHubRepositoryDto>>(result);
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
برای ثبت یک کلاینت نوع‌دار، از متد AddHttpClient به همراه ذکر نوع کلاس کلاینت، استفاده می‌شود.

مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار، بجای یک نمونه‌ی نامدار

معایب:
- اینبار تمام استفاده کنندگان از IGitHubClient ما باید دارای طول عمر transient باشند (خصوصیت کلاینت‌های نوع‌دار است)؛ برخلاف راه حل‌های پیشین که می‌توانستند singleton تعریف شوند (یا امکان فراخوانی IGitHubClient از سرویس‌های singleton نیز وجود داشت).
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 6

اگر در جائی نیاز به استفاده و تزریق یک کلاینت نوع‌دار، در یک سرویس با طول عمر singleton را داشتید، روش آن به صورت زیر است:
public class GitHubClientFactory
{
    private readonly IServiceProvider _serviceProvider;
    public GitHubClientFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public GitHubClient Create()
    {
        return _serviceProvider.GetRequiredService<GitHubClient>();
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClientFactory>();
در این روش، یک GitHubClientFactory را داریم که یک GitHubClient را باز می‌گرداند. نکته‌ی اصلی آن، کار با ServiceProvider، جهت دسترسی به GitHubClient است. مابقی آن یعنی تعریف GitHubClient، مانند روش 5 است.

مزایا:
- استفاده از IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- استفاده کننده‌ی از GitHubClientFactory، می‌توانند طول عمر singleton نیز داشته باشد

معایب:
- دریافت و ذخیره سازی کل response به صورت یک رشته.


راه حل 7

از اینجا به بعد، هدف ما بهینه سازی عملیات است و رفع مشکل کار با یک رشته‌ی بزرگ. برای این منظور بجای متد GetStringAsync، از متد SendAsync که امکان streaming را فراهم می‌کند، استفاده خواهیم کرد. به این ترتیب، بجای ارسال یک رشته‌ی بزرگ به متد Deserialize، امکان دسترسی به استریم response را توسط آن میسر کرده‌ایم.
public class GitHubClient : IGitHubClient
{
    private readonly HttpClient _httpClient;
    private readonly JsonSerializer _jsonSerializer;
    public GitHubClient(HttpClient httpClient, JsonSerializer jsonSerializer)
    {
        _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        _jsonSerializer = jsonSerializer ?? throw new ArgumentNullException(nameof(jsonSerializer));
    }

    public async Task<IReadOnlyCollection<GitHubRepositoryDto>> GetRepositories()
    {
        var request = CreateRequest();
        var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
        using (var responseStream = await result.Content.ReadAsStreamAsync())
        {
            using (var streamReader = new StreamReader(responseStream))
            using (var jsonTextReader = new JsonTextReader(streamReader))
            {
                return _jsonSerializer.Deserialize<List<GitHubRepositoryDto>>(jsonTextReader);
            }
        }
    }

    private static HttpRequestMessage CreateRequest()
    {
        return new HttpRequestMessage(HttpMethod.Get, GitHubConstants.RepositoriesPath);
    }
}
با این روش ثبت:
services.AddHttpClient<GitHubClient>(x => { x.BaseAddress = new Uri(GitHubConstants.ApiBaseUrl); });
services.AddSingleton<GitHubClientFactory>();
services.AddSingleton<JsonSerializer>();
مزایا:
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- کار با استریم response

معایب:
- استفاده از ResponseContentRead


راه حل 8

در این روش بجای سطر ذیل در راه حل 7
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseContentRead);
از این سطر استفاده خواهیم کرد:
var result = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
در حالت ResponseContentRead که حالت پیش‌فرض نیز هست، تمام هدرها و کل محتوای بازگشتی از سمت سرور باید خوانده شوند تا در اختیار مصرف کننده قرار گیرند، اما در حالت ResponseHeadersRead، فقط برای دریافت هدرها صبر خواهد شد و مابقی آن سریعا به صورت یک استریم در اختیار مصرف کننده قرار می‌گیرد.


مزایا:
- کار با IHttpClientFactory
- استفاده از یک کلاینت نوع‌دار
- کار با استریم response
- استفاده از ResponseHeadersRead

معایب:
- شاید بتوان از کتابخانه‌ی دیگری برای json deserialization استفاده کرد؟
نظرات مطالب
عبارت using و نحوه استفاده صحیح از آن
مطلب جاری بیشتر به شبیه سازی try/finally معادل using که توسط کامپایلر به صورت خودکار تولید می‌شود مرتبط است نه try/catch کلی. بحث dispose خودکار اشیاء disposable و اینکه استفاده از using به دلیلی که عنوان شد مناسب نیست. بنابراین بجای using از SafeUsingBlock استفاده کنید (شبیه سازی بهتر کاری است که کامپایلر در پشت صحنه جهت معادل سازی یا پیاده سازی using انجام می‌دهد؛ اما بدون از دست رفتن استثناهای رخ داده). مابقی را هم ELMAH انجام می‌دهد.
اگر از using استفاده کنید و ELMAH، فقط خطاهای مرتبط با مثلا iTextSharp رو در لاگ‌ها خواهید یافت؛ مثلا شیء document آن dispose شده، اما خطا و مشکل اصلی که به کدهای ما مرتبط بوده و نه iTextSharp، این میان گم خواهد شد. اما با استفاده از SafeUsingBlock ، دلیل اصلی نیز لاگ می‌شود.

پروژه‌ها
ادغام، اضافه کردن و جدا کردن صفحات PDF
این پروژه ساده را به صورت Windows Form Application و با زبان Python ساختم. می‌تواند مثالی باشد برای کسانی که میخواهند پایتون را در ویندوز یاد بگیرند و اجرا کنند. اگر کسی پروژه را امتحان کرد، خوشحال خواهم شد خطا‌ها و اشکالات را اینجا اعلام کند.
  1. ادغام چند فایل PDF به صورت فایل واحد (Merging)
  2. اضافه کردن یک فایل PDF به یک فایل موجود (Appending)
  3. جدا کردن صفحات دلخواه از یک فایل و ذخیره آنها در یک فایل جدا (Splitting)
نحوه اجرا: 
  1. در صورت نصب امکانات پایتون برای ویژوال استودیو مثل سایر پروژه‌های دات نت قابل اجرا و دیباگ است.
  2. از طریق خط فرمان  و دستور ipy که در ادامه مثالی میزنم.
  3. من خودم از طریق یک برنامه کنسول ساده پروژه را خارج از محیط توسعه اجرا میکنم که در ادامه مثالی میزنم.
 اجرا از خط فرمان:
  ipy C:\\PdfTools\\PdfTools.py

اجرا از برنامه کنسول:
var process = new Process();
var startInfo = new ProcessStartInfo
      {
            WindowStyle = ProcessWindowStyle.Hidden,
            FileName = "CMD.exe",
            Arguments = "/C ipy C:\\PdfTools\\PdfTools.py"
      };
process.StartInfo = startInfo;
process.Start();
نکته: در صورتی که بخواهید از برنامه کنسول برای اجرا استفاده کنید آیکن‌های برنامه باید در پوشه Debug وجود داشته باشند!