نظرات مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم

من از طریق کابل گوشیم رو وصل کردم اما در موارد زیر مشکل دارم میشه توضیح بدید چه باید کرد:

1- آنجا که نوشته اید " اولین بار دو برنامه بر روی گوشی شما نصب می‌شوند که برای کار دیباگ در Xamarin لازم هستند" باید دو برنامه در گوشیم نصب بشه ولی فقط یه برنامه نصب شده .

2- طبق نوشته " خوشبختانه به واسطه وجود Xaml edit and continue احتیاجی به Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml ، می‌توانید تاثیر آن را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید." اگر بصورت Emulator برنامه را اجرا کنم با تغییر کد در xml موارد در شبیه ساز هم تغییر میکند اما در کدهای سی شارپ اصلا اجازه نمیده کدها رو تغییر بدم (از طریق stop – continue ) و در حالتی که از طریق   گوشیم بخوام استفاده کنم (بجای شبیه ساز)، با تغییر کدهای xml هم نمیشه تغییرات را روی گوشیم دید و برای کدهای سی شارپ هم که مثل Emulator عمل میکنه یعنی اجازه تغییر نمیده. ولی اگر  روی کدی break point بذارم برنامه را در همانجا break point درست عمل میکنه اما باز نمیتوان کد سی شارپ را تغییر داد.

  ویندوز 10 ورژن 1809
vs2017 ورژن 15.9.4
مطالب
استفاده از Awesomium.NET در برنامه‌های وب
برای تهیه تصاویر سایت‌های معرفی شده در قسمت اشتراک‌های سایت، پیشتر از کنترل WebBrowser دات نت که در پشت صحنه از امکانات IE کمک می‌گیرد، استفاده می‌کردم. بسیار ناپایدار است؛ به روز رسانی مشکلی داشته و وابسته است به سیستم عامل جاری سیستم. برای مثال مرتبا برای تهیه تصاویر بند انگشتی (Thumbnails) سایت‌های تهیه شده با بوت استرپ کرش می‌کرد و این کرش چون از نوع unmaged code است، عملا پروسه IIS وب سایت را از کار می‌انداخت و در این حالت سایت تا ری‌استارت بعدی IIS در دسترس نبود. برای حل این مشکل و بهبود کیفیت تصاویر تهیه شده، از پروژه Awesomium که در حقیقت مرورگر کروم را جهت استفاده در انواع و اقسام زبان‌های برنامه نویسی محصور می‌کند، کمک گرفته شد؛ که شرح آن‌را در ادامه ملاحظه خواهید کرد.


دریافت و نصب Awesomium

پروژه Awesomium دارای یک SDK است که از اینجا قابل دریافت می‌باشد. بعد از نصب آن در مسیر Awesomium SDK\1.7.3.0\wrappers\Awesomium.NET\Assemblies\Packed می‌توانید محصور کننده‌ی دات نتی آن‌را مشاهده کنید. منظور از Packed در اینجا، استفاده از DLLهای فشرده شده‌ی native آن است که در مسیر Awesomium SDK\1.7.3.0\build\bin\packed کپی شده‌اند. بنابراین برای توزیع این نوع برنامه‌ها نیاز است اسمبلی دات نتی Awesomium.Core.dll به همراه دو فایل بومی icudt.dll و awesomium.dll ارائه شوند.


تهیه تصاویر سایت‌ها به کمک Awesomium.NET

پس از نصب Awesomium اگر به مسیر Documents\Awesomium SDK Samples\1.7.3.0\Awesomium.NET\Samples\Core\CSharp\BasicSample مراجعه کنید، مثالی را در مورد تهیه تصاویر سایت‌ها به کمک Awesomium.NET، مشاهده خواهید کرد. خلاصه‌ی آن چند سطر ذیل است:
            try
            {
                using (WebSession mywebsession = WebCore.CreateWebSession(
new WebPreferences() { CustomCSS = "::-webkit-scrollbar { visibility: hidden; }" }))
                {
                    using (var view = WebCore.CreateWebView(1240, 1000, mywebsession))
                    {
                        view.Source = new Uri("https://site.com/");

                        bool finishedLoading = false;
                        view.LoadingFrameComplete += (s, e) =>
                        {
                            if (e.IsMainFrame)
                                finishedLoading = true;
                        };

                        while (!finishedLoading)
                        {
                            Thread.Sleep(100);
                            WebCore.Update();
                        }

                        using (var surface = (BitmapSurface)view.Surface)
                        {
                            surface.SaveToJPEG("result.jpg");
                        }
                    }
                }
            }
            finally
            {
                WebCore.Shutdown();
            }
