مطالب
تهیه XML امضاء شده جهت تولید مجوز استفاده از برنامه
اگر به فایل مجوز استفاده از برنامه‌‌ای مانند EF Profiler دقت کنید، یک فایل XML به ظاهر ساده بیشتر نیست:
<?xml version="1.0" encoding="utf-8"?>
<license id="17d46246-a6cb-4196-98a0-ff6fc08cb67f" expiration="2012-06-12T00:00:00.0000000" type="Trial" prof="EFProf">
  <name>MyName</name>
  <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315" />
      <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
        <DigestValue>b8N0bDE4gTakfdGKtzDflmmyyXI=</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>IPELgc9BbkD8smXSe0sGqp5vS57CtZo9ME2ZfXSq/thVu...=</SignatureValue>
  </Signature>
</license>
در این فایل ساده متنی، نوع مجوز استفاده از برنامه، Trial ذکر شده است. شاید عنوان کنید که خوب ... نوع مجوز را به Standard تغییر می‌دهیم و فایل را ذخیره کرده و نهایتا استفاده می‌کنیم. اما ... نتیجه حاصل کار نخواهد کرد! حتی اگر یک نقطه به اطلاعات این فایل اضافه شود، دیگر قابل استفاده نخواهد بود. علت آن هم به قسمت Signature فایل XML فوق بر می‌گردد.
در ادامه به نحوه تولید و استفاده از یک چنین مجوزهای امضاء شده‌ای در برنامه‌های دات نتی خواهیم پرداخت.


تولید کلیدهای RSA

برای تهیه امضای دیجیتال یک فایل XML نیاز است از الگوریتم RSA استفاده شود.
برای تولید فایل XML امضاء شده، از کلید خصوصی استفاده خواهد شد. برای خواندن اطلاعات مجوز (فایل XML امضاء شده)، از کلیدهای عمومی که در برنامه قرار می‌گیرند کمک خواهیم گرفت (برای نمونه برنامه EF Prof این کلیدها را در قسمت Resourceهای خود قرار داده است).
استفاده کننده تنها زمانی می‌تواند مجوز معتبری را تولید کند که دسترسی به کلیدهای خصوصی تولید شده را داشته باشد.
        public static string CreateRSAKeyPair(int dwKeySize = 1024)
        {
            using (var provider = new RSACryptoServiceProvider(dwKeySize))
            {
                return provider.ToXmlString(includePrivateParameters: true);
            }
        }
امکان تولید کلیدهای اتفاقی مورد استفاده در الگوریتم RSA، در دات نت پیش بینی شده است. خروجی متد فوق یک فایل XML است که به همین نحو در صورت نیاز توسط متد provider.FromXmlString مورد استفاده قرار خواهد گرفت.


تهیه ساختار مجوز

در ادامه یک enum که بیانگر انواع مجوزهای برنامه ما است را مشاهده می‌کنید:
namespace SignedXmlSample
{
    public enum LicenseType
    {
        None,
        Trial,
        Standard,
        Personal
    }
}
به همراه کلاسی که اطلاعات مجوز تولیدی را دربر خواهد گرفت:
using System;
using System.Xml.Serialization;

namespace SignedXmlSample
{
    public class License
    {
        [XmlAttribute]
        public Guid Id { set; get; }
        
        public string Domain { set; get; }

        [XmlAttribute]
        public string IssuedTo { set; get; }

        [XmlAttribute]
        public DateTime Expiration { set; get; }

        [XmlAttribute]
        public LicenseType Type { set; get; }
    }
}
خواص این کلاس یا عناصر enum یاد شده کاملا دلخواه هستند و نقشی را در ادامه بحث نخواهند داشت؛ از این جهت که از مباحث XmlSerializer برای تبدیل وهله‌ای از شیء مجوز به معادل XML آن استفاده می‌شود. بنابراین المان‌های آن‌را مطابق نیاز خود می‌توانید تغییر دهید. همچنین ذکر ویژگی XmlAttribute نیز اختیاری است. در اینجا صرفا جهت شبیه سازی معادل مثالی که در ابتدای بحث مطرح شد، از آن استفاده شده است. این ویژگی راهنمایی است برای کلاس XmlSerializer تا خواص مزین شده با آن‌را به شکل یک Attribute در فایل نهایی ثبت کند.


تولید و خواندن مجوز دارای امضای دیجیتال

کدهای کامل کلاس تولید و خواندن یک مجوز دارای امضای دیجیتال را در اینجا مشاهده می‌کنید:
using System;
using System.IO;
using System.Security.Cryptography; // needs a ref. to `System.Security.dll` asm.
using System.Security.Cryptography.Xml;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

namespace SignedXmlSample
{
    public static class LicenseGenerator
    {
        public static string CreateLicense(string licensePrivateKey, License licenseData)
        {
            using (var provider = new RSACryptoServiceProvider())
            {
                provider.FromXmlString(licensePrivateKey);
                var xmlDocument = createXmlDocument(licenseData);
                var xmlDigitalSignature = getXmlDigitalSignature(xmlDocument, provider);
                appendDigitalSignature(xmlDocument, xmlDigitalSignature);
                return xmlDocumentToString(xmlDocument);
            }
        }

        public static string CreateRSAKeyPair(int dwKeySize = 1024)
        {
            using (var provider = new RSACryptoServiceProvider(dwKeySize))
            {
                return provider.ToXmlString(includePrivateParameters: true);
            }
        }

        public static License ReadLicense(string licensePublicKey, string xmlFileContent)
        {
            var doc = new XmlDocument();
            doc.LoadXml(xmlFileContent);

            using (var provider = new RSACryptoServiceProvider())
            {
                provider.FromXmlString(licensePublicKey);

                var nsmgr = new XmlNamespaceManager(doc.NameTable);
                nsmgr.AddNamespace("sig", "http://www.w3.org/2000/09/xmldsig#");

                var xml = new SignedXml(doc);
                var signatureNode = (XmlElement)doc.SelectSingleNode("//sig:Signature", nsmgr);
                if (signatureNode == null)
                    throw new InvalidOperationException("This license file is not signed.");

                xml.LoadXml(signatureNode);
                if (!xml.CheckSignature(provider))
                    throw new InvalidOperationException("This license file is not valid.");

                var ourXml = xml.GetXml();
                if (ourXml.OwnerDocument == null || ourXml.OwnerDocument.DocumentElement == null)
                    throw new InvalidOperationException("This license file is coruppted.");

                using (var reader = new XmlNodeReader(ourXml.OwnerDocument.DocumentElement))
                {
                    var xmlSerializer = new XmlSerializer(typeof(License));
                    return (License)xmlSerializer.Deserialize(reader);
                }
            }
        }

        private static void appendDigitalSignature(XmlDocument xmlDocument, XmlNode xmlDigitalSignature)
        {
            xmlDocument.DocumentElement.AppendChild(xmlDocument.ImportNode(xmlDigitalSignature, true));
        }

        private static XmlDocument createXmlDocument(License licenseData)
        {
            var serializer = new XmlSerializer(licenseData.GetType());
            var sb = new StringBuilder();
            using (var writer = new StringWriter(sb))
            {
                var ns = new XmlSerializerNamespaces(); ns.Add("", "");
                serializer.Serialize(writer, licenseData, ns);
                var doc = new XmlDocument();
                doc.LoadXml(sb.ToString());
                return doc;
            }
        }

        private static XmlElement getXmlDigitalSignature(XmlDocument xmlDocument, AsymmetricAlgorithm key)
        {
            var xml = new SignedXml(xmlDocument) { SigningKey = key };
            var reference = new Reference { Uri = "" };
            reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
            xml.AddReference(reference);
            xml.ComputeSignature();
            return xml.GetXml();
        }

        private static string xmlDocumentToString(XmlDocument xmlDocument)
        {
            using (var ms = new MemoryStream())
            {
                var settings = new XmlWriterSettings { Indent = true, Encoding = Encoding.UTF8 };
                var xmlWriter = XmlWriter.Create(ms, settings);
                xmlDocument.Save(xmlWriter);
                ms.Position = 0;
                return new StreamReader(ms).ReadToEnd();
            }
        }
    }
}
توضیحات:
در حین کار با متد CreateLicense، پارامتر licensePrivateKey همان اطلاعاتی است که به کمک متد CreateRSAKeyPair قابل تولید است. توسط پارامتر licenseData، اطلاعات مجوز در حال تولید اخذ می‌شود. در این متد به کمک provider.FromXmlString، اطلاعات کلیدهای RSA دریافت خواهند شد. سپس توسط متد createXmlDocument، محتوای licenseData دریافتی به یک فایل XML نگاشت می‌گردد (بنابراین اهمیتی ندارد که حتما از ساختار کلاس مجوز یاد شده استفاده کنید). در ادامه متد getXmlDigitalSignature با در اختیار داشتن معادل XML شیء مجوز و کلیدهای لازم، امضای دیجیتال متناظری را تولید می‌کند. با استفاده از متد appendDigitalSignature، این امضاء را به فایل XML اولیه اضافه می‌کنیم. از این امضاء جهت بررسی اعتبار مجوز برنامه در متد ReadLicense استفاده خواهد شد.
برای خواندن یک فایل مجوز امضاء شده در برنامه خود می‌توان از متد ReadLicense استفاده کرد. توسط آرگومان licensePublicKey، اطلاعات کلید عمومی دریافت می‌شود. این کلید دربرنامه، ذخیره و توزیع می‌گردد. پارامتر xmlFileContent معادل محتوای فایل XML مجوزی است که قرار است مورد ارزیابی قرار گیرد.


