- InvalidOperationException Writing is not allowed after writer was completed
- ICE on valid code after upgrading to 16.2.0
- CPU Usage Tool context menu does not navigate
- Wrong assembly when result of conversion operator is converted to reference to base class.
- DockerUpdateComposeVsGeneratedFiles throws "Value cannot be null" ("Parameter name: path1") after updating to Visual Studio 16.3
- CPU Usage Tool contect menu does not navigate.
- InvalidOperationException Writing is not allowed after writer was completed.
- Publish doesn't work in Visual Studio 16.3.1.
- This fixes an issue with the Snapshot Debugger where customers are using MSA accounts.
- Fixed an issue where customers trying to install Microsoft.Visualstuio.Shell.15.0 NuGet package received a warning message of Framework not found.
- Fixes MSVC compiler bug involving implicit conversion from a lambda to function pointer.
- Fixed issue with Visual Studio crashing due to null reference exception.
- A fix is made to address a compiler interal error when the code has a out-of-line definition of a static data member of a nested class inside a partial specialization.
GUID یا Globally unique identifier یک عدد صحیح 128 بیتی است (بنابراین 2 به توان 128 حالت را میتوان برای آن درنظر گرفت). از لحاظ آماری تولید دو GUID یکسان تقریبا صفر میباشد. به همین جهت از آن با اطمینان میتوان به عنوان یک شناسه منحصربفرد استفاده کرد. برای مثال اگر به لینکهای دانلود فایلها از سایت مایکروسافت دقت کنید، این نوع GUID ها را به وفور میتوانید ملاحظه نمائید. یا زمانیکه قرار است فایلی را که بر روی سرور آپلود شده، ذخیره نمائیم، میتوان نام آنرا یک GUID درنظر گرفت بدون اینکه نگران باشیم آیا فایل آپلود شده بر روی یکی از فایلهای موجود overwrite میشود یا خیر. یا مثلا استفاده از آن در سناریوی بازیابی کلمه عبور در یک سایت. هنگامیکه کاربری درخواست بازیابی کلمه عبور فراموش شده خود را داد، یک GUID برای آن تولید کرده و به او ایمیل میزنیم و در آخر آنرا در کوئری استرینگی دریافت کرده و با مقدار موجود در دیتابیس مقایسه میکنیم. مطمئن هستیم که این عبارت قابل حدس زدن نیست و همچنین یکتا است.
برای تولید GUID ها در دات نت میتوان مانند مثال زیر عمل کرد و خروجیهای دلخواهی را با فرمتهای مختلفی دریافت کرد:
System.Guid.NewGuid().ToString() = 81276701-9dd7-42e9-b128-81c762a172ff
System.Guid.NewGuid().ToString("N") = 489ecfc61ee7403988efe8546806c6a2
System.Guid.NewGuid().ToString("D") = 119201d9-84d9-4126-b93f-be6576eedbfd
System.Guid.NewGuid().ToString("B") = {fd508d4b-cbaf-4f1c-894c-810169b1d20c}
System.Guid.NewGuid().ToString("P") = (eee1fe00-7e63-4632-a290-516bfc457f42)
دو روش برای انجام اینکار وجود دارد
الف) عبارت دریافت شده را به new Guid پاس کنیم. اگر ورودی غیرمعتبر باشد، یک exception تولید خواهد شد.
ب) استفاده از regular expressions جهت بررسی الگوی عبارت وارد شده
پیاده سازی این دو را در کلاس زیر میتوان ملاحظه نمود:
using System;
using System.Text.RegularExpressions;
namespace sample
{
/// <summary>
/// بررسی اعتبار یک گوئید
/// </summary>
public static class CValidGUID
{
/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="guidString">ورودی</param>
/// <returns></returns>
public static bool IsGuid(this string guidString)
{
if (string.IsNullOrEmpty(guidString)) return false;
bool bResult;
try
{
Guid g = new Guid(guidString);
bResult = true;
}
catch
{
bResult = false;
}
return bResult;
}
/// <summary>
/// بررسی تعیین اعتبار ورودی
/// </summary>
/// <param name="input">ورودی</param>
/// <returns></returns>
public static bool IsValidGUID(this string input)
{
return !string.IsNullOrEmpty(input) &&
new Regex(@"^(\{{0,1}([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}\}{0,1})$").IsMatch(input);
}
}
}
using NUnit.Framework;
using sample;
namespace TestLibrary
{
[TestFixture]
public class TestCValidGUID
{
/*******************************************************************************/
[Test]
public void TestIsGuid1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsGuid());
}
[Test]
public void TestIsGuid2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsGuid());
}
[Test]
public void TestIsGuid3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsGuid());
}
[Test]
public void TestIsGuid4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsGuid());
}
[Test]
public void TestIsGuid5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsGuid());
}
/*******************************************************************************/
[Test]
public void TestIsValidGUID1()
{
Assert.IsTrue("81276701-9dd7-42e9-b128-81c762a172ff".IsValidGUID());
}
[Test]
public void TestIsValidGUID2()
{
Assert.IsTrue("489ecfc61ee7403988efe8546806c6a2".IsValidGUID());
}
[Test]
public void TestIsValidGUID3()
{
Assert.IsTrue("{fd508d4b-cbaf-4f1c-894c-810169b1d20c}".IsValidGUID());
}
[Test]
public void TestIsValidGUID4()
{
Assert.IsTrue("(eee1fe00-7e63-4632-a290-516bfc457f42)".IsValidGUID());
}
[Test]
public void TestIsValidGUID5()
{
Assert.IsFalse("81276701;9dd7;42e9-b128-81c762a172ff".IsValidGUID());
}
}
}
همانطور که ملاحظه میکنید حالت دوم یعنی استفاده از عبارات باقاعده دو حالت را نمیتواند بررسی کند (مطابق الگوی بکار گرفته شده که البته قابل اصلاح است)، اما روش معمولی استفاده از new Guid ، تمام فرمتهای تولید شده توسط دات نت را پوشش میدهد.
<PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> <MvcRazorCompileOnPublish>false</MvcRazorCompileOnPublish> </PropertyGroup>
String.format = function () { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { s = s.replace("{" + i + "}", arguments[i + 1]); } return s; };
String.format = function () { var s = arguments[0]; for (var arg in arguments) { var i = parseInt(arg); s = s.replace("{" + i + "}", arguments[i + 1]); } return s; };
console.log(String.format("{0} is nice!", "donettips.info"));
donettips.info is nice!
console.log(String.format("{0} is {1} nice! {0} is {1} nice!", "donettips.info", "very"));
donettips.info is very nice! {0} is {1} nice!
String.format = function () { var original = arguments[0], replaced; for (var i = 0; i < arguments.length - 1; i++) { replaced = ''; while (replaced != original) { original = replaced || original; replaced = original.replace("{" + i + "}", arguments[i + 1]); } } return replaced; };
donettips.info is very nice! donettips.info is very nice!
String.format = function () { var s = arguments[0]; for (var i = 0; i < arguments.length - 1; i++) { s = s.replace(new RegExp("\\{" + i + "\\}", "g"), arguments[i + 1]); } return s; };
String.format = function () { var s = arguments[0], i = arguments.length - 1; while (i--) { s = s.replace(new RegExp('\\{' + i + '\\}', 'g'), arguments[i + 1]); } return s; };
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "{2}", "two"));
zero:0 {2}:1 two:2
zero:0 two:1 two:2
console.log(String.format("{0}:0 {1}:1 {2}:2", "zero", "one", "{1}"));
zero:0 one:1 one:2
zero:0 one:1 {1}:2
String.format = function () { var args = arguments; return args[0].replace(/{(\d+)}/g, function (match, number) { return args[parseInt(number) + 1]; }); };
console.log(String.format("{0} is {1} nice!", "donettips.info"));
donettips.info is undefined nice!
String.format = function () { var s = arguments[0], args = arguments; return s.replace(/{(\d+)}/g, function (match, number) { var i = parseInt(number); return typeof args[i + 1] != 'undefined' ? args[i + 1] : match; }); };
console.log(String.format("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}} {{{{2}}}} {2}", "zero", "{2}", "two"));
zero:0 {2}:1 two:2, {zero} {{{2}}} {{{two}}} two
String.format = function () { var s = arguments[0], args = arguments; return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) { if (match == "{{") { return "{"; } if (match == "}}") { return "}"; } var i = parseInt(number); return typeof args[i + 1] != 'undefined' ? args[i + 1] : match; }); };
zero:0 {2}:1 two:2, {0} {{2}} {{2}} two
String.prototype.format = function () { ... }
String.prototype.format = function () { var s = this.toString(), args = arguments; return s.replace(/\{\{|\}\}|\{(\d+)\}/g, function (match, number) { if (match == "{{") { return "{"; } if (match == "}}") { return "}"; } return typeof args[number] != 'undefined' ? args[number] : match; }); };
console.log("{0}:0 {1}:1 {2}:2, {{0}} {{{1}}} {{{{2}}}} {2}".format("zero", "{2}", "two"));
String.format = function () { var s = arguments[0], args = arguments[1]; for (var arg in args) { s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]); } return s; };
String.prototype.format = function () { var s = this.toString(), args = arguments[0]; for (var arg in args) { s = s.replace(new RegExp("{" + arg + "}", "g"), args[arg]); } return s; };
console.log(String.format("{site} is {adj}! {site} is {adj}!", { site: "donettips.info", adj: "nice" })); console.log("{site} is {adj}! {site} is {adj}!".format({ site: "donettips.info", adj: "nice" }));
String.format = function String$format(format, args) { /// <summary locid="M:J#String.format" /> /// <param name="format" type="String"></param> /// <param name="args" parameterArray="true" mayBeNull="true"></param> /// <returns type="String"></returns> // var e = Function._validateParams(arguments, [ // { name: "format", type: String }, // { name: "args", mayBeNull: true, parameterArray: true } // ]); // if (e) throw e; return String._toFormattedString(false, arguments); }; String._toFormattedString = function String$_toFormattedString(useLocale, args) { var result = ''; var format = args[0]; for (var i = 0; ; ) { var open = format.indexOf('{', i); var close = format.indexOf('}', i); if ((open < 0) && (close < 0)) { result += format.slice(i); break; } if ((close > 0) && ((close < open) || (open < 0))) { if (format.charAt(close + 1) !== '}') { throw Error.argument('format', Sys.Res.stringFormatBraceMismatch); } result += format.slice(i, close + 1); i = close + 2; continue; } result += format.slice(i, open); i = open + 1; if (format.charAt(i) === '{') { result += '{'; i++; continue; } if (close < 0) throw Error.argument('format', Sys.Res.stringFormatBraceMismatch); var brace = format.substring(i, close); var colonIndex = brace.indexOf(':'); var argNumber = parseInt((colonIndex < 0) ? brace : brace.substring(0, colonIndex), 10) + 1; if (isNaN(argNumber)) throw Error.argument('format', Sys.Res.stringFormatInvalid); var argFormat = (colonIndex < 0) ? '' : brace.substring(colonIndex + 1); var arg = args[argNumber]; if (typeof (arg) === "undefined" || arg === null) { arg = ''; } if (arg.toFormattedString) { result += arg.toFormattedString(argFormat); } else if (useLocale && arg.localeFormat) { result += arg.localeFormat(argFormat); } else if (arg.format) { result += arg.format(argFormat); } else result += arg.toString(); i = close + 1; } return result; }
console.log(String.format("{0:n}, {0:c}, {0:p}, {0:d}", 100.0001)); // result: 100.00, ¤100.00, 10,000.01 %, 100.0001 console.log(String.format("{0:d}, {0:t}", new Date(2015, 1, 1, 10, 45))); // result: 02/01/2015, 10:45
var template = jQuery.validator.format("{0} is not a valid value"); console.log(template("abc")); // result: 'abc is not a valid value'
String.format([full format string], [arguments...]); // or: [date|number].format([partial format string]);
// Object path String.format("Welcome back, {username}!", { id: 3, username: "JohnDoe" }); // Result: "Welcome back, JohnDoe!" // Date/time formatting String.format("The time is now {0:t}.", new Date(2009, 5, 1, 13, 22)); // Result: "The time is now 01:22 PM." // Date/time formatting (without using a full format string) var d = new Date(); d.format("hh:mm:ss tt"); // Result: "02:28:06 PM" // Custom number format string String.format("Please call me at {0:+##0 (0) 000-00 00}.", 4601111111); // Result: "Please call me at +46 (0) 111-11 11." // Another custom number format string String.format("The last year result was {0:+$#,0.00;-$#,0.00;0}.", -5543.346); // Result: "The last year result was -$5,543.35." // Alignment String.format("|{0,10:PI=0.00}|", Math.PI); // Result: "| PI=3.14|" // Rounding String.format("1/3 ~ {0:0.00}", 1/3); // Result: "1/3 ~ 0.33" // Boolean values String.format("{0:true;;false}", 0); // Result: "false" // Explicitly specified localization // (note that you have to include the .js file for used cultures) msf.setCulture("en-US"); String.format("{0:#,0.0}", 3641.667); // Result: "3,641.7" msf.setCulture("sv-SE"); String.format("{0:#,0.0}", 3641.667); // Result: "3 641,7"
//inline arguments String.format("some string with {0} and {1} injected using argument {{number}}", 'first value', 'second value'); //returns: 'some string with first value and second value injected argument {number}' //single array String.format("some string with {0} and {1} injected using array {{number}}", [ 'first value', 'second value' ]); //returns: 'some string with first value and second value injected using array {number}' //single object String.format("some string with {first} and {second} value injected using {{propertyName}}",{first:'first value',second:'second value'}); //returns: 'some string with first value and second value injected using {propertyName}'
نام این جدول را با درنظر گرفتن شرایط موجود میتوان Resources گذاشت.
ستون Name برای ذخیره نام منبع درنظر گرفته شده است. این نام برابر نام منابع درخواستی در سیستم مدیریت منابع ASP.NET است که درواقع برابر همان نام فایل منبع اما بدون پسوند resx. است.
ستون Key برای نگهداری کلید ورودی منبع استفاده میشود که دقیقا برابر همان مقداری است که درون فایلهای resx. ذخیره میشود.
ستون Culture برای ذخیره کالچر ورودی منبع به کار میرود. این مقدار میتواند برای کالچر پیشفرض برنامه برابر رشته خالی باشد.
ستون Value نیز برای نگهداری مقدار ورودی منبع استفاده میشود.
برای ستون Id میتوان از GUID نیز استفاده کرد. در اینجا برای راحتی کار از نوع داده bigint و خاصیت Identity برای تولید خودکار آن در Sql Server استفاده شده است.
نکته: برای امنیت بیشتر میتوان یک Unique Constraint بر روی سه فیلد Name و Key و Culture اعمال کرد.
برای نمونه به تصویر زیر که ذخیره تعدای ورودی منبع را درون جدول Resources نمایش میدهد دقت کنید:
اصلاح کلاس DbResourceProviderFactory
برای ذخیره منابع محلی، جهت اطمینان از یکسان بودن نام منبع، متد مربوطه در کلاس DbResourceProviderFactory باید بهصورت زیر تغییر کند:
public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { if (!string.IsNullOrEmpty(virtualPath)) { virtualPath = virtualPath.Remove(0, virtualPath.IndexOf('/') + 1); // removes everything from start to the first '/' } return new LocalDbResourceProvider(virtualPath); }
ارتباط با دیتابیس
خوشبختانه برای تبادل اطلاعات با جدول بالا امروزه راههای زیادی وجود دارد. برای پیادهسازی آن مثلا میتوان از یک اینترفیس استفاده کرد. سپس با استفاده از سازوکارهای موجود مثلا بهکارگیری IoC، نمونه مناسبی از پیادهسازی اینترفیس مذبور را در اختیار برنامه قرار داد.
اما برای جلوگیری از پیچیدگی بیش از حد و دور شدن از مبحث اصلی، برای پیادهسازی فعلی از EF Code First به صورت مستقیم در پروژه استفاده شده است که سری آموزشی کاملی از آن در همین سایت وجود دارد.
پس از پیادهسازی کلاسهای مرتبط برای استفاده از EF Code First، از کلاس ResourceData که در بخش اول نیز نشان داده شده بود، برای کپسوله کردن ارتباط با دادهها استفاده میشود که نمونهای ابتدایی از آن در زیر آورده شده است:
using System.Collections.Generic; using System.Linq; using DbResourceProvider.Models; namespace DbResourceProvider.Data { public class ResourceData { private readonly string _resourceName; public ResourceData(string resourceName) { _resourceName = resourceName; } public Resource GetResource(string resourceKey, string culture) { using (var data = new TestContext()) { return data.Resources.SingleOrDefault(r => r.Name == _resourceName && r.Key == resourceKey && r.Culture == culture); } } public List<Resource> GetResources(string culture) { using (var data = new TestContext()) { return data.Resources.Where(r => r.Name == _resourceName && r.Culture == culture).ToList(); } } } }
using System.Collections.Generic; using System.Globalization; using DbResourceProvider.Data; namespace DbResourceProvider { public class DbResourceManager { private readonly string _resourceName; private readonly Dictionary<string, Dictionary<string, object>> _resourceCacheByCulture; public DbResourceManager(string resourceName) { _resourceName = resourceName; _resourceCacheByCulture = new Dictionary<string, Dictionary<string, object>>(); } public object GetObject(string resourceKey, CultureInfo culture) { return GetCachedObject(resourceKey, culture.Name); } private object GetCachedObject(string resourceKey, string cultureName) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResource = _resourceCacheByCulture[cultureName]; lock (this) { if (!cachedResource.ContainsKey(resourceKey)) { var data = new ResourceData(_resourceName); var dbResource = data.GetResource(resourceKey, cultureName); if (dbResource == null) return null; var cachedResources = _resourceCacheByCulture[cultureName]; cachedResources.Add(dbResource.Key, dbResource.Value); } } return cachedResource[resourceKey]; } } }
private object GetCachedObject(string resourceKey, string cultureName) { lock (this) { if (!_resourceCacheByCulture.ContainsKey(cultureName)) { _resourceCacheByCulture.Add(cultureName, new Dictionary<string, object>()); var cachedResources = _resourceCacheByCulture[cultureName]; var data = new ResourceData(_resourceName); var dbResources = data.GetResources(cultureName); foreach (var dbResource in dbResources) { cachedResources.Add(dbResource.Key, dbResource.Value); } } } var cachedResource = _resourceCacheByCulture[cultureName]; return !cachedResource.ContainsKey(resourceKey) ? null : cachedResource[resourceKey]; }
using System.Collections; using System.Collections.Generic; using System.Globalization; namespace DbResourceProvider { public class CultureFallbackProvider : IEnumerable<CultureInfo> { private readonly CultureInfo _startingCulture; private readonly CultureInfo _neutralCulture; private readonly bool _tryParentCulture; public CultureFallbackProvider(CultureInfo startingCulture = null, CultureInfo neutralCulture = null, bool tryParentCulture = true) { _startingCulture = startingCulture ?? CultureInfo.CurrentUICulture; _neutralCulture = neutralCulture; _tryParentCulture = tryParentCulture; } #region Implementation of IEnumerable<CultureInfo> public IEnumerator<CultureInfo> GetEnumerator() { var reachedNeutralCulture = false; var currentCulture = _startingCulture; do { if (_neutralCulture != null && currentCulture.Name == _neutralCulture.Name) { yield return CultureInfo.InvariantCulture; reachedNeutralCulture = true; break; } yield return currentCulture; currentCulture = currentCulture.Parent; } while (_tryParentCulture && !HasInvariantCultureName(currentCulture)); if (!_tryParentCulture || HasInvariantCultureName(_startingCulture) || reachedNeutralCulture) yield break; yield return CultureInfo.InvariantCulture; } #endregion #region Implementation of IEnumerable IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion private bool HasInvariantCultureName(CultureInfo culture) { return culture.Name == CultureInfo.InvariantCulture.Name; } } }
public object GetObject(string resourceKey, CultureInfo culture) { foreach (var currentCulture in new CultureFallbackProvider(culture)) { var value = GetCachedObject(resourceKey, currentCulture.Name); if (value != null) return value; } throw new KeyNotFoundException("The specified 'resourceKey' not found."); }
ابتدا تنظیمات موردنیاز فایل کانفیگ را که در قسمت قبل نشان داده شد، در برنامه خود اعمال کنید.
دادههای نمونه نشان داده شده در ابتدای این مطلب را درنظر بگیرید. حال اگر در یک برنامه وب اپلیکیشن، صفحه Default.aspx در ریشه سایت حاوی دو کنترل زیر باشد:
<asp:Label ID="Label1" runat="server" meta:resourcekey="Label1" /> <asp:Label ID="Label2" runat="server" meta:resourcekey="Label2" />
سپس تغییر زیر را در فایل web.config اعمال کنید تا کالچر UI سایت به fa تغییر یابد (به بخش "uiCulture="fa دقت کنید):
<globalization uiCulture="fa" resourceProviderFactoryType = "DbResourceProvider.DbResourceProviderFactory, DbResourceProvider" />
میبینید که با توجه به عدم وجود مقداری برای Label2.Text برای کالچر fa، عملیات fallback اتفاق افتاده است.
کار تولید یک پرووایدر منابع سفارشی دیتابیسی به اتمام رسید. تا اینجا اصول کلی تولید یک پرووایدر سفارشی شرح داده شد. بدین ترتیب میتوان برای هر حالت خاص دیگری نیز پرووایدرهای سفارشی مخصوص ساخت تا مدیریت منابع به آسانی تحت کنترل برنامه نویس قرار گیرد.
اما نکتهای را که باید به آن توجه کنید این است که در پیادهسازیهای نشان داده شده با توجه به نحوه کششدن مقادیر ورودیها، اگر این مقادیر در دیتابیس تغییر کنند، تا زمانیکه سایت ریست نشود این تغییرات در برنامه اعمال نخواهد شد. زیرا همانطور که اشاره شد، مدیریت نمونههای تولیدشده از پرووایدرهای منابع برای هر منبع درخواستی درنهایت برعهده ASP.NET است. بنابراین باید مکانیزمی پیاده شود تا کلاس DbResourceManager از بهروزرسانی ورودیهای کششده اطلاع یابد تا آنها را ریفرش کند.
در ادامه درباره روشهای مختلف نحوه پیادهسازی قابلیت بهروزرسانی ورودیهای منابع در زمان اجرا با استفاده از پرووایدرهای منابع سفارشی بحث خواهد شد. همچنین راهحلهای مختلف استفاده از این پرووایدرهای سفارشی در جاهای مختلف پروژههای MVC شرح داده میشود.
البته مباحث پیشرفتهتری چون تزریق وابستگی برای پیادهسازی لایه ارتباط با دیتابیس در بیرون و یا تولید یک Factory برای تزریق کامل پرووایدر منابع از بیرون نیز جای بحث و بررسی دارد.
منابع
http://msdn.microsoft.com/en-us/library/aa905797.aspx
http://msdn.microsoft.com/en-us/library/system.web.compilation.resourceproviderfactory.aspx
http://www.dotnetframework.org/default.aspx/.../ResourceFallbackManager@cs
http://www.codeproject.com/Articles/14190/ASP-NET-2-0-Custom-SQL-Server-ResourceProvider
چرا LINQ؟
اجزای سازندهی LINQ
- Elements عناصر
- Sequences توالیها
int[] fibonacci = {0, 1, 1, 2, 3, 5};
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; int numberOfElements = fibonacci.Count(); Console.WriteLine($"{numberOfElements}"); IEnumerable<int> distinictNumbers = fibonacci.Distinct(); Console.WriteLine("Elements in output sequence:"); foreach (var number in distinictNumbers) { Console.WriteLine(number); }
6 Elements in output sequence: 0 1 2 3 5
مفهوم Deffer Execution ( اجرای به تاخیر افتاده )
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; // ایجاد پرس و جو IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2); // در این مرحله پرس و جو ایجاد شده ولی هنوز اجرا نشده است // تغییر عنصر اول توالی fibonacci[0] = 99; // حرکت بر روی عناصر توالی باعث اجرای پرس و جو میشود foreach (var number in numbersGreaterThanTwoQuery) { Console.WriteLine(number); }
99 3 5
int[] fibonacci = { 0, 1, 1, 2, 3, 5 }; // ساخت پرس و جو IEnumerable<int> numbersGreaterThanTwoQuery = fibonacci.Where(x => x > 2) .ToArray(); // در این مرحله به خاطر عملگر استفاده شده پرس و جو اجرا میشود // تغییر اولین عنصر توالی fibonacci[0] = 99; // حرکت بر روی نتیجه foreach (var number in numbersGreaterThanTwoQuery) { Console.WriteLine(number); }
3 5
استفاده از LINQ جهت انجام کوئریها توسط NHibernate
نگارش نهایی 1.0 کتابخانهی LINQ to NHibernate اخیرا (حدود سه ماه قبل) منتشر شده است. در این قسمت قصد داریم با کمک این کتابخانه، اعمال متداول انجام کوئریها را بر روی دیتابیس قسمت قبل انجام دهیم.
توسط این نگارش ارائه شده، کلیه اعمال قابل انجام با criteria API این فریم ورک را میتوان از طریق LINQ نیز انجام داد (NHibernate برای کار با دادهها و جستجوهای پیشرفته بر روی آنها، HQL : Hibernate Query Language و Criteria API را سالها قبل توسعه داده است).
جهت دریافت پروایدر LINQ مخصوص NHibernate به آدرس زیر مراجعه نمائید:
پس از دریافت آن، به همان برنامه کنسول قسمت قبل، دو ارجاع را باید افزود:
الف) ارجاعی به اسمبلی NHibernate.Linq.dll
ب) ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم
در ابتدای متد Main برنامه قصد داریم تعدادی مشتری را به دیتابیس اضافه نمائیم. به همین منظور متد AddNewCustomers را به کلاس CDbOperations برنامه کنسول قسمت قبل اضافه نمائید. این متد لیستی از مشتریها را دریافت کرده و آنها را در طی یک تراکنش به دیتابیس اضافه میکند:
public void AddNewCustomers(params Customer[] customers)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
foreach (var data in customers)
session.Save(data);
session.Flush();
transaction.Commit();
}
}
}
پس از افزودن این ارجاعات، کلاس جدیدی را به نام CLinqTest به برنامه کنسول اضافه نمائید. ساختار کلی این کلاس که قصد استفاده از پروایدر LINQ مخصوص NHibernate را دارد باید به شکل زیر باشد (به کلاس پایه NHibernateContext دقت نمائید):
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{ }
}
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{
ISessionFactory _factory;
public CLinqTest(ISessionFactory factory)
{
_factory = factory;
}
public List<Customer> GetAllCustomers()
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>() select x;
return query.ToList();
}
}
}
}
در این کوئری، لیست تمامی مشتریها بازگشت داده میشود.
سپس جهت استفاده و بررسی آن در متد Main برنامه خواهیم داشت:
static void Main(string[] args)
{
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
var customer1 = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};
var customer2 = new Customer()
{
FirstName = "Ali",
LastName = "Hasani",
AddressLine1 = "Addr..1",
AddressLine2 = "Addr..2",
PostalCode = "4321",
City = "Shiraz",
CountryCode = "IR"
};
var customer3 = new Customer()
{
FirstName = "Mohsen",
LastName = "Shams",
AddressLine1 = "Addr...1",
AddressLine2 = "Addr...2",
PostalCode = "5678",
City = "Ahwaz",
CountryCode = "IR"
};
CDbOperations db = new CDbOperations(session);
db.AddNewCustomers(customer1, customer2, customer3);
CLinqTest lt = new CLinqTest(session);
foreach (Customer customer in lt.GetAllCustomers())
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
مهمترین مزیت استفاده از LINQ در این نوع کوئریها نسبت به روشهای دیگر، استفاده از کدهای strongly typed دات نتی تحت نظر کامپایلر است، نسبت به رشتههای معمولی SQL که کامپایلر کنترلی را بر روی آنها نمیتواند داشته باشد (برای مثال اگر نوع یک ستون تغییر کند یا نام آن، در حالت استفاده از LINQ بلافاصله یک خطا را از کامپایلر جهت تصحیح مشکلات دریافت خواهیم کرد که این مورد در زمان استفاده از یک رشته معمولی صادق نیست). همچنین مزیت فراهم بودن Intellisense را حین نوشتن کوئریهایی از این دست نیز نمیتوان ندید گرفت.
مثالی دیگر:
لیست تمام مشتریهای شیرازی را نمایش دهید:
ابتدا متد GetCustomersByCity را به کلاس CLinqTest فوق اضافه میکنیم:
public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>()
where x.City == city
select x;
return query.ToList();
}
}
foreach (Customer customer in lt.GetCustomersByCity("Shiraz"))
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
لیست کامل دیتابیسهای پشتیبانی شده توسط NHibernate را در این آدرس میتوانید مشاهده نمائید. (البته به نظر لیست آن، آنچنان هم به روز نیست؛ چون در نگارش آخر NHibernate ، پشتیبانی از اس کیوال سرور 2008 هم اضافه شده است)
نکته:
در کوئریهای مثالهای فوق همواره باید session.Linq<T> را ذکر کرد. اگر علاقمند بودید شبیه به روشی که در LINQ to SQL موجود است مثلا db.TableName بجای session.Linq<T> در کوئریها ذکر گردد، میتوان اصلاحاتی را به صورت زیر اعمال کرد:
یک کلاس جدید را به نام SampleContext به برنامه کنسول جاری با محتویات زیر اضافه نمائید:
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class SampleContext : NHibernateContext
{
public SampleContext(ISession session)
: base(session)
{ }
public IOrderedQueryable<Customer> Customers
{
get { return Session.Linq<Customer>(); }
}
public IOrderedQueryable<Employee> Employees
{
get { return Session.Linq<Employee>(); }
}
public IOrderedQueryable<Order> Orders
{
get { return Session.Linq<Order>(); }
}
public IOrderedQueryable<OrderItem> OrderItems
{
get { return Session.Linq<OrderItem>(); }
}
public IOrderedQueryable<Product> Products
{
get { return Session.Linq<Product>(); }
}
}
}
سپس بازنویسی متد GetCustomersByCity بر اساس SampleContext فوق به صورت زیر خواهد بود که به کوئریهای LINQ to SQL بسیار شبیه است:
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample1.Domain;
namespace ConsoleTestApplication
{
class CSampleContextTest
{
ISessionFactory _factory;
public CSampleContextTest(ISessionFactory factory)
{
_factory = factory;
}
public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
using (SampleContext db = new SampleContext(session))
{
var query = from x in db.Customers
where x.City == city
select x;
return query.ToList();
}
}
}
}
}
و در تکمیل این بحث، میتوان به لیستی از 101 مثال LINQ ارائه شده در MSDN اشاره کرد که یکی از بهترین و سریع ترین مراجع یادگیری مبحث LINQ است.
ادامه دارد ...
public class WrongSingleton { static WrongSingleton _instance; WrongSingleton() { } public static WrongSingleton Instance { get { return _instance ?? (_instance = new WrongSingleton()); } } }
راه حلهای زیادی برای رفع این مشکل با اعمال مباحث قفل گذاری تا نکات ریز مربوط به کامپایلر وجود دارند که لیست آنها را در اینجا میتوانید مطالعه کنید.
از دات نت 4 به بعد با معرفی الگوی Lazy، امکان پیاده سازی lazy thread safe singletons به صورت توکار در دسترس میباشد. نمونهای از آن در کدهای IoC Container معروفی به نام StructureMap بکار رفتهاست:
public class Container { // ... } public static class ObjectFactory { private static readonly Lazy<Container> _containerBuilder = new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication); public static Container Container { get { return _containerBuilder.Value; } } private static Container defaultContainer() { return new Container(); } }
- چون این وهله توسط کلاس Lazy محصور شدهاست، صرفا در اولین بار دسترسی به آن، نمونه سازی خواهد شد. این مورد سبب کاهش مصرف حافظهی برنامه و همچنین بالا رفتن سرعت برپایی اولیهی آن میشود. بسیاری از اشیایی که در یک برنامه تعریف میشوند، شاید الزاما جهت ارائه راه حلی برای مسالهای خاص، مورد استفاده قرار نگیرند. تعریف آنها به صورت Lazy، سربار نمونه سازی الزامی آنها را حذف خواهد کرد و آنرا به اولین بار استفاده از شیء مورد نظر، به تعویق خواهد انداخت.
- با استفاده از LazyThreadSafetyMode.ExecutionAndPublication، چندین ترد میتوانند به خاصیت Container دسترسی پیدا کنند، اما تنها یکی از آنها موفق به وهله سازی این کلاس خواهد شد. البته حالت ExecutionAndPublication، حالت پیش فرض است و الزاما نیازی به ذکر آن نیست.
اینبار بازنویسی کلاس ابتدای بحث با توجه به نکات ذکر شده به صورت زیر خواهد بود:
public sealed class LazySingleton { private static readonly Lazy<LazySingleton> _instance = new Lazy<LazySingleton>(() => new LazySingleton(), LazyThreadSafetyMode.ExecutionAndPublication); private LazySingleton() { } public static LazySingleton Instance { get { return _instance.Value; } } }
- تنها وهلهی در دسترس کلاس، به صورت استاتیک و نمونه سازی کلاس، توسط کلاس Lazy با پارامتر LazyThreadSafetyMode.ExecutionAndPublication انجام میشود.
- علت استفاده از lambda در سازندهی کلاس Lazy، امکان دسترسی به اعضای private کلاس، از طریق یک خاصیت static است.
Class Coupling and Cohesion
تعدادی از قواعد شهودی هم، با Coupling و Cohesion به ترتیب مابین و درون کلاسها، سروکار دارند. تلاش ما در راستای افزایش Cohesion درون کلاسها و سست کردن و کاهش Coupling مابین کلاسها میباشد. این قواعد شهودی همین اهداف را در پارادایم action-oriented، در ارتباط با توابع دارند. هدف از Tight Cohesion (انسجام و چسبندگی قوی) در توابع، انسجام بالا و ارتباط نزدیک مابین کدهای موجود در تابع، میباشد. هدفی که Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) در بین توابع دنبال میکند، اشاره دارد به اینکه اگر تابعی قصد استفاده از تابع دیگری را داشته باشد، باید وارد شدن و خروج از آن، از یک نقطه صورت گیرد. این مباحث منجربه مطرح شدن قواعد شهودی از جمله: «یک تابع باید طوری سازماندهی شود که تنها یک دستور return داشته باشد»، در پارادایم action-oriented میشود.
ما در پارادایم شیء گرا، اهداف خود از Loose Coupling و Tight Cohesion را در سطح کلاس مطرح میکنیم. 5 شکل اصلی Coupling مابین کلاسها به شرح زیر میباشد:
- Ni Coupling
- Export Coupling
- Overt Coupling
- Covert Coupling
- Surreptitious Coupling
بهترین حالتی که دو کلاس به طور مطلق هیچ وابستگی به یکدیگر ندارند. در این صورت میتوان یکی کلاسها را حذف کرد، بدون اینکه تأثیری بر روی سایر آنها داشته باشد. البته وجود برنامهای کاربردی با این نوع اتصال ممکن نخواهد بود. بهترین چیزی که میشود با این نوع اتصال ایجاد کرد، Class Libraryای میباشد که شامل مجموعه ای از کلاسهای مستقل بوده، به طوری که هیچ تأثیری بر روی یکدیگر ندارند.
در این شکل از اتصال، یک کلاس به واسط عمومی کلاس دیگر وابسته میباشد؛ به این معنی که از عملیاتی که کلاس مورد نظر در واسط عمومی خود قرار داده است، استفاده میکند.
این نوع اتصال زمانی رخ میدهد که یک کلاس از جزئیات پیاده سازی کلاس دیگر با داشتن اجازه دسترسی از جانب آن، استفاده کند. به عنوان مثال، مکانیزم کلاسهای friend در زبان سی پلاس پلاس، که امکان این را میدهد کلاس X اجازه دوستی به کلاس Y را اعطا کند و در این صورت کلاس Y میتواند به جزئیات پیاده سازی خصوصی کلاس X دسترسی داشته باشد.
این نوع اتصال هم به مانند Overt میباشد؛ با این تفاوت که هیچ اجازه دسترسی به کلاس Y داده نشده است. اگر زبانی داشته باشیم که به کلاس Y اجازه دهد خود را به عنوان دوست کلاس X معرفی کند، در این صورت نوع اتصال بین دو کلاس X و Y از نوع Covert میباشد. به عنوان مثال واقعی، میتوان به استفاده از Reflection در دات نت اشاره کرد.
آخرین نوع اتصال که بدترین حالت هم محسوب میشود، مربوط است به زمانیکه کلاس X به هر طریقی که شده از جزئیات داخلی کلاس Y آگاه باشد و از اعضای عمومی دادهای (public data member) آن کلاس استفاده کند. منظور این است که با تغییر این دادههای کلاس متوجه میشود که بر روی عملیات b از کلاس چه تأثیری میگذارد و با اتکاء به این دستاورد، جزئیات داخلی خود را پیاده سازی میکند و یک اتصال پنهان را با کلاس Y ایجاد کرده است. در این حالت یک وابستگی قوی به صورت پنهان مابین رفتاری از کلاس Y و پیاده سازی کلاس X ایجاد شده است.
اتصال و پیوستگی مابین کلاسها باید از نوع Nil یا Export باشد؛ به این معنی که یک کلاس فقط از واسط عمومی کلاس دیگر استفاده کند یا کاری با آن نداشته باشد. (Classes should only exhibit nil or export coupling with other classes, that is, a class should only use operations in the public interface of another class or have nothing to do with that class.)بجز این دو نوع اتصال، بقیه شکلهای اتصال به طریقی اجازه دسترسی به جزئیات پیاده سازی کلاسها را اعطا میکنند. در نتیجه باعث ایجاد وابستگی مابین پیاده سازی دو کلاس میشوند. این وابستگی ما بین پیاده سازیها به محض نیاز به تغییر پیاده سازی یکی از کلاسها ، باعث به وجود آمدن مشکلات نگهداری خواهند شد.
Cohesion درون کلاسها سعی بر این دارد که مطمئن شود تمام اجزای یک کلاس به شدت باهم مرتبط هستند. تعدادی از قواعد شهودی نیز در ادامه بر این خصوصیت دلالت دارند.
قاعده شهودی 2.8
یک کلاس باید یک و تنها یک Key Abstraction را تسخیر نماید. (A class should capture one and only one key abstraction)یک Key Abstraction به عنوان یک Entity در Domain Model تعریف میشود و اغلب در غالب اسم در اسناد و مشخصات نیازمندیها ظاهر میشوند. هر کدام از آنها باید فقط به یک کلاس نگاشت پیدا کنند. اگر این نگاشت به بیش از یک کلاس انجام گیرد، در نتیجه احتمالا طراح هر تابع را به عنوان یک کلاس تسخیر کرده است. اگر بیش از یک Key Abstraction به یک کلاس نگاشت پیدا کرده باشد، پس احتمالا طراح یک سیستم متمرکز را طراحی کرده است. این کلاسها Vague Classes نامیده میشوند و باید آنها در دو کلاس یا بیشتر، تسخیر شوند.
قاعده شهودی 2.9
داده و رفتار مرتبط را در یک جا (کلاس) نگه دارید. (Keep related data and behavior in one place)در واقع هدفی که این قاعده به دنبال آن میباشد این است که هر دو جزء تشکیل دهنده یک Key Abstraction ، یعنی همان داده و رفتار، باید توسط فقط یک کلاس تسخیر شوند. با نقض این قاعده، توسعه دهنده باید با قرار داد (Convention) خاصی برنامه نویسی کند.
طراح باید کلاسهایی را که مرتبا دادههای مورد نیاز خود را با متدهای get از سایر کلاسها دریافت میکنند، شناسایی کند. زیرا این نوع کلاسها این قاعده شهودی را نقض کردهاند.
مثال واقعی
یا برعکس آن ضد الگوی Anemic Domain Model که ناقض این قاعده میباشد.
در قسمت اول اشاره کردیم این قواعد را به راحتی میتوان در صورت نیاز نقض کرد. بعضی از مواقع نیاز به طراحی فیزیکی است که باعث تغییر در طراحی منطقی شده و چه بسا میتواند باعث نقض هر کدام از این قواعد شهودی نیز شود. اگر به بخش پروژههای سایت رجوع کنید این نقض کاملا مشهود (DomainClasses و ServiceLayer موجود در طراحی فیزیکی آنها) میباشد (بیشتر از Anemic Domain Model استفاده شده است)؛ ولی نمیتوان گفت که این کار اشتباه است.
قاعده شهودی 2.10
اطلاعات نامرتبط به هم را در کلاسهای جدا از هم قرار دهید. ((Spin off nonrelated information into another class (i.e., noncommunicating behavior)
هدف از این قاعده این است که اگر کلاسی داریم که یکسری از متدهایش با بخشی از داده و یکسری دیگر با بخش دیگر دادهها کار میکنند، در واقع شما دو Key Abstraction را به یک کلاس نگاشت کرده اید (Vague Class) و باید آنها را به کلاسهای جدا نگاشت کنید.
به کلاس Dictionary در تصویر زیر توجه کنید.
برای تعداد کمی داده، بهترین پیاده سازی با استفاده از List و در مقابل برای تعداد داده زیاد بهترین پیاده سازی، استفاده از HashTable میباشد. هر یک از این پیاده سازیها، به متدهایی برای add و find کلمات نیاز دارند. طراحی سمت چپ تصویر نشان از نقض این قاعده شهودی دارد.
در طرح سمت چپ، استفاده کننده باید بداند که چقدر داده وارد کند. از طرفی نمایش جزئیات پیاده سازی در نام کلاس هم ایده خوبی نیست (طرح سمت چپ). بهترین راه حل که در مقالات آینده به آنها خواهیم رسید، بحث استفاده از ارث بری میباشد. به این ترتیب، با استفاده از یک کلاس Dictionary که نمایش جزئیات داخلی خود را مخفی کرده و در شرایط لازم نمایش جزئیات داخلی خود را تغییر دهد. منظور این است که استفاده کننده درگیر جزئیات داخلی آن نشود و این جزئیات که کدام نوع PDict یا HDict استفاده خواهد شد، از دید او مخفی باشد.