مطالب دوره‌ها
استفاده از XQuery - قسمت دوم
در ادامه‌ی مباحث XQuery، سایر قابلیت‌های توکار SQL Server را برای کار با اسناد XML بررسی خواهیم کرد.

کوئری گرفتن از اسناد XML دارای فضای نام، توسط XQuery

در مثال زیر، تمام المان‌های سند XML، در فضای نام http://www.people.com تعریف شده‌اند.
DECLARE @doc XML 
SET @doc ='
<p:people xmlns:p="http://www.people.com">
 <p:person name="Vahid" /> 
 <p:person name="Farid" />
</p:people>
'
SELECT @doc.query('/people/person')
اگر کوئری فوق را برای یافتن اشخاص اجرا کنیم، خروجی آن خالی خواهد بود (و یا یک empty sequence)؛ زیرا کوئری نوشته شده به دنبال اشخاصی است که در فضای نام خاصی تعریف نشده‌اند.
سعی دوم احتمالا روش ذیل خواهد بود
 SELECT @doc.query('/p:people/p:person')
که به خطای زیر منتهی می‌شود:
 XQuery [query()]: The name "p" does not denote a namespace.
برای حل این مشکل باید از مفهومی به نام prolog استفاده کرد. هر XQuery از دو قسمت prolog و body تشکیل می‌شود. قسمت prolog می‌تواند شامل تعاریف فضاهای نام، متغیرها، متدها و غیره باشد و قسمت body، همان کوئری تهیه شده‌است. البته SQL Server از قسمت prolog استاندارد XQuery، فقط تعریف فضاهای نام آن‌را مطابق مثال ذیل پشتیبانی می‌کند:
 SELECT @doc.query('
declare default element namespace "http://www.people.com";
/people/person
')
یک سند XML ممکن است با بیش از یک فضای نام تعریف شود. در این حالت خواهیم داشت:
 SELECT @doc.query('
declare namespace aa="http://www.people.com";
/aa:people/aa:person
')
در اینجا در قسمت prolog، برای فضای نام تعریف شده در سند XML، یک پیشوند را تعریف کرده و سپس، استفاده از آن مجاز خواهد بود.
روش دیگر تعریف فضای نام، استفاده از WITH XMLNAMESPACES، پیش از تعریف کوئری است:
 WITH XMLNAMESPACES(DEFAULT 'http://www.people.com')
SELECT @doc.query('/people/person')
البته باید دقت داشت، زمانیکه WITH XMLNAMESPACES تعریف می‌شود، عبارت T-SQL پیش از آن باید با یک سمی‌کالن خاتمه یابد؛ و گرنه یک خطای دستوری خواهید گرفت.
در اینجا نیز امکان کار با چندین فضای نام وجود دارد و برای این منظور تنها کافی است از تعریف Alias استفاده شود. فضاهای نام بعدی با یک کاما از هم مجزا خواهند شد.
 WITH XMLNAMESPACES('http://www.people.com' AS aa)
SELECT @doc.query('/aa:people/aa:person')


عبارات XPath و FLOWR

XQuery از دو نوع عبارت XPath و FLOWR می‌تواند استفاده کند. XQuery همیشه از XPath برای انتخاب داده‌ها و نودها استفاده می‌کند. در اینجا هر نوع XPath سازگار با استاندارد 2 آن، یک XQuery نیز خواهد بود. برای انجام اعمالی بجز انتخاب داده‌ها، باید از عبارات FLOWR استفاده کرد؛ برای مثال برای ایجاد حلقه، مرتب سازی و یا ایجاد نودهای جدید.
در مثال زیر که data آن در قسمت قبل تعریف شد، دو کوئری نوشته شده یکی هستند:
 SELECT @data.query('
 (: FLOWE :)
 for $p in /people/person
 where $p/age > 30
 return $p
 ')

SELECT @data.query('
(: XPath :)
/people/person[age>30]
')
اولین کوئری به روش FLOWR تهیه شده‌است و دومین کوئری از استاندارد XPath استفاده می‌کند. از دیدگاه SQL Server این دو یکی بوده و حتی Query Plan یکسانی نیز دارند.

 XPath بسیار شبیه به مسیر دهی‌های یونیکسی است. بسیار فشرده بوده و همچنین مناسب است برای کار با ساختارهای تو در تو و سلسله مراتبی. مثال زیر را درنظر بگیرید:
 /books/book[1]/title/chapter
در اینجا books، المان ریشه است. سپس به اولین کتاب این ریشه اشاره می‌شود. سپس به المان عنوان و مسیر نهایی، به فصل ختم می‌شود. البته همانطور که در قسمت‌های پیشین نیز ذکر شد، حالت content، پیش فرض بوده و یک فیلد XML می‌تواند دارای چندین ریشه باشد.

در XPath توسط قابلیتی به نام محور می‌توان به المان‌های قبلی یا بعدی دسترسی پیدا کرد. این محورهای پشتیبانی شده در SQL Server عبارتند از self (خود نود)، child (فرزند نود)، parent (والد نود)، decedent (فرزند فرزند فرزند ...)و attribute (دسترسی به ویژگی‌ها). محورهای استانداردی مانند preceding-sibling و following-sibling در SQL Server با عملگرهایی مانند >> و << پشتیبانی می‌شوند.



مثال‌هایی از نحوه‌ی استفاده از محورهای XPath

اینبار قصد داریم یک سند XML نسبتا پیچیده را بررسی کرده و اجزای مختلف آن‌را به کمک XPath بدست بیاوریم.
DECLARE @doc XML 
SET @doc='
<Team name="Project 1" xmlns:a="urn:annotations">
  <Employee id="544" years="6.5">
    <Name>User 1</Name>
<Title>Architect</Title>
<Expertise>Games</Expertise>
<Expertise>Puzzles</Expertise>
<Employee id="101" years="7.1" a:assigned-to="C1">
 <Name>User 2</Name>
 <Title>Dev lead</Title>
 <Expertise>Video Games</Expertise>
 <Employee id="50" years="2.3" a:assigned-to="C2">
 <Name>User 3</Name>
 <Title>Developer</Title>
 <Expertise>Hardware</Expertise>
 <Expertise>Entertainment</Expertise>
</Employee>
</Employee> 
  </Employee>
</Team>
'
در این سند، کارمند و کارمندانی را که باید به یک کارمند گزارش دهند، ملاحظه می‌کنید.
در XPath، محور پیش فرض، child است (اگر مانند کوئری زیر مورد خاصی ذکر نشود):
 SELECT @doc.query('/Team/Employee/Name')
و اگر بخواهیم این محور را به صورت صریح ذکر کنیم، به نحو ذیل خواهد بود:
 SELECT @doc.query('/Team/Employee/child::Name')
خروجی آن User1 است.
 <Name>User 1</Name>
برای ذکر محور decedent-or-self می‌توان از // نیز استفاده کرد:
 SELECT @doc.query('//Employee/Name')
با خروجی
 <Name>User 1</Name>
<Name>User 2</Name>
<Name>User 3</Name>
در این حالت به تمام نودهای سند، در سطوح مختلف آن مراجعه شده و به دنبال نام کارمند خواهیم گشت.

برای کار با ویژگی‌ها و attributes از [] به همراه علامت @ استفاده می‌شود:
 SELECT @doc.query('
declare namespace a = "urn:annotations";
//Employee[@a:assigned-to]/Name
')
در این کوئری، تمام کارمندانی که دارای ویژگی assigned-to واقع در فضای نام urn:annotations هستند، یافت خواهند شد. با خروجی:
 <Name>User 2</Name>
<Name>User 3</Name>
معادل طولانی‌تر آن ذکر کامل محور attribute است بجای @
 SELECT @doc.query('
declare namespace a = "urn:annotations";
//Employee[attribute::a:assigned-to]/Name
')
و برای یافتن کارمندانی که دارای ویژگی assigned-to نیستند، می‌توان از عملگر not استفاده کرد:
 SELECT @doc.query('
declare namespace a = "urn:annotations";
//Employee[not(@a:assigned-to)]/Name
')
با خروجی
 <Name>User 1</Name>
و اگر بخواهیم تعداد کارمندانی را که به user 1 مستقیما گزارش می‌دهند را بیابیم، می‌توان از count به نحو ذیل استفاده کرد:
 SELECT @doc.query('count(//Employee[Name="User 1"]/Employee)')

در XPath برای یافتن والد از .. استفاده می‌شود:
 SELECT @doc.query('//Employee[../Name="User 1"]')
برای مثال در کوئری فوق، کارمندانی که والد آن‌ها user 1 هستند، یافت می‌شوند.
استفاده از .. در SQL Server به دلایل کارآیی پایین توصیه نمی‌شود. بهتر است از همان روش قبلی کوئری تعداد کارمندانی که به user 1 مستقیما گزارش می‌دهند، استفاده شود.



عبارات FLOWR

FLOWR هسته‌ی XQuery را تشکیل داده و قابلیت توسعه XPath را دارد. FLOWR مخفف for، let، order by، where و retrun است. از for برای تشکیل حلقه، از let برای انتساب، از where و order by برای فیلتر و مرتب سازی اطلاعات و از return برای بازگشت نتایج کمک گرفته می‌شود. FLOWR بسیار شبیه به ساختار SQL عمل می‌کند.
معادل عبارت SQL
 Select p.name, p.job
from people as p
where p.age > 30
order by p.age
با عبارات FLOWR، به صورت زیر است:
 for $p in /people/person
where $p.age > 30
order by $p.age[1]
return ($p/name, $p/job)
همانطور که مشاهده می‌کنید علت انتخاب FLOWR در اینجا عمدی بوده‌است؛ زیرا افرادی که SQL می‌دانند به سادگی می‌توانند شروع به کار با عبارات FLOWR کنند.
تنها تفاوت مهم، در اینجا است که در عبارات SQL، خروجی کار توسط select، در ابتدای کوئری ذکر می‌شود، اما در عبارات FLOWR در انتهای آن‌ها.

از let برای انتساب مجموعه‌ای از نودها استفاده می‌شود:
 let $p := /people/person
return $p
تفاوت آن با for در این است که در هر بار اجرای حلقه‌ی for، تنها با یک نود کار خواهد شد، اما در let با مجموعه‌ای از نودها سر و کار داریم. همچنین let از نگارش 2008 اس کیوال سرور به بعد قابل استفاده‌است.

یک نکته
اگر به order by  دقت کنید، به اولین سن اشاره می‌کند. Order by در اینجا با تک مقدارها کار می‌کند و امکان کار با مجموعه‌ای از نودها را ندارد. به همین جهت باید طوری آن‌را تنظیم کرد که هربار فقط به یک مقدار اشاره کند.
هر زمانیکه به خطای requires a singleton برخوردید، یعنی دستورات مورد استفاده با یک سری از نودها کار نکرده و نیاز است دقیقا مشخص کنید، کدام مقدار مدنظر است.


مثال‌هایی از عبارات FLOWR

دو کوئری ذیل یک خروجی 1 2 3 را تولید می‌کنند
 DECLARE @x XML = '';
SELECT @x.query('
for $i in (1,2,3)
return $i
');

SELECT @x.query('
let $i := (1,2,3)
return $i
');
در کوئری اول، هر بار که حلقه اجرا می‌شود، به یکی از اعضای توالی دسترسی خواهیم داشت. در کوئری دوم، یکبار توالی تعریف شده و کار با آن در یک مرحله صورت می‌گیرد.
در ادامه اگر سعی کنیم به این کوئری‌ها یک order by را اضافه کنیم، کوئری اول با موفقیت اجرا شده،
 DECLARE @x XML = '';
SELECT @x.query('
for $i in (1,2,3)
order by $i descending
return $i
');

SELECT @x.query('
let $i := (1,2,3)
order by $i descending
return $i
');
اما کوئری دوم با خطای ذیل متوقف می‌شود:
 XQuery [query()]: 'order by' requires a singleton (or empty sequence), found operand of type 'xs:integer +'
در خطا عنوان شده‌است که مطابق تعریف، order by با یک مجموعه از نودها، مانند حاصل let کار نمی‌کند و همانند حلقه for نیاز به singleton یا atomic values دارد.


ساخت المان‌های جدید XML توسط عبارات FLOWR

ابتدا همان سند XML قسمت قبل را درنظر بگیرید:
DECLARE @doc XML  =' 
<people>
 <person>
  <name>
<givenName>name1</givenName>
<familyName>lname1</familyName>
  </name>
  <age>33</age>
  <height>short</height>
 </person>
 <person>
  <name>
<givenName>name2</givenName>
<familyName>lname2</familyName>
  </name>
  <age>40</age>
  <height>short</height>
 </person>
 <person>
  <name>
<givenName>name3</givenName>
<familyName>lname3</familyName>
  </name>
  <age>30</age>
  <height>medium</height>
 </person>
</people>
'
در ادامه قصد داریم، المان‌های اشخاص را صرفا بر اساس مقدار givenName آن‌ها بازگشت دهیم:
 SELECT @doc.query('
for $p in /people/person
return <person>
{$p/name[1]/givenName[1]/text()}
</person>
');
در اینجا نحوه‌ی تولید پویای تگ‌های XML را توسط FLOWR مشاهده می‌کنید. عبارات داخل {} به صورت خودکار محاسبه و جایگزین می‌شوند و خروجی آن به شرح زیر است:
 <person>name1</person>
<person>name2</person>
<person>name3</person>

سؤال: اگر به این خروجی بخواهیم یک root element اضافه کنیم، چه باید کرد؟ اگر المان root دلخواهی را در return قرار دهیم، به ازای هر آیتم یافت شده، یکبار تکرار می‌شود که مدنظر ما نیست.
 SELECT @doc.query('
<root>
{
for $p in /people/person
return <person>
{$p/name[1]/givenName[1]/text()}
</person>
}
</root>
');
بله. در این حالت نیز می‌توان از همان روشی که در return استفاده کردیم، برای کل حلقه و return آن استفاده کنیم. المان root به صورت استاتیک محاسبه می‌شود و هر آنچه که داخل {} باشد، به صورت پویا. با این خروجی:
 <root>
  <person>name1</person>
  <person>name2</person>
  <person>name3</person>
</root>


مفهوم quantification در FLOWR

همان سند Team name=Project 1 ابتدای بحث جاری را درنظر بگیرید.
 SELECT @doc.query('some $emp in //Employee satisfies $emp/@years >5')
-- true

SELECT @doc.query('every $emp in //Employee satisfies $emp/@years >5')
-- false
به عبارات some و every در اینجا quantification گفته می‌شود. در کوئری اول، می‌خواهیم بررسی کنیم، آیا در بین کارمندان، بعضی از آن‌ها دارای ویژگی (با @ شروع شده) years بیشتر از 5 هستند. در کوئری دوم، عبارت «بعضی» به «هر» تغییر یافته است. 
مطالب
به روز رسانی قسمت assemblyBinding فایل‌های config توسط NuGet
زمانیکه پروژه‌ی شما وابستگی‌های متعددی داشته باشد، احتمال برخوردن به یک چنین خطایی بسیار محتمل است:
 Could not load file or assembly Newtonsoft.Json or one of its dependencies. The system cannot find the file specified.
کتابخانه‌ی Newtonsoft.Json جزو پروژه‌هایی است که مدام به روز رسانی و نگهداری می‌شود. در این بین ممکن است وابستگی A از نگارش 4.5 آن استفاده کند و وابستگی B بر اساس نگارش 4.7 آن کامپایل شده باشد و وابستگی جدیدی از نگارش 6 آن استفاده کند. در یک چنین حالتی زمانیکه پروژه‌ی شما آخرین نگارش JSON.NET را به همراه داشته باشد، وابستگی‌های A و B پیام یافت نشدن نگارش‌های خاص کتابخانه‌ی Newtonsoft.Json را صادر خواهند کرد و در این حالت، برنامه در همان ابتدای کار کرش می‌کند. برای رفع این مشکل می‌توان به دات نت گفت که تمام درخواست‌های مرتبط با JSON.NET را به اسمبلی خاصی هدایت و پردازش کن. به این قابلیت assembly redirects گفته می‌شود و به نحو ذیل در فایل‌های کانفیگ برنامه (app.config یا web.config) قابل تعریف است:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <!-- ... -->
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-6.0.0.0" newVersion="6.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>
به این ترتیب تمام اسمبلی‌هایی که بر اساس نگارش قدیمی JSON.NET کامپایل شده‌اند، باور خواهند کرد که نگارش 6 آن، همان نگارشی است که باید با آن کار کنند.


به روز رسانی قسمت assembly redirects توسط NuGet

البته اگر بسته‌ی جدیدی را توسط نیوگت نصب کنیم، این assembly redirects به صورت خودکار توسط آن اضافه خواهند شد اما ... گاهی ممکن است موارد قدیمی را به روز نکند یا موردی فراموش شوند یا حتی اگر اطلاعاتی را پیشتر از وب یافته‌اید، دارای خطای تایپی باشند. برای یک چنین مواردی اگر با خطای Could not load file or assembly ابتدای بحث مواجه شدید، مراحل ذیل را طی کنید تا بتوانید قسمت assembly redirects را بازسازی کنید:
الف) به فایل config برنامه مراجعه کرده و کلا قسمت assemblyBinding آن‌را حذف کنید.
ب) سپس دستور ذیل را در کنسول پاورشل نیوگت اجرا کنید:
 PM> Get-Project -All | Add-BindingRedirect
این دستور به تمام پروژه‌های موجود در solution جاری مراجعه کرده و قسمت assemblyBinding آن‌ها را بر اساس آخرین اطلاعات پروژه‌ها و وابستگی‌های آن‌ها به روز می‌کند.
مطالب
قابلیت چند زبانه و Localization در AngularJs- بخش سوم: Best Practiceهای angular-translate
در این بخش قصد دارم تا در قالب یک پروژه، تمامی قابلیت‌هایی را که در angular-translate و ماژول‌های مرتبط با آن وجود دارند، به شما معرفی کنم. پروژه‌ی نمونه را از لینک زیر دریافت نمایید:
AngularJs-Translate-BestPractices.zip 

این پروژه در 12 بخش گوناگون تقسیم بندی شده‌است که هر کدام در قالب یک فایل HTML می‌باشد و تمامی اسکریپت‌های مورد نیاز به آن افزوده شده‌است. هر بخش به صورت مجزا به شرح یک ویژگی کاربردی در angular-translate می‌پردازد.

ex1_basic_usage 

روند کار مثال اول خیلی ساده است. در ابتدا اسکریپت‌های زیر به صفحه اضافه شده‌اند:
    <script src="Scripts/angular.js"></script>
    <script src="Scripts/angular-translate.js"></script>
در ابتدا وابستگی pascalprecht.translate به ماژول اضافه شده‌است و پس از آن زبان‌های مختلف به صورت JSON در بخش اسکریپت وارد شده‌اند.
        angular.module('app', ['pascalprecht.translate'])
        .config([
        '$translateProvider', function ($translateProvider) {

            // Adding a translation table for the English language
            $translateProvider.translations('en_US', {
                "TITLE": "How to use",
                "HEADER": "You can translate texts by using a filter.",
                "SUBHEADER": "And if you don't like filters, you can use a directive.",
                "HTML_KEYS": "If you don't like an empty elements, you can write a key for the translation as an inner HTML of the directive.",
                "DATA_TO_FILTER": "Your translations might also contain any static ({{staticValue}}) or random ({{randomValue}}) values, which are taken directly from the model.",
                "DATA_TO_DIRECTIVE": "And it's no matter if you use filter or directive: static is still {{staticValue}} and random is still {{randomValue}}.",
                "RAW_TO_FILTER": "In case you want to pass a {{type}} data to the filter, you have only to pass it as a filter parameter.",
                "RAW_TO_DIRECTIVE": "This trick also works for {{type}} with a small mods.",
                "SERVICE": "Of course, you can translate your strings directly in the js code by using a $translate service.",
                "SERVICE_PARAMS": "And you are still able to pass params to the texts. Static = {{staticValue}}, random = {{randomValue}}."
            });

            // Adding a translation table for the Russian language
            $translateProvider.translations('ru_RU', {
                "TITLE": "Как пользоваться",
                "HEADER": "Вы можете переводить тексты при помощи фильтра.",
                "SUBHEADER": "А если Вам не нравятся фильтры, Вы можете воспользоваться директивой.",
                "HTML_KEYS": "Если вам не нравятся пустые элементы, Вы можете записать ключ для перевода в как внутренний HTML директивы.",
                "DATA_TO_FILTER": "Ваши переводы также могут содержать любые статичные ({{staticValue}}) или случайные ({{randomValue}}) значения, которые берутся прямо из модели.",
                "DATA_TO_DIRECTIVE": "И совершенно не важно используете ли Вы фильтр или директиву: статическое значение по прежнему {{staticValue}} и случайное - {{randomValue}}.",
                "RAW_TO_FILTER": "Если вы хотите передать \"сырые\" ({{type}}) данные фильтру, Вам всего лишь нужно передать их фильтру в качестве параметров.",
                "RAW_TO_DIRECTIVE": "Это также работает и для директив ({{type}}) с небольшими модификациями.",
                "SERVICE": "Конечно, Вы можете переводить ваши строки прямо в js коде при помощи сервиса $translate.",
                "SERVICE_PARAMS": "И вы все еще можете передавать параметры в тексты. Статическое значение = {{staticValue}}, случайное = {{randomValue}}."
            });

            // Tell the module what language to use by default
            $translateProvider.preferredLanguage('en_US');

        }])
در تکه کد فوق مشاهده می‌کنید که دو translate table زبان انگلیسی و روسی به صورت JSON وارد شده‌اند. شما قادرید تا چندین زبان را به همین صورت وارد نمایید. در خط آخر نیز زبان پیش فرض سیستم تعریف شده است.
حال به بررسی کد‌های درون کنترلر می‌پردازیم:
.controller('ctrl', ['$scope', '$translate', function ($scope, $translate) {

        $scope.tlData = {
            staticValue: 42,
            randomValue: Math.floor(Math.random() * 1000)
        };

        $scope.jsTrSimple = $translate.instant('SERVICE');
        $scope.jsTrParams = $translate.instant('SERVICE_PARAMS', $scope.tlData);

        $scope.setLang = function (langKey) {
            // You can change the language during runtime
            $translate.use(langKey);

            // A data generated by the script have to be regenerated
            $scope.jsTrSimple = $translate.instant('SERVICE');
            $scope.jsTrParams = $translate.instant('SERVICE_PARAMS', $scope.tlData);
        };

    }]);
می‌بینیم که وابستگی translate$ تزریق شده است. پس از آن دو عدد رندم به پارامتر‌های تعریف شده در translate table ارسال می‌گردد. تغییر زبان نیز توسط متد setLang صورت می‌پذیرد.
در بخش نهایی می‌خواهیم روش‌های گوناگون استفاده از translate tables را درون HTML نمایش دهیم. به بخش HTML همین مثال توجه کنید:
    <p>
        <a href="#" ng-click="setLang('en_US')">English</a>
        |
        <a href="#" ng-click="setLang('ru_RU')">Русский</a>
    </p>
    <!-- Translation by a filter -->
    <h1>{{'HEADER' | translate}}</h1>
    <!-- Translation by a directive -->
    <h2 translate="SUBHEADER">Subheader</h2>
    <!-- Using inner HTML as a key for translation -->
    <p translate>HTML_KEYS</p>
    <hr>
    <!-- Passing a data object to the translation by the filter -->
    <p>{{'DATA_TO_FILTER' | translate: tlData}}</p>
    <!-- Passing a data object to the translation by the directive -->
    <p translate="DATA_TO_DIRECTIVE" translate-values="{{tlData}}"></p>
    <hr>
    <!-- Passing a raw data to the filter -->
    <p>{{'RAW_TO_FILTER' | translate:'{ type: "raw" }' }}</p>
    <!-- Passing a raw data to the filter -->
    <p translate="RAW_TO_DIRECTIVE" translate-values="{ type: 'directives' }"></p>
    <hr>
    <!-- Using a $translate service -->
    <p>{{jsTrSimple}}</p>
    <!-- Passing a data to the $translate service -->
    <p>{{jsTrParams}}</p>
نحوه تعریف هر روش به صورت کامنت پیش از هر تگ نوشته شده است. شما به روش‌های مختلف و بر حسب استانداردهایی که خود از آن پیروی می‌کنید می‌توانید از یکی از روش‌های فوق استفاده نمایید. اما رایج‌ترین روش، دو روش اول یعنی استفاده از دایرکتیو و یا فیلتر است و دلیل آن هم سادگی و خوانا بودن این دو روش می‌باشد.

ex2_remember_language_cookies

در این مثال همانگونه که از اسم آن پیداست قصد داریم تا زبان مورد نظری را که کاربر در سیستم انتخاب نموده است، ذخیره کنیم تا پس از بستن و بازکردن مجدد وب سایت با همان زبان پیشین به کاربر نمایش داده شود. این کار بسیار ساده است. کافیست که در ابتدا علاوه بر اسکریپت‌های مثال قبل، اسکریپت‌های زیر را نیز به صفحه اضافه کنید:
    <script src="Scripts/angular-cookies.js"></script>
    <script src="Scripts/angular-translate-storage-cookie.js"></script>
با اضافه کردن خط زیر درون بدنه config، یک کوکی جدید برای شما ساخته می‌شود. این کوکی NG_TRANSLATE_LANG_KEY نام دارد که هر بار با id زبان کنونی که در translate table وارد نموده‌اید آپدیت می‌شود.
// Tell the module to store the language in the cookie
$translateProvider.useCookieStorage();
حال اگر صفحه را refresh کنید می‌بینید که زبان پیشینی که انتخاب نموده‌اید، مجددا بارگذاری می‌گردد.

ex3_remember_language_local_storage

این مثال همانند مثال قبل رفتار می‌کند، با این تفاوت که به جای اینکه کلید زبان کنونی را درون کوکی ذخیره کند، آن را درون Local Storage با نام NG_TRANSLATE_LANG_KEY قرار می‌دهد. برای اجرا کافیست اسکریپت‌ها و تکه کد زیر را با موارد مثال قبل جایگزین کنید.

<script src="Scripts/angular-translate-storage-local.js"></script>


// Tell the module to store the language in the local storage
$translateProvider.useLocalStorage();

مثال های ex4_set_a_storage_key  و ex5_set_a_storage_prefix نام کلیدی که برای ذخیره سازی زبان کنونی در کوکی یا Local Storage قرار می‌گیرد را تغییر می‌دهد که به دلیل سادگی از شرح آن می‌گذریم. 

ex6_namespace_support 

translate table در angular-translate قابلیت مفید namespacing را نیز داراست. این قابلیت به ما کمک می‌کند که جهت کپسوله کردن بخش‌های مختلف، ترجمه آنها را با namespace‌های خاص خود نمایش دهیم. به مثال زیر توجه کنید:

            $translateProvider.translations('en_US', {
                "TITLE": "How to use namespaces",
                "ns1": {
                    "HEADER": "A translations table supports namespaces.",
                    "SUBHEADER": "So you can to structurize your translation table well."
                },
                "ns2": {
                    "HEADER": "Do you want to have a structured translations table?",
                    "SUBHEADER": "You can to use namespaces now."
                }
            });

همانطور که توجه می‌کنید بخش ns1 خود شامل زیر مجموعه‌هایی است و ns2 نیز به همین صورت. هر کدام دارای کلید HEADER و SUBHEADER می‌باشند. فرض کنید هر کدام از این بخش‌ها می‌خواهند اطلاعات درون یک section را نمایش دهند. حال به نحوه‌ی فراخوانی این translate tableها دقت کنید:

<!-- section 1: Translate Table Called by ns1 namespace -->    
<h1 translate>ns1.HEADER</h1>
<h2 translate>ns1.SUBHEADER</h2>

<!-- section 2: Translate Table Called by ns2 namespace -->
<h1 translate>ns2.HEADER</h1>
<h2 translate>ns2.SUBHEADER</h2>

به همین سادگی می‌توان تمامی بخش‌ها را با namespace‌های مختلف در translate table قرار داد.

در بخش بعدی (پایانی) شش قابلیت دیگر angular translate که شامل فراخوانی translate table از یک فایل JSON، فراخوانی فایل‌های translate table به صورت lazy load و تغییر زبان بخشی از صفحه به صورت پویا هستند، بررسی خواهند شد.

فایل پروژه: AngularJs-Translate-BestPractices.zip  

مطالب
پیدا کردن لیست SQL server های نصب شده در یک شبکه


با آمدن SQL server 2008 استفاده از کتابخانه SQL-DMO برای انجام یک سری از امور بر روی اس کیوال سرور با استفاده از برنامه نویسی منسوخ شد. یکی از توانایی‌های این کتابخانه لیست کردن سرورهای اس کیوال (قابل دسترسی) موجود در شبکه بود.
برای مثال توسط این کتابخانه به صورت زیر می‌توان اینکار را انجام داد:
در قطعه کد زیر فرض بر این است که ارجاعی به کتابخانه sqldmo را در برگه com مربوط به project->add reference اضافه کرده‌اید:

using SQLDMO;
using System.Collections.Generic;

public static List<string> GetSQLServersList2()
{
List<string> result = new List<string>();
ApplicationClass sqlApp = new ApplicationClass();
NameList lst = sqlApp.ListAvailableSQLServers();
for (int i = 1; i <= lst.Count; i++)
result.Add(lst.Item(i));
lst = null;
sqlApp = null;

return result;

}

با منسوخ شدن این کتابخانه COM (که تنها تا اس کیوال سرور 2005 پشتیبانی می‌شود)، در نگارش‌های جدید (و قدیم) اس کیوال سرور، با استفاده از قطعه کد زیر می‌توان لیست تمام SQL server های نصب شده در یک شبکه به همراه instance های آنها را بدست آورد.

using System.Collections.Generic;
using System.Data;
using System.Data.Sql;

public class CListServers
{
public static List<string> GetSQLServersList()
{
List<string> result = new List<string>();

// Retrieve the enumerator instance and then the data.
var instance = SqlDataSourceEnumerator.Instance;
var table = instance.GetDataSources();

// Display the contents of the table.
foreach (DataRow row in table.Rows)
{
result.Add(string.Format("{0}\\{1}", row[0], row[1]));
}

return result;
}
}

راه دیگر:
کتابخانه COM یاد شده (SQL-DMO) در SQL server 2008 با کتابخانه SMO جایگزین شده است.
در این حالت خواهیم داشت:

using System.Collections.Generic;
using System.Data;
using Microsoft.SqlServer.Management.Smo;

public class CListServers
{
public static List<string> GetSQLServersListSMO()
{
List<string> result = new List<string>();
DataTable dt = SmoApplication.EnumAvailableSqlServers(false);
if (dt.Rows.Count > 0)
{
foreach (DataRow dr in dt.Rows)
{
result.Add(dr["Name"].ToString());
}
}
return result;
}
}

تقریبا کلیه اعمالی که از طریق management studio قابل انجام هستند با کمک این کتابخانه نیز از طریق برنامه نویسی می‌توان به آن‌ها پرداخت. برای مثال تهیه اسکریپت کلیه جداول ، تریگرها و غیره.

مطالب
سفارشی سازی عناصر صفحات پویای افزودن و ویرایش رکوردهای jqGrid در ASP.NET MVC
پیشنیاز این بحث مطالعه‌ی مطالب «صفحه بندی و مرتب سازی خودکار اطلاعات به کمک jqGrid در ASP.NET MVC» و «فعال سازی و پردازش صفحات پویای افزودن، ویرایش و حذف رکوردهای jqGrid در ASP.NET MVC» است و در اینجا جهت کوتاه شدن بحث، صرفا به تغییرات مورد نیاز جهت اعمال بر روی مثال‌ها اکتفاء خواهد شد.


صورت مساله

    public class Product
    {
        public int Id { set; get; }
        public DateTime AddDate { set; get; }
        public string Name { set; get; }
        public decimal Price { set; get; }
    }
در اینجا تعریف محصول، شامل خاصیت‌های تاریخ ثبت، نام و قیمت آن است.
می‌خواهیم زمانیکه فرم‌های پویای ویرایش یا افزودن رکوردها ظاهر شدند، در حین تکمیل نام، یک auto complete ظاهر شود:


در حین ورود تاریخ، یک date picker شمسی جهت سهولت ورود اطلاعات نمایش داده شود:


همچنین در قسمت ورود مبلغ و قیمت، به صورت خودکار حرف سه رقم جدا کننده هزارها، نمایش داده شوند تا کاربران در حین ورود مبالغ بالا دچار اشتباه نشوند.



پیشنیازها

- برای نمایش auto complete از همان امکانات توکار jQuery UI که به همراه jqGrid عرضه می‌شوند، استفاده خواهیم کرد.
- برای نمایش date picker شمسی از مطلب «PersianDatePicker یک DatePicker شمسی به زبان JavaScript که از تاریخ سرور استفاده می‌کند» کمک خواهیم گرفت.
- جهت اعمال خودکار حرف سه رقم جدا کننده هزارها از افزونه‌ی Price Format جی‌کوئری استفاده می‌کنیم.

تعریف و الحاق این پیشنیازها، فایل layout برنامه را به شکل زیر تغییر خواهد داد:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>

    <link href="~/Content/themes/base/jquery.ui.all.css" rel="stylesheet" />
    <link href="~/Content/jquery.jqGrid/ui.jqgrid.css" rel="stylesheet" />
    <link href="~/Content/PersianDatePicker.css" rel="stylesheet" />
    <link href="~/Content/Site.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <div>
        @RenderBody()
    </div>

    <script src="~/Scripts/jquery-1.7.2.min.js"></script>
    <script src="~/Scripts/jquery-ui-1.8.11.min.js"></script>
    <script src="~/Scripts/i18n/grid.locale-fa.js"></script>
    <script src="~/Scripts/jquery.jqGrid.min.js"></script>
    <script src="~/Scripts/PersianDatePicker.js"></script>
    <script src="~/Scripts/jquery.price_format.2.0.js"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>


تغییرات مورد نیاز سمت کلاینت، جهت اعمال افزونه‌های جی‌کوئری و سفارشی سازی عناصر دریافت اطلاعات

الف) نمایش auto complete در حین ورود نام محصولات
                colModel: [
                    {
                        name: 'Name', index: 'Name', align: 'right', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 40,
                            dataInit: function (elem) {
                                // http://jqueryui.com/autocomplete/
                                $(elem).autocomplete({
                                    source: '@Url.Action("GetProductNames","Home")',
                                    minLength: 2,
                                    select: function (event, ui) {
                                        $(elem).val(ui.item.value);
                                        $(elem).trigger('change');
                                    }
                                });
                            }
                        },
                        editrules: {
                            required: true
                        }
                    }           
     ],
برای اعمال هر نوع افزونه‌ی جی‌کوئری به عناصر فرم‌های خودکار ورود اطلاعات در jqGrid، تنها کافی است که رویداد dataInit یک ستون را بازنویسی کنیم. در اینجا توسط elem، المان جاری را در اختیار خواهیم داشت. سپس از این المان جهت اعمال افزونه‌ای دلخواه استفاده می‌کنیم. برای مثال در اینجا از متد autocomplete استفاده شده‌است که جزئی از jQuery UI استاندارد است.
برای پردازش سمت سرور آن و مقدار دهی url آن، یک چنین اکشن متدی را می‌توان تدارک دید:
        public ActionResult GetProductNames(string term)
        {
            var list = ProductDataSource.LatestProducts
                .Where(x => x.Name.StartsWith(term))
                .Select(x => x.Name)
                .Take(10)
                .ToArray();
            return Json(list, JsonRequestBehavior.AllowGet);
        }
مقدار term، عبارتی است که کاربر وارد کرده است. توسط متد StartsWith، کلیه نام‌هایی را که با این عبارت شروع می‌شوند (البته 10 مورد از آن‌ها را) بازگشت می‌دهیم.

ب) نمایش date picker شمسی در حین ورود تاریخ
                colModel: [
                    {
                        name: 'AddDate', index: 'AddDate', align: 'center', width: 100,
                        editable: true, edittype: 'text',
                        editoptions: {
                            maxlength: 10,
                            // https://www.dntips.ir/post/1382
                            onclick: "PersianDatePicker.Show(this,'@today');"
                        },
                        editrules: {
                            required: true
                        }
                    }
                ],
Date picker مورد استفاده، وابستگی خاصی به jQuery ندارد. مطابق مستندات آن باید در رویدادگردان onclick، این تقویم شمسی را فعال کرد. بنابراین در قسمت onclick دقیقا این مورد را اعمال می‌کنیم.

 @{
ViewBag.Title = "Index";
var today = DateTime.Now.ToPersianDate();
}
مقدار today آن در ابتدای View به نحو فوق تعریف شده‌است. کدهای کامل متد کمکی ToPersianDate در پروژه‌ی پیوست موجود است.

ج) اعمال حروف سه رقم جدا کننده هزارها در حین ورود قیمت
                colModel: [
                    {
                        name: 'Price', index: 'Price', align: 'center', width: 100,
                        formatter: 'currency',
                        formatoptions:
                        {
                            decimalSeparator: '.',
                            thousandsSeparator: ',',
                            decimalPlaces: 2,
                            prefix: '$'
                        },
                        editable: true, edittype: 'text',
                        editoptions: {
                            dir: 'ltr',
                            dataInit: function (elem) {
                                // http://jquerypriceformat.com/
                                $(elem).priceFormat({
                                    prefix: '',
                                    thousandsSeparator: ',',
                                    clearPrefix: true,
                                    centsSeparator: '',
                                    centsLimit: 0
                                });
                            }
                        },
                        editrules: {
                            required: true,
                            minValue: 0
                        }
                    }
                ],
افزونه‌ی price format نیز یک افزونه‌ی جی‌کوئری است. بنابراین دقیقا مانند حالت auto complete آن‌را در dataInit فعال سازی می‌کنیم و همچنین یک سری تنظیم ابتدایی مانند مشخص سازی  thousandsSeparator آن‌را مقدار دهی خواهیم کرد.


یک نکته

همین تعاریف را دقیقا به فرم‌های جستجو نیز می‌توان اعمال کرد. در اینجا برای حالات ویرایش و افزودن رکوردها، editoptions مقدار دهی شده‌است؛ در مورد فرم‌های جستجو باید searchoptions و برای مثال dataInit آن‌را مقدار دهی کرد.



مشکل مهم!

با تنظیمات فوق، قسمت UI بدون مشکل کار می‌کند. اما اگر در سمت سرور، مقادیر دریافتی را بررسی کنیم، نه تاریخ و نه قیمت، قابل دریافت نیستند. زیرا تاریخ ارسالی به سرور شمسی است و مدل برنامه DateTime میلادی می‌باشد. همچنین به دلیل وجود حروف سه رقم جدا کننده هزارها، عبارت دریافتی قابل تبدیل به عدد نیستند و مقدار دریافتی صفر خواهد بود.
برای رفع این مشکلات، نیاز به تغییر model binder توکار ASP.NET MVC است. برای تاریخ‌ها از کلاس PersianDateModelBinder می‌توان استفاده کرد. برای اعداد decimal از کلاس ذیل:
using System;
using System.Globalization;
using System.Threading;
using System.Web.Mvc;

namespace jqGrid05.CustomModelBinders
{
    /// <summary>
    /// How to register it in the Application_Start method of Global.asax.cs
    /// ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder());
    /// </summary>
    public class DecimalBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType == typeof(decimal?))
            {
                return bindDecimal(bindingContext);
            }
            return base.BindModel(controllerContext, bindingContext);
        }

        private static object bindDecimal(ModelBindingContext bindingContext)
        {
            var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (valueProviderResult == null)
                return null;
            
            bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
            decimal value;
            var valueAsString = valueProviderResult.AttemptedValue == null ?
                                        null : valueProviderResult.AttemptedValue.Trim();
            if (string.IsNullOrEmpty(valueAsString))
                return null;
            
            if (!decimal.TryParse(valueAsString, NumberStyles.Any, Thread.CurrentThread.CurrentCulture, out value))
            {
                const string error ="عدد وارد شده معتبر نیست";
                var ex = new InvalidOperationException(error, new Exception(error, new FormatException(error)));
                bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
                return null;
            }
            return value;
        }
    }
}
در اینجا عبارت ارسالی به سرور به صورت یک رشته دریافت شده و سپس تبدیل به یک عدد decaimal می‌شود. در آخر به سیستم model binding بازگشت داده خواهد شد. به این ترتیب دیگر مشکلی با پردازش حروف سه رقم جدا کننده هزارها نخواهد بود.

برای ثبت و معرفی این کلاس‌ها باید به نحو ذیل در فایل global.asax.cs برنامه عمل کرد:
using System;
using System.Web.Mvc;
using System.Web.Routing;
using jqGrid05.CustomModelBinders;

namespace jqGrid05
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());
            ModelBinders.Binders.Add(typeof(decimal), new DecimalBinder());
        }
    }
}


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
jqGrid05.zip
 