مثالی در مورد نحوه استفاده از کلاس تولید مجوز

در ادامه نحوه استفاده از متدهای CreateLicense و  ReadLicense را ملاحظه می‌کنید؛ به همراه آشنایی با نمونه کلیدهایی که باید به همراه برنامه منتشر شوند:
using System;
using System.IO;

namespace SignedXmlSample
{
    class Program
    {
        static void Main(string[] args)
        {
            //Console.WriteLine(LicenseGenerator.CreateRSAKeyPair());
            writeLicense();            
            readLicense();

            Console.WriteLine("Press a key...");
            Console.ReadKey();
        }

        private static void readLicense()
        {
            var xml = File.ReadAllText("License.xml");
            const string publicKey = @"<RSAKeyValue>
                                    <Modulus>
                                        mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc=
                                    </Modulus>
                                    <Exponent>
                                        AQAB
                                    </Exponent>
                                 </RSAKeyValue>";

            var result = LicenseGenerator.ReadLicense(publicKey, xml);

            Console.WriteLine(result.Domain);
            Console.WriteLine(result.IssuedTo);
        }

        private static void writeLicense()
        {
            const string rsaData = @"<RSAKeyValue>
                    <Modulus>
                        mBNKFIc/LkMfaXvLlB/+6EujPkx3vBOvLu8jdESDSQLisT8K96RaDMD1ORmdw2XNdMw/6ZBuJjLhoY13qCU9t7biuL3SIxr858oJ1RLM4PKhA/wVDcYnJXmAUuOyxP/vfvb798o6zAC1R2QWuzG+yJQR7bFmbKH0tXF/NOcSgbc=
                    </Modulus>
                    <Exponent>
                        AQAB
                    </Exponent>
                    <P>
                        xwPKN77EcolMTD2O2Csv6k9Y4aen8UBVYjeQ4PtrNGz0Zx6I1MxLEFzRpiKC/Ney3xKg0Icwj0ebAQ04d5+HAQ==
                    </P>
                    <Q>
                        w568t0Xe6OBUfCyAuo7tTv4eLgczHntVLpjjcxdUksdVw7NJtlnOLApJVJ+U6/85Z7Ji+eVhuN91yn04pQkAtw==
                    </Q>
                    <DP>
                        svkEjRdA4WP5uoKNiHdmMshCvUQh8wKRBq/D2aAgq9fj/yxlj0FdrAxc+ZQFyk5MbPH6ry00jVWu3sY95s4PAQ==
                    </DP>
                    <DQ>
                        WcRsIUYk5oSbAGiDohiYeZlPTBvtr101V669IUFhhAGJL8cEWnOXksodoIGimzGBrD5GARrr3yRcL1GLPuCEvQ==
                    </DQ>
                    <InverseQ>
                        wIbuKBZSCioG6MHdT1jxlv6U1+Y3TX9sHED9PqGzWWpVGA+xFJmQUxoFf/SvHzwbBlXnG0DLqUvxEv+BkEid2w==
                    </InverseQ>
                    <D>                        Yk21yWdT1BfXqlw30NyN7qNWNuM/Uvh2eaRkCrhvFTckSucxs7st6qig2/RPIwwfr6yIc/bE/TRO3huQicTpC2W3aXsBI9822OOX4BdWCec2txXpSkbZW24moXu+OSHfAdYoOEN6ocR7tAGykIqENshRO7HvONJsOE5+1kF2GAE=
                    </D>
                  </RSAKeyValue>";

            string data = LicenseGenerator.CreateLicense(
                                                rsaData,
                                                new License
                                                {
                                                     Id = Guid.NewGuid(),
                                                     Domain = "dotnettips.info",
                                                     Expiration = DateTime.Now.AddYears(2),
                                                     IssuedTo = "VahidN",
                                                     Type = LicenseType.Standard
                                                });
            File.WriteAllText("License.xml", data);
        }
    }
}
ابتدا توسط متد CreateRSAKeyPair کلیدهای لازم را تهیه و ذخیره کنید. این کار یکبار باید صورت گیرد.
همانطور که مشاهده می‌کنید، اطلاعات کامل یک نمونه از آن، در متد writeLicense مورد نیاز است. اما در متد readLicense تنها به قسمت عمومی آن یعنی Modulus و Exponent نیاز خواهد بود (موارد قابل انتشار به همراه برنامه).

سؤال: امنیت این روش تا چه اندازه است؟
پاسخ: تا زمانیکه کاربر نهایی به کلیدهای خصوصی شما دسترسی پیدا نکند، امکان تولید معادل آن‌ها تقریبا در حد صفر است و به طول عمر او قد نخواهد داد!
اما ... مهاجم می‌تواند کلیدهای عمومی و خصوصی خودش را تولید کند. مجوز دلخواهی را بر این اساس تهیه کرده و سپس کلید عمومی جدید را در برنامه، بجای کلیدهای عمومی شما درج (patch) کند! بنابراین روش بررسی اینکه آیا برنامه جاری patch شده است یا خیر را فراموش نکنید. یا عموما مطابق معمول قسمتی از برنامه که در آن مقایسه‌ای بین اطلاعات دریافتی و اطلاعات مورد انتظار وجود دارد، وصله می‌شوند؛ این مورد عمومی است و منحصر به روش جاری نمی‌باشد.

دریافت نسخه جنریک این مثال:
SignedXmlSample.zip
اشتراک‌ها
تهیه قالبی بهتر برای برنامه‌ها‌ی کنسول دات نتی

اگر امروز با استفاده از دستور dotnet new console یک برنامه‌ی کنسول را شروع کنیم، به یک فایل program.cs تقریبا خالی و یک فایل csproj. مرتبط می‌رسیم و ... همین! اما نیازهای واقعی ما در یک برنامه‌ی کنسول، شامل این موارد هستند:
- نیاز به انجام تزریق وابستگی‌ها
- نیاز به دسترسی به فایل‌های کانفیگ؛ شبیه به برنامه‌های وب
- نیاز به کار با EF و مدیریت صحیح طول عمر Context آن
- مشخص بودن شماره نگارش دات نت و همچنین ابزارهای EF استفاده شده
- غنی سازی کامپایلر با یک سری آنالایزر و تنظیمات مخصوص آن جهت اعمال سخت گیری‌های بیشتر!
- استفاده از مدیریت مرکزی بسته‌های نیوگت برای پروژه‌های مختلف در Solution
- نیاز به کار با HttpClient؛ آن هم به نحو صحیحی
- نیاز به دسترسی به سوئیچ‌ها و پارامترهای ارسالی به برنامه‌ی کنسول از طریق خط فرمان
- نیاز به دسترسی به سیستم Logging؛ شبیه به برنامه‌های وب
- امکان انجام آزمون‌های واحد و یکپارچگی درون حافظه‌ای

تمام این موارد به صورت از پیش تنظیم شده، در قالب ارائه شده‌ی توسط این پروژه موجود هستند.
برای نصب آن‌، ابتدا مخزن کد فوق را clone کرده و سپس فایل register_template.cmd آن‌را اجرا کنید. پس از آن دستور dotnet new dntconsole، جهت ایجاد پروژه‌های جدید مبتنی بر این قالب، در دسترس خواهد بود.