کار با ایجاد یک WebSession شروع می‌شود. سپس با مقدار دهی CustomCSS، اسکرول بار صفحات را جهت تهیه تصاویری بهتر مخفی می‌کنیم. سپس یک WebView آغاز شده و منبع آن به Url مدنظر تنظیم می‌شود. در ادامه باید اندکی صبر کنیم تا بارگذاری سایت خاتمه یافته و نهایتا می‌توانیم سطح این View را به صورت یک تصویر ذخیره کنیم.


مشکل! این روش در برنامه‌های ASP.NET کار نمی‌کند!

مثال همراه آن یک مثال کنسول ویندوزی است و به خوبی کار می‌کند؛ اما در برنامه‌های وب پس از چند روز سعی و خطا مشخص شد که:
الف) WebCore.Shutdown فقط باید در پایان کار یک برنامه فراخوانی شود. یعنی اصلا نیازی نیست تا در برنامه‌های وب فراخوانی شود.
 System.InvalidOperationException: You are attempting to re-initialize the WebCore.
The WebCore must only be initialized once per process and must be shut down only when the process exits.
ب) Awesomium فقط در یک ترد کار می‌کند. به این معنا که اگر کدهای فوق را در یک صفحه‌ی وب فراخوانی کنید، بار اول کار خواهد کرد. بار دوم برنامه کرش می‌کند؛ با این پیغام خطا:
 System.AccessViolationException: Attempted to read or write protected memory.
This is often an indication that other memory is corrupt. at Awesomium.Core.NativeMethods.WebCore_CreateWebView_1(HandleRef jarg1, Int32 jarg2, Int32 jarg3, HandleRef jarg4)
چون هر صفحه‌ی وب در یک ترد مجزا اجرا می‌شود، عملا استفاده‌ی مستقیم از Awesomium در آن ممکن نیست.
خطای فوق هم از آن نوع خطاهایی است که پروسه‌ی IIS را درجا خاموش می‌کند.


استفاده از Awesomium در یک ترد پس زمینه

راه حلی که نهایتا پاسخ داد و به خوبی و پایدار کار می‌کند، شامل ایجاد یک ترد مجزای Awesomium در زمان آغاز برنامه‌ی وب و زنده نگه داشتن آن تا زمان پایان کار برنامه است.
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Threading;
using System.Web;
using Awesomium.Core;

namespace AwesomiumWebModule
{
    public class AwesomiumModule : IHttpModule
    {
        private static readonly Thread WorkerThread = new Thread(awesomiumWorker);
        private static readonly ConcurrentQueue<AwesomiumRequest> TaskQueue = new ConcurrentQueue<AwesomiumRequest>();
        private static bool _isRunning = true;

        static AwesomiumModule()
        {
            WorkerThread.Start();
        }       

        private static void awesomiumWorker()
        {
            while (_isRunning)
            {
                if (TaskQueue.Count != 0)
                {
                    AwesomiumRequest outRequest;
                    if (TaskQueue.TryDequeue(out outRequest))
                    {
                        var img = AwesomiumThumbnail.FetchWebPageThumbnail(outRequest);
                        File.WriteAllBytes(outRequest.SavePath, img);
                        Thread.Sleep(500);
                    }
                }
                Thread.Sleep(5);
            }
        }

        public void Dispose()
        {
            _isRunning = false;
            WebCore.Shutdown();
        }

        public void Init(HttpApplication context)
        {
            context.EndRequest += endRequest;
        }

        static void endRequest(object sender, EventArgs e)
        {
            var url = HttpContext.Current.Items[Constants.AwesomiumRequest] as AwesomiumRequest;
            if (url!=null)
            {
                TaskQueue.Enqueue(url);
            }
        }
    }
}
در اینجا اگر در برنامه‌های وب فرم، از طریق HttpContext.Current.Items.Add و یا در برنامه‌های MVC به کمک this.HttpContext.Items.Add یک آیتم جدید، با کلید Constants.AwesomiumRequest و از نوع کلاس AwesomiumRequest دریافت گردد، مقدار آن به یک ConcurrentQueue اضافه خواهد شد. این صف در یک ترد مجزا مدام در حال بررسی است. اگر مقداری به آن اضافه شده‌است، از صف خارج شده و پردازش خواهد شد.
نمونه‌ی استفاده از آن، در سمت یک برنامه‌ی وب نیز به صورت زیر است. ابتدا ماژول تهیه شده باید در برنامه ثبت شود:
<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <httpModules>
      <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/>
    </httpModules>
  </system.web>

  <system.webServer>
    <validation validateIntegratedModeConfiguration="false"/>
    <modules>
      <add name="AwesomiumWebModule" type="AwesomiumWebModule.AwesomiumModule"/>
    </modules>
  </system.webServer>