اشتراک‌ها
آزمون Regex
در طی 13 سؤال با حدس زدن Regex درست هر مرحله.
آزمون Regex
مطالب
ساخت گزارش Code coverage مربوط به تست‌ها
اگر برای پروژه‌های خود تست نویسی انجام می‌دهید، یکی از موارد مهم، Code coverage مربوط به تست‌ها می‌باشد که نشان می‌دهد چند درصد از کدهای شما تست شده، کدام قسمت از کد، تست نشده‌است و ... .
در این مطلب نحوه‌ی استفاده از ReportGenerator را برای ایجاد گزارش مربوط به Code coverage، ارائه می‌دهیم. ReportGenerator دیتاهای تولید شده‌ی توسط coverlet, OpenCover, dotCover, Visual Studio, NCover, Cobertura, JaCoCo, Clover و ... را به یک گزارش قابل درک در فرمت‌های Html, Coberura و CSV تبدیل می‌کند. در  این مطلب چند موردتست نویسی با xUnit در Asp.Net Core  را نوشته‌ام و اکنون می‌خواهیم برای تست‌های نوشته شده، Code coverage آنها را بدست آوریم.
در کد زیر، سه تست برای متد AverageUsersAge نوشته شده است:
[Fact]
public async Task AverageUsersAge_When_Repository_Return_Null_Then_Zero_Should_Be_Returned()
{
    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync((List<User>)null);
    var result = await _userService.AverageUsersAge();
    result.Should().Be(0);
}
[Fact]
public async Task AverageUsersAge_When_Repository_Return_Empty_Then_Zero_Should_Be_Returned()
{
    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync(new List<User>());
    var result = await _userService.AverageUsersAge();
    result.Should().Be(0);
}
[Fact]
public async Task AverageUsersAge_When_Repository_Return_List_Of_Users_Then_Average_Of_Age_Should_Be_Retunred()
{

    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync(UserMockData.People);
    var result = await _userService.AverageUsersAge();
    var expected = (UserMockData.AdultUser.Age + UserMockData.ChildUser.Age + UserMockData.InfantUser.Age) / 3;
    result.Should().Be(expected);
}
اکنون اگر بخواهیم Code coverage مربوط به این تست‌ها را مشاهده کنیم، ابتدا باید پکیج coverlet.collector  را در پروژه‌ی تست نصب کنیم. سپس باید دستور زیر را برای نصب ReportGenerator اجرا کنید؛ درون powershell:
dotnet tool install -g dotnet-reportgenerator-globaltool
اکنون میتوانیم Code coverage تست‌ها را تولید کنیم و با استفاده از ReportGenerator، آن‌را به قالب HTML تبدیل کنیم. اگر به مسیر پروژه تست خود بروید و دستور زیر را وارد کنید:
dotnet test --collect:"XPlat Code Coverage"
در پروژه‌ی تست شما، یک پوشه به نام TestResults ایجاد می‌شود و همچنین یک فایل xml با عنوان coverage.cobertura ایجاد شده‌است که نتیجه‌ی Code coverage مربوط به تست‌های شما در آن قرار دارد. 
اکنون می‌توانید با استفاده از ReportGenerator گزارش تست‌های خود را از فایل ایجاد شده، تهیه کنید. برای این کار باید دستور زیر را در مسیر پروژه تست، در powershell  وارد کنید:
reportgenerator -reports:"C:\Users\Farhad\source\repos\xUnitExample\xUnitExample.Tests\TestResults\*\coverage.cobertura.xml" -targetdir:"coveragereport" -reporttypes:Html
در پارامتر reports باید مسیر فایل xml ایجاد شده مربوط به Code coverage را قرار دهید (با هر بار اجرای دستور "dotnet test --collect:"XPlat Code Coverage یک فایل xml جدید ایجاد می‌شود و ممکن است در سیستم شما مقدار GUID ایجاد شده، تفاوت داشته باشد). 
درون پوشه TestResults یک پوشه دیگر قرار دارد که نام آن پوشه، یک GUID می‎‌باشد و به همین دلیل به جای استفاده از GUID از * استفاده کرده‌ایم.
 اگر دستور بالا را اجرا کنید باید خروجی زیر را مشاهده کنید:
2021-11-18T14:05:02: Arguments
2021-11-18T14:05:02:  -reports:C:\Users\Farhad\source\repos\xUnitExample\xUnitExample.Tests\TestResults\*\coverage.cobertura.xml
2021-11-18T14:05:02:  -targetdir:coveragereport
2021-11-18T14:05:02:  -reporttypes:Html
2021-11-18T14:05:02: Writing report file 'coveragereport\index.html'
2021-11-18T14:05:02: Report generation took 0.3 seconds
اکنون اگر در مسیری که دستور بالا را اجرا کردید بروید، یک پوشه به نام coveragereport مشاهده می‌کنید که یک فایل index.html دارد و اگر آنرا در مرورگر مشاهده کنید، Code coverage مربوط به تست‌های نوشته شده را مشاهده میکنید:


در عکس ارسال شده، تست‌های نوشته شده برای تمامی لایه‌ها به صورت جدا ایجاد شده‌است.
تست مربوط به UserService:

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

برای مثال در عکس زیر مشخص شده‌است که برای فایل ApplicationConfiguration هیچ تستی نوشته نشده‌است.


اگر از CICD استفاده میکنید، می‌توانید در قسمت CI پروژه، هربار دستورات بالا را اجرا کنید و Code coverage مربوط به هر Build را به صورت جداگانه در کنار فایل‌های آپلود شده داشته باشد.
نکته: برای اجرای دستورات بالا و ساخت گزارش باید ورژن SDK نصب شده بر روی سیستم شما برابر 2.2.401 یا بیشتر باشد و ورژن Microsoft.NET.Test.Sdk برابر 16.5.0 یا بیشتر باشد.