تهیه قالبی بهتر برای برنامه‌ها‌ی کنسول دات نتی
نظرات مطالب
‫نکات نصب برنامه‌های ASP.NET 4.0 بر روی IIS 6
خیر. «باید» به ازای هر برنامه یک application pool مجزا درست کنید. برای مطالعه بیشتر:
- برای مدیریت نگارش‌های مختلف دات نت مورد استفاده (مطلب جاری)
- جهت مدیریت بهتر میزان مصرف حافظه یک برنامه: مروری بر تاریخچه محدودیت حافظه مصرفی برنامه‌های ASP.NET در IIS
- برای جلوگیری از ری‌استارت شدن بی‌جهت سایر برنامه‌ها: چه زمان‌هایی یک برنامه‌ی ASP.NET ری استارت می‌شود؟    
- جهت کنترل میزان مصرف CPU یک برنامه: بررسی علت CPU Usage بالای برنامه در حال اجرا   
نظرات مطالب
کنترل DatePicker شمسی مخصوص Silverlight 4
چندتا مطلب هست:
- احتمالا منظور شما از VB.NET همان WPF بوده و این بحث جاری هم سیلورلایت است. نسخه WPF این کنترل هم موجود است. باید سورس‌ها را دریافت کنید و کامپایل. مثال WPF هم دارد در سورس‌های مجموعه (در پوشه WpfPersianDatePickerUsage).
- خطایی که نوشتید مشکل VS.NET است که در دو جای مختلف به دنبال یک سری اسمبلی می‌گردد. در مسیر ProjectAssemblies\zgrq7cfx01 و در مسیر Common7\IDE. این تداخل رو می‌تونید با پاک کردن پوشه‌های obj و bin و مسیرهای یاد شده و بعد rebuild کامل پروژه احتمالا برطرف کنید. در کل مشکل کنترل دریافتی نیست. تداخل نگارش‌ها رخ داده روی سیستم شما.
- بله. مراجعه کنید به مثال موجود در پوشه WpfPersianDatePickerUsage. (سورس را باید دریافت کنید)
مطالب دوره‌ها
متدهای async تقلبی
تا اینجا مشاهده کردیم که اگر یک چنین متد زمانبری را داشته باشیم که در آن عملیاتی طولانی انجام می‌شود،
class MyService
{
  public int CalculateXYZ()
  {
    // Tons of work to do in here!
    for (int i = 0; i != 10000000; ++i)
      ;
    return 42;
  }
}
برای نوشتن معادل async آن فقط کافی است که امضای متد را به async Task تغییر دهیم و سپس داخل آن از Task.Run استفاده کنیم:
class MyService
{
  public async Task<int> CalculateXYZAsync()
  {
    return await Task.Run(() =>
    {
      // Tons of work to do in here!
      for (int i = 0; i != 10000000; ++i)
        ;
      return 42;
    });
  }
}
و ... اگر از آن در یک کد UI استفاده کنیم، ترد آن‌را قفل نکرده و برنامه، پاسخگوی سایر درخواست‌های رسیده خواهد بود. اما ... به این روش اصطلاحا Fake Async گفته می‌شود؛ یا Async تقلبی!
کاری که در اینجا انجام شده، استفاده‌ی ناصحیح از Task.Run در حین طراحی یک متد و یک API است. عملیات انجام شده در آن واقعا غیرهمزمان نیست و در زمان انجام آن، باز هم ترد جدید اختصاص داده شده را تا پایان عملیات قفل می‌کند. اینجا است که باید بین CPU-bound operations و IO-bound operations تفاوت قائل شد. اگر Entity Framework 6 و یا کلاس WebClient و امثال آن، متدهایی Async را نیز ارائه داده‌اند، این‌ها به معنای واقعی کلمه، غیرهمزمان هستند و در آن‌ها کوچکترین CPU-bound operation ایی انجام نمی‌شود.
در حلقه‌ای که در مثال فوق در حال پردازش است و یا تمام اعمال انجام شده توسط CPU، از مرزهای سیستم عبور نمی‌کنیم. نه قرار است فایلی را ذخیره کنیم، نه با اینترنت سر و کار داشته باشیم و یا مثلا اطلاعاتی را از وب سرویسی دریافت کنیم و نه هیچگونه IO-bound operation خاصی قرار است صورت گیرد.
زمانیکه برنامه نویسی قرار است با API شما کار کند و به امضای async Task می‌رسد، فرضش بر این است که در این متد واقعا یک کار غیرهمزمان در حال انجام است. بنابراین جهت بالابردن کارآیی برنامه، این نسخه را نسبت به نمونه‌ی غیرهمزمان انتخاب می‌کند.
حال تصور کنید که استفاده کننده از این API یک برنامه‌ی دسکتاپ نیست، بلکه یک برنامه‌ی ASP.NET است. در اینجا Task.Run فراخوانی شده صرفا سبب خواهد شد عملیات مدنظر، بر روی یک ترد دیگر، نسبت به ترد اصلی اختصاص داده شده توسط ASP.NET برای فراخوانی و پردازش CalculateXYZAsync، صورت گیرد. این عملیات بهینه نیست. تمام پردازش‌های درخواست‌های ASP.NET در تردهای خاص خود انجام می‌شوند. وجود ترد دوم ایجاد شده توسط Task.Run در اینجا چه حاصلی را بجز سوئیچ بی‌جهت بین تردها و همچنین بالا بردن میزان کار Garbage collector دارد؟ در این حالت نه تنها سبب بالا بردن مقیاس پذیری سیستم نشده‌ایم، بلکه میزان کار Garbage collector و همچنین سوئیچ بین تردهای مختلف را در Thread pool برنامه به شدت افزایش داده‌ایم. همچنین یک چنین سیستمی برای تدارک تردهای بیشتر و مدیریت آن‌ها، مصرف حافظه‌ی بیشتری نیز خواهد داشت.


یک اصل مهم در طراحی کدهای Async
استفاده از Task.Run در پیاده سازی بدنه متدهای غیرهمزمان، یک code smell محسوب می‌شود.


چکار باید کرد؟
اگر در کدهای خود اعمال Async واقعی دارید که IO-bound هستند، از معادل‌های Async طراحی شده برای کار با آن‌ها، مانند متد SaveChangesAsync در EF، متد DownloadStringTaskAsync کلاس WebClient و یا متدهای جدید Async کلاس Stream برای خواندن و نوشتن اطلاعات استفاده کنید. در یک چنین حالتی ارائه متدهای async Task بسیار مفید بوده و در جهت بالابردن مقیاس پذیری سیستم بسیار مؤثر واقع خواهند شد.
اما اگر کدهای شما صرفا قرار است بر روی CPU اجرا شوند و تنها محاسباتی هستند، اجازه دهید مصرف کننده تصمیم بگیرد که آیا لازم است از Task.Run برای فراخوانی متد ارائه شده در کدهای خود استفاده کند یا خیر. اگر برنامه‌ی دسکتاپ است، این فراخوانی مفید بوده و سبب آزاد شدن ترد UI می‌شود. اگر برنامه‌ی وب است، به هیچ عنوان نیازی به Task.Run نبوده و فراخوانی متداول آن با توجه به اینکه درخواست‌های برنامه‌های ASP.NET در تردهای مجزایی اجرا می‌شوند، کفایت می‌کند.

به صورت خلاصه
از Task.Run در پیاده سازی بدنه متدهای API خود استفاده نکنید.
از Task.Run در صورت نیاز (مثلا در برنامه‌های دسکتاپ) در حین فراخوانی و استفاده از متدهای API ارائه شده استفاده نمائید:
 private async void MyButton_Click(object sender, EventArgs e)
{
  await Task.Run(() => myService.CalculateXYZ());
}
در این مثال از همان نسخه‌ی غیرهمزمان متد محاسباتی استفاده شده‌است و اینبار مصرف کننده است که تصمیم گرفته در حین فراخوانی و استفاده نهایی، برای آزاد سازی ترد UI از await Task.Run استفاده کند (یا خیر).

بنابراین نوشتن یک چنین کدهایی در پیاده سازی یک API غیرهمزمان
await Task.Run(() =>
{
   for (int i = 0; i != 10000000; ++i)
     ;
});
صرفا خود را گول زدن است. کل این عملیات بر روی CPU انجام شده و هیچگاه از مرزهای IO سیستم عبور نمی‌کند.

برای مطالعه بیشتر
Should I expose asynchronous wrappers for synchronous methods
مطالب
استفاده از افزونه‌های Owin مخصوص سایر نگارش‌های ASP.NET در ASP.NET Core
همانطور که اطلاع دارید یکسری از کتابخانه‌های کمکی و ثالث ASP.NET Core همچون OData و SignalR ، Thinktecture IdentityServer هنوز در حال تکمیل هستند و از آنجایی که هر روزه محبوبیت ASP.NET Core در بین برنامه نویسان در حال افزایش است و خیلی از پروژه‌های نرم افزاری که امروزه start میخورند، از این فریم ورک جدید استفاده میکنند، پس خیلی به اهمیت این مقوله افزوده میشود که بتوان از تکنولوژی‌های فوق در پروژه‌های جدید نیز استفاده کرد و یکی از معقول‌ترین راه‌های ممکن آن، پیاده سازی Owin  در کنار ASP.NET Core Pipeline میباشد. بدین معنا که ما قصد نداریم owin pipeline را جایگزین آن نماییم؛ همانطور که در ادامه‌ی این مقاله گفته خواهد شد، از هر دو معماری (افزونه‌های Owin مخصوص نگارش کامل دات نت و همچنین خود ASP.NET Core) در کنار هم استفاده خواهیم کرد.
پیشنیاز این مقاله آشنایی با ASP.NET Core و MiddleWare و همچنین آشنایی با Owin میباشد. همانطور که عرض کردم اگر مقاله‌ی پیاده سازی OData را مطالعه کرد‌ه‌اید و هم اکنون قصد و یا علاقه‌ی به استفاده‌ی از آن را در پروژه‌ی ASP.NET Core خود دارید، میتوانید این مقاله را دنبال نمایید.
پس تا کنون متوجه شدیم که هدف از این کار، توانایی استفاده از ابزارهایی است که با Owin سازگاری کامل را دارند و همچنین به نسخه‌ی پایدار خود رسیده‌اند. بطور مثال IdentityServer 4 در حال تهیه است که با ASP.NET Core سازگار است. اما هنوز چند نسخه‌ی beta از آن بیرون آمده و به پایداری کامل نرسیده است. همچنین زمان توزیع نهایی آن نیز دقیقا مشخص نیست.
شما برای استفاده از ASP.NET Core RTM 1.0 احتیاج به Visual studio 2015 update 3 دارید. ابتدا یک پروژه‌ی #C از نوع Asp.Net Core Web Application  را «به همراه full .Net Framework» به نام OwinCore میسازیم.
در حال حاضر بدلیل اینکه پکیج‌هایی مانند OData, SignalR هنوز به نسخه‌ی Net Coreی خود آماده نشده‌اند (در حال پیاده سازی هستند)، پس مجبور به استفاده از full .Net framework هستیم و خوب مسلما برنامه در این حالت چند سکویی نخواهد بود. اما به محض اینکه آن‌ها آماده شدند، میتوان full .Net را کنار گذاشته و از NET Core. استفاده نمود و از آنجایی که Owin فقط یک استاندارد هست، هیچ مشکلی با NET Core. نداشته و برنامه را میتوان بدان منتقل کرد و از مزایای چند سکویی بودن آن نیز بهره برد. پس مشکل در حال حاضر Owin نبوده و مجبور به منتظر بودن برای آماده شدن این پکیج‌ها خواهیم بود. مثال عملی این قضیه نیز Nancy است که نسخه‌ی NET Core.ی آن با استفاده از Owin در ASP.NET Core پیاده سازی شده است. در اینجا مثال عملی آن را میتوانید پیدا کنید.