</configuration>
سپس باید تنها مدیریت  HttpContext.Current.Items در سمت برنامه صورت گیرد:
        protected void btnStart_Click(object sender, EventArgs e)
        {
            var host = new Uri(txtUrl.Text).Host;
            HttpContext.Current.Items.Add(Constants.AwesomiumRequest, new AwesomiumRequest
            {
                Url = txtUrl.Text,
                SavePath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Thumbnails\\" + host + ".jpg"),
                TempDir = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\Temp")
            });
            lblInfo.Text = "Please wait. Your request will be served shortly.";
        }
در اینجا کاربر درخواست خود را در صف قرار می‌دهد. پس از مدتی کار آن در WorkerThread ماژول تهیه شده انجام گردیده و تصویر نهایی تهیه می‌شود.
Url، آدرس وب سایتی است که می‌خواهید تصویر آن تهیه شود. SavePath مسیر کامل فایل jpg نهایی است که قرار است دریافت و ذخیره گردد. TempDir محل ذخیره سازی فایل‌های موقتی Awesomium است. Awesomium یک سری کوکی، تصاویر و فایل‌های هر سایت را به این ترتیب کش کرده و در دفعات بعدی سریعتر عمل می‌کند.

پروژه‌ی کامل آن‌را از اینجا می‌توانید دریافت کنید:
AwesomiumWebApplication_V1.0.zip
 
مطالب
گرفتن خروجی JSON از جداول در SQL Server 2012
در مطلب قبلی با استفاده از دستور For XML خروجی xml تولید کردیم اما با همین دستور می‌توان تا حدودی خروجی Json نیر تولید نمود. البته به صورت native هنور در sql server این امکان وجود ندارد که با رای دادن به این لینک از تیم ماکروسافت بخواهید که این امکان را در نسخه بعدی اضافه کند. 
برای این کار یک جدول موقت ایجاد کرده و چند رکورد در آن درج می‌کنیم:
declare @t table(id int, name nvarchar(max), active bit)
insert @t values (1, 'Group 1', 1), (2, 'Group 2', 0)
 حال با استفاده از همان for xml  و پارامتر type که نوع خروجی xml را خودمان می‌توانیم تعیین نماییم و پارمتر Path این کار را بصورت زیر انجام می‌دهیم:
select '[' + STUFF((
        select 
            ',{"id":' + cast(id as varchar(max))
            + ',"name":"' + name + '"'
            + ',"active":' + cast(active as varchar(max))
            +'}'

        from @t t1
        for xml path(''), type
    ).value('.', 'varchar(max)'), 1, 1, '') + ']'
توجه کنید در این جا از پارامتر path بدون نام استفاده شده است و از تابع STUFF  برای در یک رشته در رشته دیگر استفاده شده است. خروجی در زیر آورده شده است:
[{"id":1,"name":"Group 1","active":1},{"id":2,"name":"Group 2","active":0}]
حالت پیشرفته‌تر آن است که بتوانیم یک join را بصورت فرزندان آن در json نمایش دهیم قطعه کد زیر را مشاهده فرمایید:
declare @group table(id int, name nvarchar(max), active bit)
insert @group values (1, 'Group 1', 1), (2, 'Group 2', 0)

declare @member table(id int, groupid int,name nvarchar(max))
insert @member values (1, 1,'Ali'), (2, 1,'Mojtaba'),(3,2,'Hamid')

select '[' + STUFF((
        select 
            ',{"id":' + cast(g.id as varchar(max))
            + ',"name":"' + g.name + '"'
+ ',"members": { "children": [' + 
(select + STUFF((
select 
 ',{"id":' + cast(m.id as varchar(max))
+ ',"name":"' + m.name + '"}'
from @member m
where m.groupid = g.id
for xml path(''), type
 ).value('.', 'varchar(max)'), 1, 1, '')

 + ']}'
            + ',"active":' + cast(g.active as varchar(max))
            +'}')

        from @group g 
        for xml path(''), type
    ).value('.', 'varchar(max)'), 1, 1, '') + ']'