مطالب
فعالسازی امکانات Refactoring افزونه‌ی Roslynator در VSCode
یکی از قابلیت‌های افزونه‌ی C# for Visual Studio Code پس از نگارش 1.10.0 آن، امکان بارگذاری افزونه‌های مخصوص Roslyn است که قابلیت‌های Refactoring را به همراه دارند؛ مانند افزونه‌ی معروف و جامع Roslynator. البته هنوز افزونه‌های Analyzers مبتنی بر Roslyn، با VSCode سازگاری ندارند که قرار است در نگارش‌های آتی افزوده شوند. در این مطلب، نحوه‌ی فعالسازی افزونه‌های Roslyn refactoring ثالث را بررسی خواهیم کرد.


فعالسازی قدم به قدم Roslyn refactoring افزونه‌ی Roslynator

الف) فایل VSIX آن‌را از اینجا دریافت کنید و سپس پسوند آن‌را به zip تغییر دهید.
ب) این فایل zip را در پوشه‌ای مشخص باز کنید.
ج) پس از باز کردن این فایل zip، دو فایل Roslynator.VisualStudio.Core.dll و Roslynator.VisualStudio.dll آن‌را حذف کنید. این فایل‌ها مخصوص نگارش کامل ویژوال استودیو هستند و در صورت وجود، با سیستم بارگذاری افزونه‌های OmniSharp تداخل می‌کنند.
د) در آخر مسیر زیر را گشوده:
%USERPROFILE%/.omnisharp
و سپس فایل جدید omnisharp.json را با محتوای ذیل، در آن مسیر ایجاد کنید:
{
  "RoslynExtensionsOptions": {
    "LocationPaths": [
      "C:\\lib\\roslynator"
    ]
  }
}
که در اینجا مسیر ذکر شده، به پوشه‌ای اشاره می‌کند که فایل zip افزونه را در آنجا گشوده‌اید (به ذکر \\ هم دقت داشته باشید؛ تا فایل json نهایی، به درستی تشکیل شود).