در قسمت بعد، قالب را هم از نوع empty انتخاب مینماییم.


در ادامه فایل project.json را باز کرده و در قسمت dependencies، تغییرات زیر را اعمال نمایید.

قبل از اینکه شما را از این همه وابستگی نگران کنم، باید عرض کنم فقط Microsoft.Owin , Microsoft.AspNetCore.Owin، پکیج‌های اجباری هستند؛ باقی آن‌ها برای نشان دادن انعطاف پذیری بالای این روش میباشند:

"dependencies": {
    "Microsoft.AspNet.OData": "5.9.1",
    "Microsoft.AspNet.SignalR": "2.2.1",
    "Microsoft.AspNet.WebApi.Client": "5.2.3",
    "Microsoft.AspNet.WebApi.Core": "5.2.3",
    "Microsoft.AspNet.WebApi.Owin": "5.2.3",
    "Microsoft.AspNetCore.Diagnostics": "1.0.0",
    "Microsoft.AspNetCore.Hosting": "1.0.0",
    "Microsoft.AspNetCore.Mvc": "1.0.0",
    "Microsoft.AspNetCore.Owin": "1.0.0",
    "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Net.Http": "2.2.29",
    "Microsoft.Owin": "3.0.1",
    "Microsoft.Owin.Diagnostics": "3.0.1",
    "Microsoft.Owin.FileSystems": "3.0.1",
    "Microsoft.Owin.StaticFiles": "3.0.1",
    "Newtonsoft.Json": "9.0.1"
  },
//etc...

بعد از ذخیره کردن این فایل، در پنجره‌ی Output خود شاهد دانلود شدن این پکیج‌ها خواهید بود. در اینجا پکیج‌های مربوط به Owin, Odata, SignalR را مشاهد می‌کنید. ضمن اینکه در کنار آن، AspNetCore.Mvc را نیز مشاهده میفرمایید. دلیل این کار این است که این دو نوع متفاوت قرار است در کنار هم کار کنند و هیچ مشکلی با دیگری ندارند.

در مسیر اصلی پروژه‌ی خود کلاسی به نام OwinExtensions را با محتوای زیر بسازید:

namespace OwinCore
{
    public static class OwinExtensions
    {
        public static IApplicationBuilder UseOwinApp(
            this IApplicationBuilder aspNetCoreApp,
            Action<IAppBuilder> configuration)
        {
            return aspNetCoreApp.UseOwin(setup => setup(next =>
            {
                AppBuilder owinAppBuilder = new AppBuilder();

                IApplicationLifetime aspNetCoreLifetime = (IApplicationLifetime)aspNetCoreApp.ApplicationServices.GetService(typeof(IApplicationLifetime));

                AppProperties owinAppProperties = new AppProperties(owinAppBuilder.Properties);

                owinAppProperties.OnAppDisposing = aspNetCoreLifetime?.ApplicationStopping ?? CancellationToken.None;

                owinAppProperties.DefaultApp = next;

                configuration(owinAppBuilder);

                return owinAppBuilder.Build<Func<IDictionary<string, object>, Task>>();
            }));
        }
    }
}

یک Extension Method به نام UseOwinApp اضافه شده به IApplicationBuilder که مربوط به ASP.NET Core میباشد و درون آن نیز AppBuilder را که مربوط به Owin pipeline میباشد، نمونه سازی کرده‌ایم که باعث میشود Owin pipeline بر روی ASP.NET Core pipeline سوار شود.

حال میخواهیم یک Middleware سفارشی را با استفاده از Owin نوشته و در Startup پروژه، آن را فراخوانی نماییم. کلاسی به نام AddSampleHeaderToResponseHeadersOwinMiddleware را با محتوای زیر تولید مینماییم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersOwinMiddleware : OwinMiddleware
    {
        public AddSampleHeaderToResponseHeadersOwinMiddleware(OwinMiddleware next)
            : base(next)
        {
        }
        public async override Task Invoke(IOwinContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test", new[] { context.Request.Uri.ToString() });

            await Next.Invoke(context);
        }
    }
}

کلاسی است که از owinMiddleware ارث بری کرده و در متد override شده‌ی Invoke نیز با استفاده از IOwinContext، به پیاده سازی Middleware خود میپردازیم. Exception مربوطه را comment کرده (بعدا در مرحله‌ی تست از آن نیز استفاده مینماییم) و در خط بعدی در هدر response هر request، یک شیء را به نام Test و با مقدار Uri آن request، میسازیم.

خط بعدی هم اعلام میدارد که به Middleware بعدی برود.

در ادامه فایل Startup.cs را باز کرده و اینگونه متد Configure را تغییر دهید:

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });
        }

مشاهده میفرمایید با استفاده از UserOwinApp میتوانیم Middleware‌های Owinی خود را register نماییم و نکته‌ی قابل توجه این است که در کنار آن نیز می‌توانیم از IHostingEnviroment مربوط به ASP.NET core استفاده نماییم. owinApp.UseErrorPage از Microsoft.Owin.Diagnostics گرفته شده است و در خط بعدی نیز Middleware شخصی خود را register کرده‌ایم. پروژه را run کرده و در response این را مشاهد مینمایید.

اکنون اگر در Middleware سفارشی خود، آن Exception را از حالت comment در بیاوریم، در صورتیکه در حالت development باشیم، با این صفحه مواجه خواهیم شد:

Exception مربوطه را به حالت comment گذاشته و ادامه میدهیم.

برای اینکه نشان دهیم Owin و ASP.NET Core pipeline در کنار هم میتوانند کار کنند، یک Middleware را از نوع ASP.NET Core نوشته و آن را register مینماییم. کلاسی جدیدی را به نام AddSampleHeaderToResponseHeadersAspNetCoreMiddlware با محتوای زیر میسازیم:

namespace OwinCore
{
    public class AddSampleHeaderToResponseHeadersAspNetCoreMiddlware
    {
        private readonly RequestDelegate Next;

        public AddSampleHeaderToResponseHeadersAspNetCoreMiddlware(RequestDelegate next)
        {
            Next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            //throw new InvalidOperationException("ErrorTest");

            context.Response.Headers.Add("Test2", new[] { "some text" });

            await Next.Invoke(context);
        }
    }
}

متد Configure در Startup.cs را نیز اینگونه تغییر میدهیم

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }

                owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

اکنون AddSampleHeaderToResponseHeadersAspNetCoreMiddlware رجیستر شده است و بعد از run کردن پروژه و بررسی header response باید این را ببینیم

میبینید که به ترتیب اجرای Middleware‌ها، ابتدا Test مربوط به Owin و بعد آن Test2 مربوط به ASP.NET Core تولید شده است.

حال اجازه دهید Odata را با استفاده از Owin پیاده سازی نماییم. ابتدا کلاسی را به نام Product با محتوای زیر تولید نمایید:

namespace OwinCore
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public decimal Price { get; set; }
    }
}

حال کلاسی را به نام ProductsController با محتوای زیر میسازیم:

namespace OwinCore
{
    public class ProductsController : ODataController
    {
        [EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                 new Product { Id = 1, Name = "Test" , Price = 10 }
            }
            .AsQueryable();
        }
    }
}

اگر مقاله‌ی پیاده سازی Crud با استفاده از OData را مطالعه کرده باشید، قاعدتا با این کد‌ها آشنا خواهید بود. ضمن اینکه پرواضح است که OData هیچ وابستگی به entity framework ندارد.

برای config آن نیز در Startup.cs پروژه و متد Configure، تغییرات زیر را اعمال مینماییم.