خروجی json بصورت زیر است: 
[{"id":1,"name":"Group 1","members": 
     { "children": [{"id":1,"name":"Ali"},{"id":2,"name":"Mojtaba"}]}
,"active":1},
{"id":2,"name":"Group 2","members": 
      { "children": [{"id":3,"name":"Hamid"}]}
,"active":0}]
حالت‌های خاص و پیشرفته‌تر را با امکانات t-sql خودتان می‌توانید به همین شکل تولید نمایید.
مطالب
کار با اسناد در RavenDb 4، بازیابی اسناد
در قسمت قبل عملیات ثبت و ویرایش اسناد را بررسی کردیم. همچنین نحوه‌ی کار متد LoadAsync (و یا Load) را دیدیم. برای بازیابی یک سند، به همرا اسناد مرتبط با آن، از Load به همراه متد Include استفاده می‌کنیم.
در این مثال میخواهیم آدرس شخص مورد نظر در برنامه با کد 59 بازیابی شود.
var user = _documentSession
    .Include<User>(x => x.Apps[59].AddressId)
    .Load("Users/131-A");
var address = _documentSession.Load<Address>(user.Apps[59].AddressId)

و در صورتیکه بخواهیم تمام آدرس‌های او در تمام برنامه‌های ثبت شده را داشته باشیم، به کد زیر می‌رسیم:
var user = _documentSession
    .Include<User>(x => x.Apps.Values.Select(app => app.AddressId))
    .Load("Users/131-A");
var addresses = List<Address>();
foreach(app in user.Apps)
{
    addresses.Add(_documentSession.Load<Address>(app.AddressId)); //query‌سمت کلاینت انجام اجرا می‌شود
}

 متد Load بسیار سریع کل سند ما را بازیابی میکند اما:
  • حتما باید Id سند(ها) را داشته باشیم.
  • کل سند را بازیابی میکند.
برای رفع این دو مشکل میتوانیم از امکانات Query نویسی در RavenDb استفاده کنیم. به دلیل ذخیره سازی (ظاهرا) فله‌ای اطلاعات در NoSqlها، Query گرفتن از حجم بسیار زیاد این اطلاعات، کار زمان بری است و اجرای Query بدون Index گذاری، کار بیهوده‌ای می‌شود. به همین دلیل با هر Query که اجرا می‌شود، به صورت خودکار یک Index برای آن توسط RavenDb ایجاد شده و Query بر روی Index ایجاد شده، اجرا می‌شود. عملیات Index کردن اطلاعات بصورت اتوماتیک در اولین بار اجرای Query با توجه به حجم داده‌ها می‌تواند بسیار کند باشد. همچنین ما کنترلی بر روی مدیریت ایندکس‌های ایجاد شده نداریم.
Queryها در RavenDb به چند صورت نوشته می‌شوند:

Query
متد Query برای ایجاد Query با استفاده از Linq کاربرد دارد. به مثال زیر توجه کنید:
List<User> users = await _documentSession
    .Query<Users>()
    .Where(u => u.PhoneNumber.StartsWith("915"))
    .ToListAsync();
اجرای Query بالا ابتدا باعث ایجاد یک Index بر روی ویژگی PhoneNumber می‌شود و سپس لیست کاربران را بر می‌گرداند.
برای بازیابی اطلاعات کاربران یک برنامه میتوانیم از Dictionary خود Query بگیریم:
var users = await _documentSession.Query<AppUser>()
    .Where(u => u.Id.Equals("915"))
    .Select(u => new
    {
        u.Apps[appCode].FirstName,
        u.Apps [appCode].LastName,
    })
    .ToListAsync();
این Query در RQL که زبان پرس و جوی مخصوص RavenDb است، چیزی شبیه کد زیر می‌شود:
from Users as user
where startsWith(user.PhoneNumber, "915")
select  {
    FirstName : user.Apps ["59"].FirstName,
    LastName : user.Apps ["59"].LastName
}
مشکلی که در این Query وجود دارد این‌است که کاربرانی که شماره تماس آن‌ها با 915 شروع شده است ولی در برنامه‌ای با کد 59 ثبت نشده‌اند هم در Query بازگشت داده می‌شوند و مقادیر بازگشتی برای فیلدها هم null خواهد بود. اگر بجای ذکر صریح عبارت u. Apps [appCode].FirstName به صورت زیر عمل کنیم:
from u in _documentSession.Query<User>()
                where u.PhoneNumber.StartsWith("915")
                let app = u.Apps["59"]
                select new
                {
                    app.FirstName,
                    app.LastName,
                };