اکنون اگر VSCode را اجرا کنید، شاهد افزوده شدن امکانات Refactoring مخصوص افزونه‌ی Roslynator به لیست Refactoring پیش‌فرض OmniSharp خواهید بود:



خودکار سازی دریافت، نصب و فعالسازی Roslyn refactoring افزونه‌ی Roslynator

مراحل فوق را می‌توان تبدیل به یک اسکریپت پاورشل کرد که با هر بار اجرای آن، به صورت خودکار کار دریافت و نصب این افزونه صورت گیرد:
Write-Host "Download, unzip and enable Roslynator for Visual Studio Code"

$name = "josefpihrt.Roslynator2017"
$url = "https://marketplace.visualstudio.com/items?itemName=$name"
$currentDir = $PSScriptRoot
$file = "$currentDir\Roslynator.zip"

$pattern = "<script class=`"vss-extension`" defer=`"defer`" type=`"application\/json`">(.*?)<\/script>"
$regex = [regex]"(?m)$pattern"
Write-Host "Grab the home page of the $name."
$dom = (New-Object Net.WebClient).DownloadString($url); 
if($dom -and $dom -match $pattern) 
{
    $matches = $regex.Match($dom)
    $jsonText = $matches[0].Groups[1]

    $json = ConvertFrom-Json $jsonText

    $version = $Json.versions[0].version # Parse the json in the page for the latest version number
    $parts = $name.Split(".")
    $publisher = $parts[0]
    $package = $parts[1]

    # Assemble the url for the vsix package
    $packageUrl = "https://marketplace.visualstudio.com/_apis/public/gallery/publishers/$publisher/vsextensions/$package/$version/vspackage"
    Write-Host "Download the vsix package: $packageUrl"
    (New-Object Net.WebClient).DownloadFile($packageUrl, $file)

    Write-Host "Using $currentDir as the current dir."
    Write-Host "Unzip $file."
    $shellApp = new-object -com shell.application 
    $zipFile = $shellApp.namespace($file) 
    $destination = $shellApp.namespace($currentDir) 
    $destination.Copyhere($zipFile.items(), 0x14)# overwrite and be silent

    Write-Host "Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp."
    Remove-Item "$currentDir\Roslynator.VisualStudio.Core.dll","$currentDir\Roslynator.VisualStudio.dll", "$currentDir\Roslynator.VisualStudio.pkgdef"

    $omnisharpJsonFilePath = "$env:USERPROFILE\.omnisharp\omnisharp.json";
    Write-Host "Create $omnisharpJsonFilePath file."
    $omnisharpJson = @" 
{{
  "RoslynExtensionsOptions": {{
    "LocationPaths": [
      "{0}"
    ]
  }}
}}
"@ -f $currentDir -Replace "\\","\\"
    $omnisharpJson | Out-File "$omnisharpJsonFilePath" -Confirm

    Write-Host "Done!"
}
else
{
    Write-Host "Failed to find the packageUrl!"
}
کدهای فوق را با نام فرضی update.ps1 ذخیره کنید (و یا از اینجا دریافت کنید: update.zip ). سپس می‌توانید آن‌را با اجرای دستور update.ps1\.، اجرا کرده و به صورت خودکار شاهد این مراحل باشید:
PS D:\Prog\1396\RoslynatorVSCode> .\update.ps1
Download, unzip and enable Roslynator for Visual Studio Code
Grab the home page of the josefpihrt.Roslynator2017.
Download the vsix package: https://marketplace.visualstudio.com/_apis/public/gallery/publishers/josefpihrt/vsextensions/Roslynator2017/1.7.0/vspackage
Using D:\Prog\1396\RoslynatorVSCode as the current dir.
Unzip D:\Prog\1396\RoslynatorVSCode\Roslynator.zip.
Delete VS specific files. Otherwise they will interfere with the MEF services inside OmniSharp.
Create C:\Users\Vahid\.omnisharp\omnisharp.json file.

Confirm
Are you sure you want to perform this action?
Performing the operation "Output to File" on target "C:\Users\Vahid\.omnisharp\omnisharp.json".
[Y] Yes  [A] Yes to All  [N] No  [L] No to All  [S] Suspend  [?] Help (default is "Y"):
Done!
ابتدا صفحه‌ی اصلی این افزونه دریافت می‌شود. سپس از داخل آن، پارامترهای مخصوص دانلود افزونه استخراج و آدرس دریافت نهایی آن تشکیل می‌شود.
در ادامه این افزونه دریافت شده و در پوشه‌ی جاری باز خواهد شد. سپس فایل omnisharp.json نیز به صورت خودکار تشکیل و مقدار دهی می‌شود.
اکنون اگر VSCode را اجرا کنید، همه چیز آماده بوده و امکانات این افزونه در دسترس خواهند بود.