public void Configure(IApplicationBuilder aspNetCoreApp, IHostingEnvironment env)
        {
            //aspNetCoreApp.UseMvc();

            aspNetCoreApp.UseOwinApp(owinApp =>
            {
                if (env.IsDevelopment())
                {
                    owinApp.UseErrorPage(new ErrorPageOptions()
                    {
                        ShowCookies = true,
                        ShowEnvironment = true,
                        ShowExceptionDetails = true,
                        ShowHeaders = true,
                        ShowQuery = true,
                        ShowSourceCode = true
                    });
                }
                // owinApp.UseFileServer(); as like as asp.net core static files middleware
                // owinApp.UseStaticFiles(); as like as asp.net core static files middleware
                // owinApp.UseWebApi(); asp.net web api / odata / web hooks

                HttpConfiguration webApiConfig = new HttpConfiguration();
                ODataModelBuilder odataMetadataBuilder = new ODataConventionModelBuilder();
                odataMetadataBuilder.EntitySet<Product>("Products");
                webApiConfig.MapODataServiceRoute(
                    routeName: "ODataRoute",
                    routePrefix: "odata",
                    model: odataMetadataBuilder.GetEdmModel());
                owinApp.UseWebApi(webApiConfig);

                owinApp.MapSignalR();

                //owinApp.Use<AddSampleHeaderToResponseHeadersOwinMiddleware>();
            });

            //aspNetCoreApp.UseMiddleware<AddSampleHeaderToResponseHeadersAspNetCoreMiddlware>();
        }

برای config مخصوص Odata، به HttpConfiguration نیاز داریم. بنابراین instanceی از آن گرفته و برای مسیریابی Odata از آن استفاده مینماییم.

با استفاده از پیاده سازی که از استاندارد Owin انجام دادیم، مشاهده کردید که Odata را همانند یک پروژه‌ی معمولی asp.netی، config نمودیم. در خط بعدی هم SignalR را مشاهده مینمایید.

اکنون اگر آدرس زیر را در مرورگر خود وارد نمایید، پاسخ زیر را از Odata دریافت خواهید کرد:

http://localhost:YourPort/odata/Products

بعد از فرستادن request فوق، باید response زیر را دریافت نمایید:

{ 
 "@odata.context":"http://localhost:4675/odata/$metadata#Products","value":[
    {
      "Id":1,"Name":"Test","Price":10
    }
  ]
}

تعداد زیادی Owin Middleware موجود همانند Thinktecture IdentityServer, NWebSec, Nancy, Facebook OAuth , ... هم با همان آموزش راه اندازی بر روی Owin که دارند میتوانند در ASP.NET Core نیز استفاده شوند و زمانی که نسخه‌ی ASP.NET Core اینها به آمادگی کامل رسید، با کمترین تغییری میتوان از آنها استفاده نمود.
اگر در پیاده سازی مقاله‌ی فوق با مشکل رو به رو شدید (اگر مراحل به ترتیب اجرا شوند، مشکلی نخواهید داشت) میتوانید از Github ، کل این پروژه را clone کرده و همچنین طبق commit‌های منظم از طریق history آن، مراحل جلو رفتن پروژه را دنبال نمایید.
مطالب
ایجاد جداول بهینه سازی شده برای حافظه در SQL Server 2014
پس از نگاهی به مفاهیم مقدماتی OLTP درون حافظه‌ای در SQL Server 2014، در ادامه به نحوه‌ی انجام تنظیمات خاص جداول بهینه سازی شده برای حافظه خواهیم پرداخت.


ایجاد یک بانک اطلاعاتی با پشتیبانی از جداول بهینه سازی شده برای حافظه

برای ایجاد جداول بهینه سازی شده برای حافظه، ابتدا نیاز است تا تنظیمات خاصی را به بانک اطلاعاتی آن اعمال کنیم. برای اینکار می‌توان یک بانک اطلاعاتی جدید را به همراه یک filestream filegroup ایجاد کرد که جهت جداول بهینه سازی شده برای حافظه، ضروری است؛ یا اینکه با تغییر یک بانک اطلاعاتی موجود و افزودن filegroup یاد شده نیز می‌توان به این مقصود رسید.
در اینگونه جداول خاص، اطلاعات در حافظه‌ی سیستم ذخیره می‌شوند و برخلاف جداول مبتنی بر دیسک سخت، صفحات اطلاعات وجود نداشته و نیازی نیست تا به کش بافر وارد شوند. برای مقاصد ذخیره سازی نهایی اطلاعات جداول بهینه سازی شده برای حافظه، موتور OLTP درون حافظه‌ای آن، فایل‌های خاصی را به نام checkpoint در یک filestream filegroup ایجاد می‌کند که از آن‌ها جهت ردیابی اطلاعات استفاده خواهد کرد و نحوی ذخیره سازی اطلاعات در آن‌ها از شیوه‌ی با کارآیی بالایی به نام append only mode پیروی می‌کند.
با توجه به متفاوت بودن نحوه‌ی ذخیره سازی نهایی اطلاعات اینگونه جداول و دسترسی به آن‌ها از طریق استریم‌ها، توصیه شده‌است که filestream filegroup‌های تهیه شده را در یک SSD یا Solid State Drive قرار دهید.

پس از اینکه بانک اطلاعاتی خود را به روش‌های معمول ایجاد کردید، به برگه‌ی خواص آن در management studio مراجعه کنید. سپس صفحه‌ی file groups را در آن انتخاب کرده و در پایین برگه‌ی آن، در قسمت جدید memory optimized data، بر روی دکمه‌ی Add کلیک کنید. سپس نام دلخواهی را وارد نمائید.


پس از ایجاد یک گروه فایل جدید، به صفحه‌ی files خواص بانک اطلاعاتی مراجعه کرده و بر روی دکمه‌ی Add کلیک کنید. سپس File type این ردیف اضافه شده را از نوع file stream data و file group آن‌را همان گروه فایلی که پیشتر ایجاد کردیم، تنظیم کنید. در ادامه logical name دلخواهی را وارد کرده و در آخر بر روی دکمه‌ی Ok کلیک کنید تا تنظیمات مورد نیاز جهت تعاریف جدول بهینه سازی شده برای حافظه به پایان برسد.


این مراحل را توسط دو دستور T-SQL ذیل نیز می‌توان سریعتر انجام داد:
USE [master]
GO
ALTER DATABASE [testdb2] 
      ADD FILEGROUP [InMemory_InMemory] CONTAINS MEMORY_OPTIMIZED_DATA 
GO
ALTER DATABASE [testdb2] 
      ADD FILE ( NAME = N'InMemory_InMemory', FILENAME = N'D:\SQL_Data\MSSQL11.MSSQLSERVER\MSSQL\DATA\InMemory_InMemory' ) 
      TO FILEGROUP [InMemory_InMemory]
GO

ساختار گروه فایل بهینه سازی شده برای حافظه

گروه فایل بهینه سازی شده برای حافظه، دارای چندین دربرگیرنده است که هر کدام چندین فایل را در خود جای خواهند داد:
- Root File که در برگیرنده‌ی متادیتای اطلاعات است.
- Data File که شامل ردیف‌های اطلاعات ثبت شده در جداول بهینه سازی شده‌ی برای حافظه هستند. این ردیف‌ها همواره به انتهای data file اضافه می‌شوند و دسترسی به آن‌ها ترتیبی است. کارآیی IO این روش نسبت به روش دسترسی اتفاقی به مراتب بالاتر است. حداکثر اندازه این فایل 128 مگابایت است و پس از آن یک فایل جدید ساخته می‌شود.
- Delta File شامل ردیف‌هایی است که حذف شده‌اند. به ازای هر ردیف، حداقل اطلاعاتی از آن را در خود ذخیره خواهد کرد؛ شامل ID ردیف حذف شده و شماره تراکنش آن. همانطور که پیشتر نیز ذکر شد، این موتور جدید درون حافظه‌ای، برای یافتن راه چاره‌ای جهت به حداقل رسانی قفل گذاری بر روی اطلاعات، چندین نگارش از ردیف‌ها را به همراه timestamp آن‌ها در خود ذخیره می‌کند. به این ترتیب، هر به روز رسانی به همراه یک حذف و سپس ثبت جدید است. به این ترتیب دیگر بانک اطلاعاتی نیازی نخواهد داشت تا به دنبال رکورد موجود برگردد و سپس اطلاعات آن‌را به روز نماید. این موتور جدید فقط اطلاعات به روز شده را در انتهای رکوردهای موجود با فرمت خود ثبت می‌کند.


ایجاد جداول بهینه سازی شده برای حافظه

پس از آماده سازی بانک اطلاعاتی خود و افزودن گروه فایل استریم جدیدی به آن برای ذخیره سازی اطلاعات جداول بهینه سازی شده برای حافظه، اکنون می‌توانیم اینگونه جداول خاص را در کنار سایر جداول متداول موجود، تعریف و استفاده نمائیم:
-- It is not a Memory Optimized
CREATE TABLE tblNormal
(
   [CustomerID] int NOT NULL PRIMARY KEY NONCLUSTERED, 
   [Name] nvarchar(250) NOT NULL,
   CustomerSince DATETIME not NULL
      INDEX [ICustomerSince] NONCLUSTERED
)