عبارت let app = u.Apps["59"] در RQL تبدیل به یک متد جاوااسکریپتی می‌شود و به کدی شبیه به کد زیر می‌رسیم:
declare function output(u) {
var app = u.Apps["59"];
return { FirstName : app.FirstName, LastName : app.LastName};
}
from Users as user
where startsWith(user.PhoneNumber, "915")
select output(user)
حالا میتوانیم Key مورد نظر در دیکشنری را هم در Query به شکل زیر دخیل کنیم:
app.FirstName,
app.LastName,
*key = u.ActiveInApps.Select(a => a.Key)
و در ادامه با استفاده از متد Search، این فیلد را که به کلید دیکشنری اشاره می‌کند، محدود کرده و بعد از آن Query خود را اجرا میکنیم:
query = query.Search(u => u.key, "59");
در صورتیکه بجای دیکشنری از آرایه استفاده کرده باشیم هم کدهای ما به همین صورت می‌باشد با کمی تغییرات مربوط به تفاوت List و Dictionary!
اما هنوز Query ما بدرستی کار نمیکند چرا که ویژگی Key در RavenDb ایندکس نشده‌است و نمیتواند این ایندکس را هم تشخیص دهد. دلیل آن هم این است که تنها ویژگی‌هایی که در مرتب سازی (Sort) و یا فیلتر مورد استفاده قرار گیرند، به ایندکس‌ها اضافه می‌شوند. برای حل این مشکل باید بصورت دستی Index خود را در RavenDb بسازیم. این کار با ارث بری از کلاس پایه‌ی AbstractIndexCreationTask شروع می‌شود و مدلی را که میخواهیم Index بر روی آن اعمال شود نیز ذکر میکنیم و بعد از آن در سازنده‌ی کلاس، Index خود را می‌سازیم:
public class User_MyIndex : AbstractIndexCreationTask<User>
{
    Map = users => 
                           from u in users
                           from app in u.Apps
                           select new
                           {
                                 Id = u.Id,
                                 PhoneNumber = u.PhoneNumber,
                                 UserName = app.Value.UserName,
                                 FirstName = app.Value.FirstName,
                                 LastName = app.Value.LastName,
                                 IsActive = app.Value.IsActive,
                                 key = app.Key
     };
}
در این ایندکس به ازای هر کاربر، تمام برنامه‌هایی که ثبت شده، بررسی شده و ایندکس می‌شوند. نکته‌ای که باید به آن توجه کنید این است که ویژگی‌های ذکر شده فقط به RavenDb نحوه‌ی بازیابی فیلدهای سند را برای Index گذاری می‌گوید و همچنان خروجی این Index از نوع User بوده و تمام سند را بازگشت میدهد و باید از متد Select در صورت نیاز استفاده کنیم. برای اعمال این ایندکس به سمت سرور از متد:
new User_MyIndex().Execute(store);
و برای ارسال چندین Index به سمت سرور از متد:
IndexCreation.CreateIndexes(typeof(User_MyIndex).Assembly, store);
استفاده می‌کنیم. اکنون اگر به Query خود این ایندکس را معرفی کنیم، خروجی ما به‌درستی فقط کاربران برنامه مورد نظر را بر می‌گرداند:
from u in _documentSession.Query<User, User_MyIndex>() ...
کلاس AbstractIndexCreationTask متدهای زیادی برای کنترل دقیق Indexها در اختیار ما قرار میدهد که پرکاربردترین آن‌ها میتوانند متدهای زیر باشند: 
Index : نحوه‌ی Index کردن هر یک از پراپرتی‌ها را مشخص می‌کند.
Store : برای مواقعی کاربرد دارد که شما می‌خواهید مقدار Index شده را برای دسترسی سریع‌تر همرا با Index ذخیره کنید.
LoadDocument: این متد Id یا لیستی از Idها را به عنوان ورودی گرفته و سند مورد نظر را بازیابی می‌کند. زمانیکه میخواهیم اسناد مرتبط را همراه با سند، Index کنیم کاربرد دارد. برای مثال وقتی میخواهیم Addressهای کاربر را که در سندی جداگانه قرار دارند، به همراه اطلاعات او در Index شرکت دهیم:
select new
{
      ...
      key = aia.Key,
      Address = LoadDocument<Address>(aia.Value.AddressId),
      // City = LoadDocument<Address>(aia.Value.AddressId).City,
};
و برای Indexکردن لیستی از اسناد مرتبط به صورت زیر از LoadDocument استفاده میکنیم:
Message = app.Messages.Select(m => LoadDocument<Message>(m).Content)
* زمانی که میخواهید کلید یک Dictionary را Index کنید و میخواهید نام فیلد آن را key قرار دهید باید از k کوچک استفاده کنید؛ چرا که Key، جزء کلمات رزرو شده‌ی RavenDb می‌باشد.

