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 ، تمام فرمتهای تولید شده توسط دات نت را پوشش میدهد.
public class ProductService { public IEnumerable<Product> GetOrderedProducts() { using (var ctx = new Entites()) { return ctx.Products.OrderBy(x => x.Name).ToList(); } } }
public interface IUnitOfWork { IDbSet<TEntity> Set<TEntity>() where TEntity : class; int SaveAllChanges(); } public class Entites : DbContext, IUnitOfWork { public virtual DbSet<Product> Products { get; set; } // This is virtual because Moq needs to override the behaviour public new virtual IDbSet<TEntity> Set<TEntity>() where TEntity : class // This is virtual because Moq needs to override the behaviour { return base.Set<TEntity>(); } public int SaveAllChanges() { return base.SaveChanges(); } }
public class ProductService { private readonly IDbSet<Product> _products; private readonly IUnitOfWork _uow; public ProductService(IUnitOfWork uow) { _uow = uow; _products = _uow.Set<Product>(); } public IEnumerable<Product> GetOrderedProducts() { return _products.OrderBy(x => x.Name).ToList(); } }
[TestFixture] public class ProductServiceTest { private readonly ProductService _productService; public ProductServiceTest() { IQueryable<Product> data = GetRoadNetworks().AsQueryable(); var mockSet = new Mock<DbSet<Product>>(); mockSet.As<IQueryable<Product>>().Setup(m => m.Provider).Returns(data.Provider); mockSet.As<IQueryable<Product>>().Setup(m => m.Expression).Returns(data.Expression); mockSet.As<IQueryable<Product>>().Setup(m => m.ElementType).Returns(data.ElementType); mockSet.As<IQueryable<Product>>().Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator()); var context = new Mock<Entites>(); context.Setup(c => c.Products).Returns(mockSet.Object); context.Setup(m => m.Set<Product>()).Returns(mockSet.Object); _productService = new ProductService(context.Object); } private IEnumerable<Product> GetRoadNetworks() { return new List<Product> { new Product { Id = 1, Name = "A" }, new Product { Id = 2, Name = "B" }, new Product { Id = 3, Name = "C" } }; } [Test] public void GetOrderedProductTest() { IEnumerable<Product> products = _productService.GetOrderedProducts(); List<string> names = products.Select(x => x.Name).ToList(); var expected = new List<string> {"A", "B", "C"}; CollectionAssert.AreEqual(names, expected); } }
در NHibernate چندین و چند روش، جهت تهیه کوئریها وجود دارد که QueryOver یکی از آنها است (+). QueryOver نسبت به LINQ to NH سازگاری بهتری با ساز و کار درونی NHibernate دارد؛ برای مثال امکان یکپارچگی آن با سطح دوم کش. هر چند ظاهر QueryOver با LINQ یکی است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب میکند. برای مثال در LINQ to NH میتواند نوشت x.Property.Contains اما در QueryOver متدی به نام contains قابل استفاده نیست (هر چند در Intellisense ظاهر میشود اما عملا تعریف نشده است و نباید آنرا با LINQ اشتباه گرفت) و سعی در استفاده از آنها به استثناهای زیر ختم میشوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
using NHibernate.Validator.Constraints;
namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }
[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
[NotNull]
public virtual int Balance { set; get; }
}
}
1) یافتن رکوردهایی که در یک مجموعهی مشخص قرار دارند. برای مثال balance آنها مساوی 10 و 12 است:
var list = new[] { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)
2) پیاده سازی همان متد Contains ذکر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */
در اینجا بر اساس مقادیر مختلف MatchMode میتوان متدهای StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نیز تهیه نمود.
انجام مثال دوم راه سادهتری نیز دارد. قسمت WhereRestrictionOn و IsLike به صورت یک سری extension متد ویژه در فضای نام NHibernate.Criterion تعریف شدهاند. ابتدا این فضای نام را به کلاس جاری افزوده و سپس میتوان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();
این فضای نام شامل چهار extension method به نامهای IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.
چگونه extension method سفارشی خود را تهیه کنیم؟
بهترین کار این است که به سورس NHibernate ، فایلهای RestrictionsExtensions.cs و ExpressionProcessor.cs که تعاریف متد IsLike در آنها وجود دارد مراجعه کرد. در اینجا میتوان با نحوهی تعریف و سپس ثبت آن در رجیستری extension methods مرتبط با QueryOver توسط متد عمومی RegisterCustomMethodCall آشنا شد. در ادامه سه کار را میتوان انجام داد:
-متد مورد نظر را در کدهای خود (نه کدهای اصلی NH) اضافه کرده و سپس با فراخوانی RegisterCustomMethodCall آنرا قابل استفاده نمائید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید.
-متد خود را به سورس اصلی NH اضافه کرده و کامپایل کنید (بهتر است همان روش نامگذاری بکار گرفته شده در فایلهای ذکر شده رعایت شود). یک تست هم برای آن بنویسید (تست نویسی هم یک سری اصولی دارد (+)). سپس یک patch از آن روی آن ساخته (+) و برای تیم NH ارسال نمائید (تا جایی که دقت کردم از کلیه ارسالهایی که آزمون واحد نداشته باشند، صرفنظر میشود).
مثال:
میخواهیم extension متد جدیدی به نام Year را به QueryOver اضافه کنیم. این متد را هم بر اساس توابع توکار بانکهای اطلاعاتی، تهیه خواهیم نمود. لیست کامل این نوع متدهای بومی SQL را در فایل Dialect.cs سورسهای NH میتوان یافت (البته به صورت پیش فرض از متد extract برای جداسازی قسمتهای مختلف تاریخ استفاده میکند. این متد در فایلهای Dialect مربوط به بانکهای اطلاعاتی مختلف، متفاوت است و برحسب بانک اطلاعاتی جاری به صورت خودکار تغییر خواهد کرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;
namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}
public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}
public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}
اکنون برای استفاده خواهیم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //یکبار در ابتدای اجرای برنامه باید ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();
برای مثال اگر بانک اطلاعاتی انتخابی از نوع SQLite باشد، خروجی SQL مرتبط به شکل زیر خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */
هر چند ما تابع year را در متد ProcessAnsiYear ثبت کردهایم اما بر اساس فایل SQLiteDialect.cs ، تعاریف مرتبط و مخصوص این بانک اطلاعاتی (مانند متد strftime فوق) به صورت خودکار دریافت میگردد و کد ما مستقل از نوع بانک اطلاعاتی خواهد بود.
نکته جالب!
LINQ to NH هم قابل بسط است؛ کاری که در ORM های دیگر به این سادگی نیست. چند مثال در این زمینه:
چگونه تابع سفارشی SQL Server خود را به صورت یک extension method تعریف و استفاده کنیم: (+) ، یک نمونه دیگر: (+) و نمونهای دیگر: (+).
پیاده سازی ساده Google Recaptcha در ASP.NET MVC
public static class AsyncHelper { private static readonly TaskFactory _taskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default); public static TResult RunSync<TResult>(Func<Task<TResult>> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); public static void RunSync(Func<Task> func) => _taskFactory .StartNew(func) .Unwrap() .GetAwaiter() .GetResult(); }
AsyncHelper.RunSync(() => client. IsAcceptAsync ( response , secretkey ));
یکی
از ویژگیهای جدید اضافه شده به سی شارپ 9، Attributes on
local functions نام دارد و این توانایی را به ما میدهد تا بر روی متدهای محلی که
درون متدها تعریف میشوند، Attributes قرار دهیم. قابلیت local functions در نسخه 7 سی شارپ اضافه شدهاست و با استفاده از این قابلیت میتوانیم درون یک متد، تابع دیگری را تعریف کنیم و در همان متد، از آن تابع درونی
استفاده کنیم. در واقع تابع درونی، لوکال متد بیرونی است و در خارج از متد بیرونی،
قابل دسترسی نیست. مانند مثال زیر:
// Main method public static void Main() { // Local Function void AddValue(int a, int b) { Console.WriteLine("Value of a is: " + a); Console.WriteLine("Value of b is: " + b); Console.WriteLine("Sum of a and b is: {0}", a + b); Console.WriteLine(); } // Calling Local function AddValue(20, 40); AddValue(40, 60); }
برای بررسی این ویژگی جدید سی شارپ 9.0، از یک مثال استفاده میکنیم. فرض کنید یک برنامهی کنسول را داریم و میخواهیم یک قطعه کد فقط در حالتی در خروجی نوشته شود که برنامه در حالت دیباگ اجرا شده باشد و اگر در حالت ریلیز باشد، در خروجی مشاهده نشود. قبل از نسخهی 9.0 سی شارپ، مجبور هستیم از directive های کامپایلر زبان استفاده کنیم و از طریق آن به کامپایلر بفهمانیم که چه زمانی این قطعه کد را کامپایل کند. مانند مثال زیر:
static void Main(string[] args) { static void DoAction() { // DoAction Console.WriteLine("DoAction..."); } #if DEBUG DoAction(); #endif }
اما با استفاده قابلیت اضافه شدهی در این نسخه از سی شارپ، میتوان روی متدهای محلی هم Attributes اضافه کرد. پس میتوانیم از ConditionalAttribute استفاده کنیم و آن را در بالای متد محلی قرار دهیم و از کامپایلر بخواهیم در حالت دیباگ اجرا شود. مانند کد زیر
static void Main(string[] args) { [Conditional("DEBUG")] static void DoAction() { // Do Action Here Console.WriteLine("Do Action..."); } DoAction(); }
اگر بر روی متدهای محلی C# 8.0 از Attribute استفاده کنیم، با خطای زیر روبرو میشویم:
ErrorCS8400Feature 'local function attributes' is not available in C# 8.0. Please use language version 9.0 or greater.
نکته: دقت کنید که تنها یک فولدر App_GlobalResources به هر پروزه میتوان افزود. همچنین در ریشه هر مسیر موجود در پروژه تنها میتوان یک فولدر Appp_LocalResources داشت. پس از افزودن هر یک از این فولدرهای مخصوص، منوی فوق به صورت زیر در خواهد آمد:
protected object GetLocalResourceObject(string resourceKey) protected object GetLocalResourceObject(string resourceKey, Type objType, string propName)
txtTest.Text = GetLocalResourceObject("txtTest.Text") as string;
protected object GetGlobalResourceObject(string className, string resourceKey) protected object GetGlobalResourceObject(string className, string resourceKey, Type objType, string propName)
TextBox1.Text = GetGlobalResourceObject("Resource1", "String1") as string;
public static object GetLocalResourceObject(string virtualPath, string resourceKey) public static object GetLocalResourceObject(string virtualPath, string resourceKey, CultureInfo culture)
txtTest.Text = HttpContext.GetLocalResourceObject("~/Default.aspx", "txtTest.Text") as string;
public static object GetGlobalResourceObject(string classKey, string resourceKey) public static object GetGlobalResourceObject(string classKey, string resourceKey, CultureInfo culture)
TextBox1.Text = HttpContext.GetGlobalResourceObject("Resource1", "String1") as string;
//------------------------------------------------------------------------------ // <auto-generated> // This code was generated by a tool. // Runtime Version:4.0.30319.17626 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> //------------------------------------------------------------------------------ namespace Resources { using System; /// <summary> /// A strongly-typed resource class, for looking up localized strings, etc. /// </summary> // This class was auto-generated by the StronglyTypedResourceBuilder // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option or rebuild the Visual Studio project. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Web.Application.StronglyTypedResourceProxyBuilder", "10.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resource1 { private static global::System.Resources.ResourceManager resourceMan; private static global::System.Globalization.CultureInfo resourceCulture; [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal Resource1() { } /// <summary> /// Returns the cached ResourceManager instance used by this class. /// </summary> [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Resources.Resource1", global::System.Reflection.Assembly.Load("App_GlobalResources")); resourceMan = temp; } return resourceMan; } } /// <summary> /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// </summary> [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] internal static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } set { resourceCulture = value; } } /// <summary> /// Looks up a localized string similar to String1. /// </summary> internal static string String1 { get { return ResourceManager.GetString("String1", resourceCulture); } } } }
TextBox1.Text = Resources.Resource1.String1;