--  DURABILITY = SCHEMA_AND_DATA
CREATE TABLE tblMemoryOptimized_Schema_And_Data
(
    [CustomerID] INT NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA)


-- DURABILITY = SCHEMA_ONLY
CREATE TABLE tblMemoryOptimized_Schema_Only
(
    [CustomerID] INT NOT NULL 
PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    [Name] NVARCHAR(250) NOT NULL,
    [CustomerSince] DATETIME NOT NULL
INDEX [ICustomerSince] NONCLUSTERED
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)
در اینجا سه جدول را مشاهده می‌کنید که در بانک اطلاعاتی آماده شده در مرحله‌ی قبل، ایجاد خواهند شد. مورد اول یک جدول معمولی است که از آن برای مقایسه سرعت ثبت اطلاعات با سایر جداول ایجاد شده، استفاده خواهد شد.
همانطور که مشخص است، دو جدول بهینه سازی شده برای حافظه، همان سه ستون جدول معمولی مبتنی بر دیسک سخت را دارا هستند؛ اما با این تفاوت‌ها:
- دارای ویژگی MEMORY_OPTIMIZED = ON می‌باشند. به این ترتیب اینگونه جداول نسبت به جداول متداول مبتنی به دیسک سخت متمایز خواهند شد.
- دارای ویژگی DURABILITY بوده و توسط مقدار SCHEMA_AND_DATA آن مشخص می‌کنیم که آیا قرار است اطلاعات و ساختار جدول، ذخیره شوند یا تنها قرار است ساختار جدول ذخیره گردد (حالت SCHEMA_ONLY).
- بر روی ستون Id آن‌ها یک hash index ایجاد شده‌است که وجود آن ضروری است و در کل بیش از 8 ایندکس را نمی‌توان تعریف کرد.
برخلاف ایندکس‌های B-tree جداول مبتنی بر سخت دیسک، ایندکس‌های جداول بهینه سازی شده برای حافظه، اطلاعات را تکرار نمی‌کنند. این‌ها صرفا اشاره‌گرهایی هستند به ردیف‌های اصلی اطلاعات. به این معنا که این ایندکس‌ها لاگ نشده و همچنین بر روی سخت دیسک ذخیره نمی‌شوند. کار بازسازی مجدد آن‌ها در اولین بار بازیابی بانک اطلاعاتی و آغاز آن به صورت خودکار انجام می‌شود. به همین جهت مباحثی مانند index fragmentation و نگهداری ایندکس‌ها دیگر در اینجا معنا پیدا نمی‌کنند.
دو نوع ایندکس را در اینجا می‌توان تعریف کرد. اولین آن‌ها hash index است و دومین آن‌ها range index. هش ایندکس‌ها برای حالاتی که در کوئری‌ها از عملگر تساوی استفاده می‌شود بسیار مناسب هستند. برای عملگرهای مقایسه‌ای از ایندکس‌های بازه‌ای استفاده می‌شود.
همچنین باید دقت داشت که پس از ایجاد ایندکس‌ها، دیگر امکان تغییر آن‌ها و یا تغییر ساختار جدول ایجاد شده نیست.
همچنین ایندکس‌های تعریف شده در جداول بهینه سازی شده برای حافظه، تنها بر روی ستون‌هایی غیرنال پذیر از نوع BIN2 collation مانند int و datetime قابل تعریف هستند. برای مثال اگر سعی کنیم بر روی ستون Name ایندکسی را تعریف کنیم، به این خطا خواهیم رسید:
 Indexes on character columns that do not use a *_BIN2 collation are not supported with indexes on memory optimized tables.
- در حین تعریف هش ایندکس‌ها، مقدار BUCKET_COUNT نیز باید تنظیم شود. هر bucket توسط مقداری که حاصل هش کردن یک ستون است مشخص می‌شود. کلیدهای منحصربفرد دارای هش‌های یکسان در bucketهای یکسانی ذخیره می‌شوند. به همین جهت توصیه شده‌است که حداقل مقدار bucket تعیین شده در اینجا مساوی یا بیشتر از مقدار تعداد کلیدهای منحصربفرد یک جدول باشد؛ مقدار پیش فرض 2 برابر توسط مایکروسافت توصیه شده‌است.
- نوع‌های قابل تعریف ستون‌ها نیز در اینجا به موارد ذیل محدود هستند و جمع طول آن‌ها از 8060 نباید بیشتر شود:
 bit, tinyint, smallint, int, bigint, money, smallmoney, float, real, datetime, smalldatetime, datetime2,
date, time, numberic, decimal, char(n),  varchar(n) ,nchar(n),  nvarchar(n), sysname, binary(n),
varbinary(n), and Uniqueidentifier


همچنین در management studio، گزینه‌ی جدید new -> memory optimized table نیز اضافه شده‌است و انتخاب آن سبب می‌شود تا قالب T-SQL ایی برای تهیه این نوع جداول، به صورت خودکار تولید گردد.


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


ثبت اطلاعات در جداول معمولی و بهینه سازی شده برای حافظه و مقایسه کارآیی آن‌ها

در مثال زیر، 100 هزار رکورد را در سه جدولی که پیشتر ایجاد کردیم، ثبت کرده و سپس مدت زمان اجرای هر کدام از مجموعه عملیات را بر حسب میلی ثانیه بررسی می‌کنیم:
set statistics time off
SET STATISTICS IO Off
set nocount on
go
-----------------------------

Print 'insert into tblNormal'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblNormal values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end

Print DATEDIFF(ms,@start,getdate());
go
-----------------------------

Print 'insert into tblMemoryOptimized_Schema_And_Data'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblMemoryOptimized_Schema_And_Data values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end
Print DATEDIFF(ms,@start,getdate());
Go
-----------------------------

Print 'insert into tblMemoryOptimized_Schema_Only'

DECLARE @start datetime = getdate()
declare @insertCount int = 100000
declare @startId int = 1
declare @customerID int = @startId

while @customerID < @startId + @insertCount
begin
    insert into tblMemoryOptimized_Schema_Only values (@customerID, 'Test', '2013-01-01T00:00:00')
    set @customerID +=1
end
Print DATEDIFF(ms,@start,getdate());

Go
با این خروجی تقریبی که بر اساس توانمندی‌های سخت افزاری سیستم می‌تواند متفاوت باشد:
 insert into tblNormal
36423

insert into tblMemoryOptimized_Schema_And_Data
30516

insert into tblMemoryOptimized_Schema_Only
3176
و برای حالت select خواهیم داشت:
 set nocount on
print 'tblNormal'
set statistics time on
select count(CustomerID) from tblNormal
set statistics time off
go
print 'tblMemoryOptimized_Schema_And_Data'
set statistics time on
select count(CustomerID) from tblMemoryOptimized_Schema_And_Data
set statistics time off
go
print 'tblMemoryOptimized_Schema_Only'
set statistics time on
select count(CustomerID) from tblMemoryOptimized_Schema_Only
set statistics time off
go
با این خروجی
 tblNormal
 SQL Server Execution Times:
CPU time = 46 ms,  elapsed time = 52 ms.

tblMemoryOptimized_Schema_And_Data
 SQL Server Execution Times:
CPU time = 32 ms,  elapsed time = 33 ms.

tblMemoryOptimized_Schema_Only
 SQL Server Execution Times:
CPU time = 31 ms,  elapsed time = 30 ms.
تاثیر جداول بهینه سازی شده برای حافظه را در 350K inserts بهتر می‌توان با نمونه‌های متداول مبتنی بر دیسک مقایسه کرد.


برای مطالعه بیشتر

Getting started with SQL Server 2014 In-Memory OLTP
Introduction to SQL Server 2014 CTP1 Memory-Optimized Tables
Overcoming storage speed limitations with Memory-Optimized Tables for SQL Server
Memory-optimized Table – Day 1 Test
Memory-Optimized Tables – Insert Test
Memory Optimized Table – Insert Test …Again
مطالب
نکاتی توصیه ای برای برنامه نویسی اندروید : قسمت دوم
در ادامه‌ی قسمت اول، ده مورد دیگر از نکات کاربردی را بیان می‌کنیم.

  یازده. در جاوا رویدادها با استفاده از اینترفیس‌ها پیاده سازی می‌شوند. برای نامگذاری یک رویداد، قاعده آن در جاوا بدین شکل است که نام‌ها به صورت (+ ) Camel نوشته شده و آخرین عبارت هم Listener باشد و نیازی هم به حرف I در نامگذاری اینترفیس نیست؛ چون همه می‌دانند که این Listener آخری یعنی رویدادی که با اینترفیس پیاده سازی شده است و استفاده از I بی معنی است. هر چند بر خلاف دات نت، در اینجا استفاده از قاعده I چندان متداول نیست.
 public interface CopyFileListener
    {
        void PublishProgress(long fileSize,long copiedSize);
    }

دوازده. گوگل اینترفیس‌هایی را که برای رویدادها میسازد، داخل کلاس اصلی تعریف میکند. پس بهتر هست که شما هم همین روند را ادامه بدید و از این قاعده خارج نشوید. اگر خوب دقت کرده باشید، در برنامه نویسی اندروید تمام اینترفیس‌ها داخل کلاس اصلی هستند:
 textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });
در کد بالا رویداد OnclickListener در خود کلاس View تعریف شده است. پس ما هم بهتر هست همین کار را بکنیم:
public class MemoryWare {

    public interface CopyFileListener
    {
        void PublishProgress(long fileSize,long copiedSize);
    }
....
}
یک نکته دیگر اینکه موقعی که یک رویداد را به یک پراپرتی set انتساب می‌دهیم، رسم این است که نام آن پراپرتی با عبارت SetOn آغاز شود و با نام اینترفیس پایان یابد:
SetOnClickListener
البته اگر کلاس شما لیستی از رویدادها را درست میکند بهتر است از عبارت Add به جای SetOn استفاده کنید و برای آن یک Remove هم قرار دهید. نمونه آن را می‌توانید در کد زیر ببینید:
 editText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {

            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

سیزده
. آداپتورها و آداپتور ویوها (چون لیست) قسمت مهمی از برنامه‌های اندرویدی به شمار می‌آیند؛ تا حدی که در بیشتر برنامه‌های ساده هم حضور پررنگی دارند. ولی برای استفاده از این آداپتورها باید بدانید که نحوه کار آن‌ها چگونه است. بسیاری از کاربران در این قسمت اشتباهات زیادی می‌کنند. اگر در stackOverflow هم در اینباره نگاه کنید، با حجم انبوهی از سوالات روبرو می‌شوید و فقط به خاطر اینکه نحوه کارکرد آن را نمی‌دانند، به مشکل برخورده‌اند.
کلاس BaseAdapter اصلی‌ترین کلاس آداپتور هاست که بقیه از آن مشتق شده‌اند و معروفترین مشتقات آن، کلاس‌های CursorAdapter و ArrayAdapter هستند که امکانات بیس آداپتور را افزایش داده‌اند.به عنوان مثال در کد پایین از ArrayAdapter استفاده شده است.


نحوه کار یک آداپتور بدین صورت است که متدی را به نام GetView با قابلیت override دارد که با هر تعداد آیتم موجود صدا زده می‌شود. ولی اگر تصور کنیم فقط چند صدهزار آیتم هم داشته باشیم، آیا واقعا اجرا می‌شود؟ جواب این سوال این است که با هر بار اسکرولی که شما میکنید آیتم‌های بعدی ایجاد می‌شوند ولی باز این سوال پیش می‌آید که هر آیتم برای خود جداگانه تشکیل می‌شود؟ مطمئنا جواب خیر است. آداپتورها از سیستمی به نام ViewRecycler برای کش کردن آیتم‌های ایجاد شده استفاده می‌کنند و با هر اسکرولی که انجام می‌شود آیتم‌های بعدی از روی آیتم‌های قبلی که قبلا از صفحه خارج شده‌اند، ساخته می‌شوند و آیتم‌های کش شده قبلی را با پارامتری با نام convertView به دست شما می‌رساند.
کد زیر را ببینید:
  @Override
    public View getView(int position, View rowView, ViewGroup parent) {

        ViewHolder viewHolder=null;
        if(rowView==null)
        {
            // 1. Create inflater
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            // 2. Get rowView from inflater
            rowView = inflater.inflate(R.layout.row_bank_group_list, parent, false);
            viewHolder=new ViewHolder();
            viewHolder.txtGroupName=(TextView) rowView.findViewById(R.id.text_groupName);
            rowView.setTag(viewHolder);
        }
        else
        {
            viewHolder=(ViewHolder)rowView.getTag();
        }
        viewHolder.txtGroupName.setText(getItem(position).getName());
        viewHolder.txtGroupName.setTypeface(new FontSystem().get_General_Font(context));
        viewHolder.txtGroupName.setTextColor(context.getResources().getColor(R.color.black));

        return rowView;
    }
کد بالا ابتدا بررسی میکند که آیا convertView نال است یا خیر. اگر نال بود به این معنا است که کش برای شما چیزی نداشته است و باید آیتم جدیدی را بسازید. پس اشیاء موجود در آن را در حافظه می‌آورید و مقداردهی می‌کنید. ولی اگر برابر نال نباشد، یعنی کش حاوی یک سری آیتم است که قبلا ایجاد شده‌اند. پس نیاز به inflate کردن مجدد ندارد و میتوانید  همان را مستقیما مورد استفاده قرار دهید و با مقادیر جدید آن را ست کنید.
کلاس داخلی ViewHolder هم یک الگو برای عدم بررسی Viewهای داخل آن است که نیازی به یافتن و تبدیل مجدد آن‌ها نداشته باشید. در این روش شیء، داخل خصوصیت tag آیتم قرار گرفته است و وقتی از کش برداشته شود، خاصیت تگ آن را می‌خوانیم و مستقیما مورد استفاده قرار می‌دهیم. در این حالت شما بهترین استفاده را از پردازش‌ها و حافظه، می‌کنید.

چهارده. یکی از کارهایی را که قبل از کار کردن در یک مسیر فیزیکی باید انجام دهید این است که مطمئن باشید اجازه نوشتن در آن ناحیه را دارید یا خیر. در غیر اینصورت برنامه شما با خطای FC روبرو می‌شود و اجرای آن خاتمه می‌یابد. به همین دلیل اکثر برنامه نویسان از متد CanWrite در کلاس File استفاده می‌کنند. ولی در هنگام استفاده از این متد باید دقت داشته باشید که کلاس File فقط باید حاوی مسیر باشد و اسمی از فایل مربوطه در آن نباشد. دلیل هم آن است که این احتمال می‌رود اگر فایلی هم وجود نداشته باشد، مقدار false را به شما برگرداند. مثال زیر قرار است فایلی را در کارت حافظه بنویسید، ولی بررسی اجازه نوشتن در مسیر، اشتباه است:
File file=new File(sdcardPath,fileName);

if(file.CanWrite())
{
  .....
}
کد بالا را به طور صحیح بازنویسی می‌کنیم:
File file=new File(sdcardPath);

if(file.CanWrite())
{
file=new File(sdcardPath,filePath);
  .....
}
اگر هنگام تست این کد مشکلی نداشتید، دلیل بر صحت کد نیست. بلکه بنابر تجربه شخصی من، زمانی این مشکل پیش آمده بود که روی چند گوشی تست شده و بعدها مشکل در گوشی پیش آمده بود که هم مدل و دقیقا مشابه یکی از گوشی‌های تستی بود.

پانزده. کارت حافظه خارجی: همه برنامه نویسان اندروید حداقل یکبار از کد زیر استفاده کرده اند:
Environment.getExternalStorageDirectory()
بررسی‌ها در استک اورفلو نشان می‌دهد که برنامه نویسان، گزارش عملکرد اشتباهی را از این متد دارند. ولی حقیقت این است که این متد به هیچ عنوان مقدار اشتباهی را بر نمی‌گرداند. بلکه منطق آن متفاوت از چیزی است که شما فکر می‌کنید. وقتی ما صحبت از حافظه خارجی برای یک گوشی میکنیم، منظور همان کارت حافظه‌ای است که به طور جداگانه داخل گوشی قرار داده‌ایم و انتظار داریم متد بالا آدرس و مدخل همین کارت را برای ما فراهم کند. ولی در کمال تعجب می‌بینیم که آدرس حافظه داخلی برگردانده می‌شود. پس باید ببینیم اندروید از  آن چه انتظاری دارد؟
هر برنامه‌ای که در اندروید نصب می‌شود در مسیر
/Data/Data
یک دایرکتوری با نام خود دارد مثل:
/Data/Data/Info.Dotnettips.MyApp
این دایرکتوری تنها متعلق به این برنامه بوده و کسی جز Root به آن دسترسی ندارد. اندروید این دایرکتوری را به عنوان حافظه داخلی در نظر میگیرد که برای کار با آن نیاز به هیچ آدرس دهی نیست. ولی در کنار این دایرکتوری حافظه داخلی خود گوشی که در آن انبوه فایل‌های خود را ذخیره می‌کنید هم هست که اندروید آن را حافظه خارجی می‌پندارد. حال آن حافظه‌ای را که شما جداگانه به صورت یک کارت یا USB OTG متصل میکنید، به عنوان حافظه خارجی در نظر نمیگیرد. در صورتی که شما چنین انتظاری را دارید، برای حل این مشکل بهتر است از کدهای موجود مثل کد زیر استفاده کنید:
    /**
     * it will returns sd path for you
     *  <p>
     *  <b>Required Permission: </b>android.permission.READ_EXTERNAL_STORAGE<br/>
     * </p>
     * @return
     */
    public  List<String> GetExternalMounts() {
        final List<String> out = new ArrayList<>();
        String reg = "(?i).*vold.*(vfat|ntfs|exfat|fat32|ext3|ext4).*rw.*";
        String s = "";
        try {
            final Process process = new ProcessBuilder().command("mount")
                    .redirectErrorStream(true).start();
            process.waitFor();
            final InputStream is = process.getInputStream();
            final byte[] buffer = new byte[1024];
            while (is.read(buffer) != -1) {
                s = s + new String(buffer);
            }
            is.close();
        } catch (final Exception e) {
            e.printStackTrace();
        }

        // parse output
        final String[] lines = s.split("\n");
        for (String line : lines) {
            if (!line.toLowerCase(Locale.US).contains("asec")) {
                if (line.matches(reg)) {
                    String[] parts = line.split(" ");
                    for (String part : parts) {
                        if (part.startsWith("/"))
                            if (!part.toLowerCase(Locale.US).contains("vold"))
                                if(new File(part).canWrite())
                                    out.add(part);
                    }
                }
            }
        }
        return out;
    }

شانزده.
یکی از روش‌های انتقال اطلاعات بین اکتیویتی‌ها مختلف استفاده از Extras هاست که شما با تعیین یک نام یا کلید، اطلاعات مربوطه را ارسال و توسط همان کلید؛ اطلاعات را در اکتیویتی مقصد دریافت میکنید:
notesIntent.putExtra("PartyId", PartyId);
startActivity(notesIntent);
و در مقصد:
PartyId=getIntent().getLongExtra("PartyId",0);
در این حالت بهتر است با این رشته‌ها نیز همانند مورد شماره دو در قسمت اول رفتار شود تا نیازی به نوشتن و تکرار این نام‌ها نباشد. ولی با یک نگاه به استانداردهای نوشته شده در خود اندروید و بسیاری از کتابخانه‌های ثالث در می‌یابیم که بهترین روش این است که این کلید‌ها را به صورت متغیرهای ایستا در خود اکتیویتی ذخیره کنیم؛ در این حالت هر کلید در جایگاه واقعی خودش قرار گرفته است. نمونه‌ای از این استفاده را می‌توانید در کتابخانه FilePicker مشاهده کنید:
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, false);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_FILE);