DocumentQuery
دسترسی بیشتری را بر روی Query ارسالی به سمت سرور به ما می‌دهد؛ اما  strongly typed  نیست. برای مثال Query بالا را به این صورت میتوانیم با DocumentQuery پیاده کنیم:
var users = _documentSession.Advanced.AsyncDocumentQuery<User, User_MyIndex>()
      .WhereStartsWith(nameof(AppUser.PhoneNumber), "915")
      .WhereEquals("key", appCode, exact: true)
      .SelectFields<AppUserModel>(new[] { $"Apps[{appCode}].FirstName", $"Apps[{appCode}].LastName" })
      .ToListAsync();
متدهای DocumentQuery بسیار متنوع هستند و میتوانید لیست آن‌ها را در اینجا مشاهده کنید.

MoreLikeThis (اسناد شبیه)
از رایج‌ترین کارهایی که در وب سایت‌های مطرح دیده می‌شود نمایش مطالب مرتبط با مطلب جاری می‌باشد و از آنجایی که RavenDb از Lucene.NET برای ایندکس کردن اسناد استفاده می‌کند، میتواند براحتی از MoreLikeThis موجود در پروژه‌ی Contrib آن استفاده نماید.
مدل زیر را در نظر بگیرید:
public class Post
    {
        public int Id { get; set; }
        public string Content { get; set; }
        public string Title { get; set; }

        public List<string> Tags { get; set; }
        public string WriterName { get; set; }
        public string WriterId { get; set; }
    }
برای استفاده از MoreLikeThis باید ابتدا محتویات مطلب خود را با استفاده از StandardAnalyzer ایندکس گذاری کنیم. همانطور که گفته شد، برای Index کردن یک سند از کد زیر میتوانیم استفاده کنیم. با این تفاوت که نحوه‌ی آنالیز سند را نیز مشخص میکنیم:
public class Post_ByContent : AbstractIndexCreationTask<Post>
{
    public Post_ByContent()
    {
        Map = posts=> from post in posts
                      select new
                      {
                          post.Content
                      };

        Analyzers.Add(p => p.Content, "StandardAnalyzer");
    }
}
از این ایندکس در Query به همراه متد MoreLikeThis استفاده میکنیم:
List<Post> posts = _documentSession
    .Query<Post, Post_ByContent>()
    .MoreLikeThis(builder => builder
        .UsingDocument(p => p.Id == "posts/59-A")
        .WithOptions(new MoreLikeThisOptions
        {
            Fields = new[] { nameof(Post.Content) },
            StopWordsDocumentId = "appConfig/StopWords"
        }))
    .ToList();
ابتدا سندی را که میخواهیم اسناد شبیه به آن بازیابی شود، معرفی میکنیم. به اینصورت بررسی بر روی تمام فیلدهای Indexگذاری شده اعمال می‌شود. اگر بخواهیم تنظیماتی را به متد اضافه کنیم از MoreLikeThisOptions استفاده میکنیم. حداقل تنظیمات میتواند معرفی نام فیلد مورد نظر برای کاهش بار سرور و همچنین معرفی سندی که StopWordهای ما در آن قرار دارد، باشد. می‌توانید در مورد StopWordها و کاربرد آن در Lucene از این مقاله استفاده کنید. 
نظرات مطالب
نمایش در حال بارگذاری بودن صفحه در برنامه‌های Angular
یک نکته‌ی تکمیلی
چون interceptor تعریف شده سراسری است، ممکن است در صفحه چندین درخواست HTTP وجود داشته باشند که از این interceptor رد می‌شوند. به همین جهت خاتمه کار loading bar باید زمانی باشد که تمام این درخواست‌ها خاتمه یافته‌اند. نگارش نهایی LoaderInterceptorService را با درنظر گرفتن این شرط در اینجا می‌توانید مشاهده کنید.
مطالب
پیاده سازی سیاست‌های دسترسی پویای سمت سرور و کلاینت در برنامه‌های Blazor WASM
فرض کنید در حال توسعه‌ی یک برنامه‌ی Blazor WASM هاست شده هستید و می‌خواهید که نیازی نباشد تا به ازای هر صفحه‌ای که به برنامه اضافه می‌کنید، یکبار منوی آن‌را به روز رسانی کنید و نمایش منو به صورت خودکار توسط برنامه صورت گیرد. همچنین در این حالت نیاز است در قسمت مدیریتی برنامه، بتوان به صورت پویا، به ازای هر کاربری، مشخص کرد که به کدامیک از صفحات برنامه دسترسی دارد و یا خیر و به علاوه اگر به صفحاتی دسترسی ندارد، مشخصات این صفحه، در منوی پویا برنامه ظاهر نشود و همچنین با تایپ آدرس آن در نوار آدرس مرورگر نیز قابل دسترسی نباشد. امن سازی پویای سمت کلاینت، یک قسمت از پروژه‌است؛ قسمت دیگر چنین پروژه‌ای، لیست کردن اکشن متدهای API سمت سرور پروژه و انتساب دسترسی‌های پویایی به این اکشن متدها، به کاربران مختلف برنامه‌است.