هفده.
قواعد نامگذاری: برای نامگذاری متغیرها از قانون CamelCase استفاده میکنیم. ولی برای حالات زیر از روش‌های دیگر استفاده می‌شود:
  • برای ثابت‌ها از حروف بزرگ و _ استفاده کنید.
  • برای متغیرهای خصوصی از حرف m در ابتدای نام متغیر استفاده کنید.
  • برای متغیرهای استاتیک از حرف s در ابتدای نام متغیر استفاده کنید.
نمونه ای از این نامگذاری که توسط مستندات گوگل سفارش شده است:
public class MyClass {
    public static final int SOME_CONSTANT = 42;
    public int publicField;
    private static MyClass sSingleton;
    int mPackagePrivate;
    private int mPrivate;
    protected int mProtected;
}

هجده:
قاعده نظم و ترتیب در import‌ها توسط مستندات گوگل بدین شکل تعریف شده است:
  1. نام پکیج‌های ارائه شده توسط گوگل
  2. نام پکیج‌های ثالث
  3. نام پکیج‌های موجود در java و javax
  4. پکیج‌های موجود در پکیج اصلی
البته رعایت این قاعده کمی سخت و عموما بدون اجراست ولی نگران نباشید. از آنجایی که ادیتور از طرف jet brains ارائه شده‌است و عمل import به طور خودکار صورت میگیرد و با ابزارهای دیگری که در دل این ادیتور قرار گرفته است، این عمل به طور خودکار انجام می‌گیرد.

نوزدهم. مرتب سازی متدهای دسترسی یک کلاس: بسیار خوب است که همیشه کدهای ما نظم خاصی را داشته باشد تا پیگیری‌های شخصی و تیمی در آن راحت‌تر صورت بگیرد. برای مثال در یک کلاس ابتدا متدهای public و سپس private قرار گیرند و الی آخر.
الگوی عمومی که برای کار با جاوا صورت گرفته است به شکل زیر می‌باشد:
public, protected, private,abstract, static, transient, volatile, synchronized, final, native.
البته این متدهای دسترسی بسته به فیلد بودن و متد بودن نیز تغییر میکند. به عنوان مثال ابتدا فیلدها در نظر گرفته می‌شوند و سپس متدها و ...
ادیتور intelij شامل تنظمیاتی برای مرتب سازی کدهاست که در این مورد بسیار سودمند است. با طی کردن مسیر زیر می‌توانید برای آن ترتیب اینگونه موارد را مشخص کنید.
Settings>Editor>Code Style>Arrangement
در اینجا شما امکان تعاریف این ترتیب‌ها را دارید. البته بهتر هست در حالت پیش فرض باشد تا حالتی عمومی بین افراد مختلف برقرار باشد.

در تصویر بالا متدها به ترتیب متدهای دستری بین بلوک‌های کامنت method start و method end قرار گرفته اند.

 همچنین شامل گزینه‌های دیگری نیز میباشد که به نظرم فعال کردنشان بسیار خوب است. گزینه keep overridden methods together به شما کمک می‌کند تا متدهایی را که رونویسی می‌شوند، در کنار یکدیگر قرار بگیرند که برای کلاس‌های اندرویدی مثل اکتیویتی‌ها و فرگمنت‌ها و ... بسیار خوب است. گزینه مفید دیگر Keep dependent methods together است که در دو حالت عمقی یا خطی متدهای وابسته (متدهایی که متدهای دیگر را در آن کلاس صدا میزنند) در کنار یکدیگر قرار میدهد و مابقی گزینه‌ها، که بسیار سودمند هست. به هر حال هر قاعده‌ای را که برای خود انتخاب میکنید اگر در حالت پیش فرض نیست بهتر است در مستندات پروژه ذکر شود تا افراد دیگر سریعتر به موضوع پی ببرند.

قسمت بیستم. این مورد برای افراد تازه کار می‌باشد که تازه اندروید استادیو را باز کرده‌اند و مشغول کدنویسی می‌باشند. یکی از مواردی که در همان مرحله اول به آن برمیخورید این است که intellisense  ادیتور به بزرگی و کوچکی حروف حساس است و تنها با حرف اول سازگاری دارد. برای تغییر این مسئله باید مسیر زیر را طی کنید:
Settings>Editor>Completion>Case-sensitive Completion>None
حالا با تایپ هر کلمه، دستورات و آبجکت‌هایی را که شامل آن کلمات باشند، به شما نمایش داده خواهند شد.

نظرات مطالب
BloggerToCHM 1.5
اول باید با نحوه‌ی تولید و اجزای یک فایل CHM آشنا شوید: اینجا
بعد فایل hhp نهایی را به hhc.exe ارسال کنید برای کامپایل:
new Process
 {  StartInfo =
               {
                   FileName = Path.Combine(installPath, "hhc.exe"),
                   Arguments = ("\"" + path + "\\blog.hhp\""),
                   CreateNoWindow = true,
                   WorkingDirectory = blogDir,
                   UseShellExecute = false,
                   RedirectStandardOutput = true,
                   RedirectStandardError = true
                }
 }.Start();
مطالب
ابزاری برای تغییر خودکار ارجاعات پروژه برای کار با SVN SourceControl
ما در شرکت برای Source Control از SVN استفاده می‌کنیم، مزایای سورس کنترل آنقدر واضح است که دیگه من اینجا چیزی ازش نمیگم
اما برای استفاده از سورس کنترل یک مشکلی وجود دارد، اگر شما تعدادی پروژه را به کاربران خاصی بدین و تعدادی رو ندین، اون کاربر وقتی پروژه‌ها را می‌گیره با مشکل ارجاعات پروژه‌ها مواجه است. چرا که برخی از پروژه‌های ارجاعی، روی کامپیوتر برنامه نویس 1 وجود نداره. برعکس ش هم همین طوره، چون اون کاربر، پروژه‌های ارجاعی رو نداره، باید به جاش به اسمبلی نهایی اون پروژه ارجاع بده. بنابراین وقتی مدیر پروژه‌ها رو می‌گیره، باز ارجاعات اشتباه هستند!
ما اینجا برای رفع این مشکل ابزاری درست کردیم، به اسم SolutionExplorer.
این ابزار فایل solution رو به همراه پوشه حاوی فایل‌های اسمبلی می‌گیره. اگر پروژه ای به اسمبلی ای ارجاع داده باشه که پروژه اش توی solution باشه، ارجاع به اسمبلی رو تبدیل میکنه به ارجاع به پروژه و برعکسش، اگر پروژه ای به پروژه دیگه ای ارجاع داده باشه که توی solution وجود نداشته باشه، توی پوشه اسمبلی ها، دنبال اسمبلی ای می‌گرده که اسمش شبیه اسم پروژه ارجاعی باشه و اگر پیدا کنه، ارجاع رو عوض می‌کنه
البته برای جلوگیری از به هم ریختگی، نرم افزار از فایل‌های پروژه ای که دستکاری می‌کنه، پشتیبان می‌گیره
توجه:
* این برنامه از تمامی جهات تست نشده است، با ریسک خودتون ازش استفاده کنید (ما تو شرکت دیگه ریسکی نداریم :) )
* سیستم نامگذاری اسمبلی‌ها و پروژه‌های ما ممکنه فرق کنه
* اگر به مشکلی برخوردید، لطفا زیر همین مطلب برام بنویسید
* انتخاب پوشه اسمبلی ها، الزامی نیست