دریافت کدهای کامل این پروژه

کدهای کامل پروژه‌ای که نیازمندی‌های فوق را پیاده سازی می‌کند، در اینجا می‌توانید مشاهده و دریافت کنید. در این مطلب از قرار دادن مستقیم این کدها صرفنظر شده و سعی خواهد شد بجای آن، نقشه‌ی ذهنی درک کدهای آن توضیح داده شود.


پیشنیازها

در پروژه‌ی فوق برای شروع به کار، از اطلاعات مطرح شده‌ی در سلسله مطالب زیر استفاده شده‌است:

- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- پیاده سازی اعتبارسنجی کاربران در برنامه‌های Blazor WASM؛ قسمت‌های 31 تا 33 .
- «غنی سازی کامپایلر C# 9.0 با افزونه‌ها»
- «مدیریت مرکزی شماره نگارش‌های بسته‌های NuGet در پروژه‌های NET Core.»
- «کاهش تعداد بار تعریف using‌ها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»


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

صفحات امن سازی شده‌ی سمت کلاینت، با ویژگی Authorize مشخص می‌شوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا می‌توان یک نمونه‌ی سفارشی سازی شده‌ی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارث‌بری کرده و دقیقا مانند آن عمل می‌کند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمت‌های بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافه‌تری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شده‌ی در منوی اصلی برنامه‌است و Title همان عنوان صفحه که در این منو نمایش داده می‌شود. اگر صفحه‌ی محافظت شده‌ای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شده‌اند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.


نمونه‌ای از استفاده‌ی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 می‌توانید مشاهده کنید که خلاصه‌ی آن به صورت زیر است:
 @attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]

ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.


نیاز به لیست کردن صفحات علامتگذاری شده‌ی با ویژگی ProtectedPage

پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آن‌ها است. اینکار توسط سرویس ProtectedPagesProvider صورت می‌گیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننت‌های تعریف شده‌ی در برنامه را از اسمبلی جاری استخراج می‌کند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آن‌هایی که دارای RouteAttribute هستند، پردازش می‌شوند؛ یعنی کامپوننت‌هایی که به همراه مسیریابی هستند. پس از آن بررسی می‌شود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.


نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شده‌ی با ویژگی ProtectedPage

اکنون که لیست صفحات امن سازی شده‌ی توسط ویژگی ProtectedPage را در اختیار داریم، می‌توانیم آن‌ها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش می‌دهد.


تهیه‌ی جداول و سرویس‌های ثبت دسترسی‌های پویای سمت کلاینت


جداول و فیلدهای مورد استفاده‌ی در این پروژه را در تصویر فوق ملاحظه می‌کنید که در پوشه‌ی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقش‌های کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، می‌توان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:


در اینجا نقش‌ها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستم‌های اعتبارسنجی آن بر همین اساس کار می‌کنند، قابل مشاهده‌است. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شده‌ی سمت کلاینت و ::DynamicServerPermission::  جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شده‌ی سمت سرور نیز تعریف شده‌اند. رابطه‌ای این اطلاعات با جدول کاربران، many-to-many است.


به این ترتیب است که مشخص می‌شود کدام کاربر، به چه claimهایی دسترسی دارد.

برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شده‌اند. UserTokens اطلاعات توکن‌های صادر شده‌ی توسط برنامه را ذخیره می‌کند و توسط آن می‌توان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکن‌های او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شده‌است.
اطلاعات این سرویس‌ها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامه‌ی کلاینت قرار می‌گیرند.


نیاز به قسمت مدیریتی ثبت دسترسی‌های پویای سمت کلاینت و سرور

قبل از اینکه بتوان قسمت‌های مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفاده‌ی از لیست ProtectedPageها نیز تهیه کرد:


در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آن‌را از کنترلر UsersAccountManagerController دریافت می‌کند، انجام می‌دهد. در مقابل نام هر کاربر، دو دکمه‌ی ثبت اطلاعات پویای دسترسی‌های سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت می‌شود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شده‌ی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه می‌دهد. اگر در اینجا صفحه‌ای انتخاب شد، اطلاعات آن به سمت سرور ارسال می‌شود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.


شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش می‌دهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری می‌شود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آن‌ها را در مسیر src\Server\Controllers\Tests می‌توانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission::  به کاربر انتخابی انتساب داده می‌شود.



بازگشت اطلاعات پویای دسترسی‌های سمت کلاینت از API

تا اینجا کامپوننت‌های امن سازی شده‌ی سمت کلاینت و اکشن متدهای امن سازی شده‌ی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویس‌های سمت سرور، اطلاعات آن‌ها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفاده‌ی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال می‌شوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقش‌های او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشده‌است، برنامه می‌تواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال می‌شود. این اطلاعات به توکن دسترسی اضافه نشده‌اند تا بی‌جهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده می‌کند.


اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شده‌اند. می‌شد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شده‌ی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آن‌را مشکل می‌کند.
این سه توکن توسط سرویس BearerTokensStore، در برنامه‌ی سمت کلاینت ذخیره و بازیابی می‌شوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواست‌های ارسالی توسط برنامه الصاق خواهد شد.


مدیریت خودکار اجرای Refresh Token در برنامه‌های Blazor WASM

دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحله‌ی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.

- برای مدیریت حالت الف، یک Policy ویژه‌ی Polly طراحی شده‌است که آن‌را در کلاس ClientRefreshTokenRetryPolicy مشاهده می‌کنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شده‌است. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتن‌های تکراری، در هر جائیکه از سرویس‌های HttpClient استفاده می‌شود، می‌باشد.

- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده می‌کنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمه‌ی F5) و در کلاس آغازین ClientAuthenticationStateProvider می‌باشد.



نیاز به پیاده سازی Security Trimming سمت کلاینت

از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج می‌شود. بنابراین مرحله‌ی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شده‌ی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکن‌های دریافتی پس از لاگین را انجام می‌دهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده می‌کنیم تا توسط آن بتوان زمانیکه قرار است آیتم‌های منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژه‌ی SecurityTrim.razor، با استفاده از نقش‌ها و claims یک کاربر، می‌تواند تعیین کند که آیا قسمت محصور شده‌ی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده می‌کنند، استفاده می‌کند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor می‌توانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده می‌شود.
نحوه‌ی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت می‌دهد. این لیست پس از لاگین موفقیت آمیز دریافت شده‌است. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقه‌ای از سرویس ClientSecurityTrimmingService عبور می‌دهیم. یعنی مسیر صفحه و همچنین دسترسی‌های پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتم‌های منوی پویای برنامه، نمایش داده نمی‌شود.


نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آن‌ها دسترسی ندارد

با استفاده از ClientSecurityTrimmingService، در حلقه‌ای که آیتم‌های منوی سایت را نمایش می‌دهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرس‌ها را به صورت مستقیم در مرورگر وارد کند، به آن‌ها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده می‌کنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیش‌فرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحه‌ای اعمال شد، از این سیاست دسترسی استفاده می‌کند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده می‌کند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال می‌کنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟


نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آن‌ها دسترسی ندارد

شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission::  است که در صفحه‌ی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده می‌کند نیز DynamicServerPermissionsAuthorizationHandler می‌باشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شده‌است. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شده‌ی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» است؛ اما بدون استفاده‌ی از ASP.NET Core Identity.


روش اجرای برنامه

چون این برنامه از نوع Blazor WASM هاست شده‌است، نیاز است تا برنامه‌ی Server آن‌را در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه می‌شود. لیست کاربران پیش‌فرض آن‌را در اینجا می‌توانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آن‌ها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.