public class Book { public string Title { get; set; } public int Id { get; set; } public Author Author { get; set; } public Publisher Publisher { get; set; } public IList<Book> GetBooks() { var books = new List<Book>(); for (int i = 0; i < 10; i++) { var book = new Book() { Id = i, Title = $"Title {i}", Author = new Author() { FirstName = $"Author {i} First Name", LastName = $"Author {i} Last Name", Person = new Person() { Type = $"Type {i}", Value = $"Value {i}" } }, Publisher = new Report.Publisher() { Name = $"Publiser {i}" } }; books.Add(book); } return books; } } public class Author { public string FirstName { get; set; } public string LastName { get; set; } public Person Person { get; set; } } public class Person { public string Type { get; set; } public string Value { get; set; } } public class Publisher { public string Name { get; set; } }
public abstract class ResourceProviderFactory { public abstract IResourceProvider CreateGlobalResourceProvider(string classKey); public abstract IResourceProvider CreateLocalResourceProvider(string virtualPath); }
public interface IResourceProvider { IResourceReader ResourceReader { get; } object GetObject(string resourceKey, CultureInfo culture); }
// Type: System.Web.Compilation.ResXResourceProviderFactory // Assembly: System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a // Assembly location: C:\Windows\Microsoft.NET\assembly\GAC_32\System.Web\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Web.dll using System.Runtime; using System.Web; namespace System.Web.Compilation { internal class ResXResourceProviderFactory : ResourceProviderFactory { [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] public ResXResourceProviderFactory() { } public override IResourceProvider CreateGlobalResourceProvider(string classKey) { return (IResourceProvider) new GlobalResXResourceProvider(classKey); } public override IResourceProvider CreateLocalResourceProvider(string virtualPath) { return (IResourceProvider) new LocalResXResourceProvider(VirtualPath.Create(virtualPath)); } } }
internal class GlobalResXResourceProvider : BaseResXResourceProvider { private string _classKey; internal GlobalResXResourceProvider(string classKey) { _classKey = classKey; } protected override ResourceManager CreateResourceManager() { string fullClassName = BaseResourcesBuildProvider.DefaultResourcesNamespace + "." + _classKey; // If there is no app resource assembly, return null if (BuildManager.AppResourcesAssembly == null) return null; ResourceManager resourceManager = new ResourceManager(fullClassName, BuildManager.AppResourcesAssembly); resourceManager.IgnoreCase = true; return resourceManager; } public override IResourceReader ResourceReader { get { // App resources don't support implicit resources, so the IResourceReader should never be needed throw new NotSupportedException(); } } }
internal const string DefaultResourcesNamespace = "Resources";
internal class LocalResXResourceProvider : BaseResXResourceProvider { private VirtualPath _virtualPath; internal LocalResXResourceProvider(VirtualPath virtualPath) { _virtualPath = virtualPath; } protected override ResourceManager CreateResourceManager() { ResourceManager resourceManager = null; Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly != null) { string fileName = _virtualPath.FileName; resourceManager = new ResourceManager(fileName, pageResAssembly); resourceManager.IgnoreCase = true; } else { throw new InvalidOperationException(SR.GetString(SR.ResourceExpresionBuilder_PageResourceNotFound)); } return resourceManager; } public override IResourceReader ResourceReader { get { // Get the local resource assembly for this page Assembly pageResAssembly = GetLocalResourceAssembly(); if (pageResAssembly == null) return null; // Get the name of the embedded .resource file for this page string resourceFileName = _virtualPath.FileName + ".resources"; // Make it lower case, since GetManifestResourceStream is case sensitive resourceFileName = resourceFileName.ToLower(CultureInfo.InvariantCulture); // Get the resource stream from the resource assembly Stream resourceStream = pageResAssembly.GetManifestResourceStream(resourceFileName); // If this page has no resources, return null if (resourceStream == null) return null; return new ResourceReader(resourceStream); } } [PermissionSet(SecurityAction.Assert, Unrestricted = true)] private Assembly GetLocalResourceAssembly() { // Remove the page file name to get its directory VirtualPath virtualDir = _virtualPath.Parent; // Get the name of the local resource assembly string cacheKey = BuildManager.GetLocalResourcesAssemblyName(virtualDir); BuildResult result = BuildManager.GetBuildResultFromCache(cacheKey); if (result != null) { return ((BuildResultCompiledAssembly)result).ResultAssembly; } return null; } }
نکته: باتوجه به استفاده از عبارات بومیسازی ضمنی در استفاده از ورودیهای منابع محلی، خاصیت ResourceReader در این کلاس نمونهای متناظر برای درخواست جاری از کلاس ResourceReader با استفاده از Stream استخراج شده از اسمبلی یافته شده، تولید میکند.
internal abstract class BaseResXResourceProvider : IResourceProvider { private ResourceManager _resourceManager; ///// IResourceProvider implementation public virtual object GetObject(string resourceKey, CultureInfo culture) { // Attempt to get the resource manager EnsureResourceManager(); // If we couldn't get a resource manager, return null if (_resourceManager == null) return null; if (culture == null) culture = CultureInfo.CurrentUICulture; return _resourceManager.GetObject(resourceKey, culture); } public virtual IResourceReader ResourceReader { get { return null; } } ///// End of IResourceProvider implementation protected abstract ResourceManager CreateResourceManager(); private void EnsureResourceManager() { if (_resourceManager != null) return; _resourceManager = CreateResourceManager(); } }
System.Resources.ResourceManager(string baseName, Assembly assemblyName)
var manager = new System.Resources.ResourceManager("Resources.Resource1", typeof(Resource1).Assembly)
var manager = new System.Resources.ResourceManager("Resources.Resource1", Assembly.LoadFile(@"c:\MyResources\MyGlobalResources.dll"))
public static ResourceManager CreateFileBasedResourceManager(string baseName, string resourceDir, Type usingResourceSet)
resgen d:\MyResources\MyResource.fa.resx
private ResourceManager CreateGlobalResourceManager(string classKey) { var baseName = "Resources." + classKey; var buildManagerType = typeof(BuildManager); var property = buildManagerType.GetProperty("AppResourcesAssembly", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.GetField); var appResourcesAssembly = (Assembly)property.GetValue(null, null); return new ResourceManager(baseName, appResourcesAssembly) { IgnoreCase = true }; }
var manager = CreateGlobalResourceManager("Resource1"); Label1.Text = manager.GetString("String1");
private ResourceManager CreateLocalResourceManager(string virtualPath) { var virtualPathType = typeof(BuildManager).Assembly.GetType("System.Web.VirtualPath", true); var virtualPathInstance = Activator.CreateInstance(virtualPathType, BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { virtualPath }, CultureInfo.InvariantCulture); var buildResultCompiledAssemblyType = typeof(BuildManager).Assembly.GetType("System.Web.Compilation.BuildResultCompiledAssembly", true); var propertyResultAssembly = buildResultCompiledAssemblyType.GetProperty("ResultAssembly", BindingFlags.NonPublic | BindingFlags.Instance); var methodGetLocalResourcesAssemblyName = typeof(BuildManager).GetMethod("GetLocalResourcesAssemblyName", BindingFlags.NonPublic | BindingFlags.Static); var methodGetBuildResultFromCache = typeof(BuildManager).GetMethod("GetBuildResultFromCache", BindingFlags.NonPublic | BindingFlags.Static, null, new Type[] { typeof(string) }, null); var fileNameProperty = virtualPathType.GetProperty("FileName"); var virtualPathFileName = (string)fileNameProperty.GetValue(virtualPathInstance, null); var parentProperty = virtualPathType.GetProperty("Parent"); var virtualPathParent = parentProperty.GetValue(virtualPathInstance, null); var localResourceAssemblyName = (string)methodGetLocalResourcesAssemblyName.Invoke(null, new object[] { virtualPathParent }); var buildResultFromCache = methodGetBuildResultFromCache.Invoke(null, new object[] { localResourceAssemblyName }); Assembly localResourceAssembly = null; if (buildResultFromCache != null) localResourceAssembly = (Assembly)propertyResultAssembly.GetValue(buildResultFromCache, null); if (localResourceAssembly == null) throw new InvalidOperationException("Unable to find the matching resource file."); return new ResourceManager(virtualPathFileName, localResourceAssembly) { IgnoreCase = true }; }
نحوه استفاده از متد فوق نیز به صورت زیر است:
var manager = CreateLocalResourceManager("~/Default.aspx"); Label1.Text = manager.GetString("Label1.Text");
سایت ExtensionMethod.NET بانک اطلاعاتی است از قابلیتی تحت عنوان extension methods معرفی شده در C# 3.0 و Visual Basic 2008 . هدف اصلی از extension methods ، بسط کلاسهای دات نت فریم ورک توسط جامعهی برنامه نویسها میباشد.
مثلا در کلاس پایه string ، متدی به نام Right وجود ندارد. برای اضافه کردن آن میتوان به صورت زیر عمل کرد:
public static string Right(this string s, int length)
{
length = Math.Max(length, 0);
if (s.Length > length)
{
return s.Substring(s.Length - length, length);
}
else
{
return s;
}
}
و پس از آن هم استفاده از این متد که کلاس پایه string را بسط داده است به شکل زیر خواهد بود (همانند یکی از متدهای کلاس string میتوان از آن استفاده کرد):
string s = "abcde";
s = s.Right(3); //s becomes "cde"
برای پیگیری سایت هم میتوان از فید آن استفاده نمود.
• Next : یک عدد تصادفی را برای ما تولید میکند.
• NextByte : آرایهای از بایتها را که با اعداد تصادفی پر شدهاند تولید میکند.
• NextDouble : یک عدد تصادفی را بین 0.0 و 1.0 باز میگرداند.
بررسی متد Next
متد Next سه Overload مختلف دارد و این امکان را برای شما مهیا میکند تا 2 عدد را به عنوان بازه تولید اعداد تصادفی انتخاب کنید (حد پایین و بالای بازه).
تولید یک عدد تصادفی:
var rand = new Random().Next();
var rand = new Random().Next(1000);
کد زیر عددی تصادفی را بین محدوده تعیین شده تولید میکند:
public static int RandomNumber(int min, int max) { var rand = new Random(); return rand.Next(min, max); }
بررسی متد NextDouble
قطعه کد زیر (به کمک توابع کلاس Random) رشتهای تصادفی را با طول مشخصی برای ما تولید میکند؛ با قابلیت تعیین بزرگ و یا کوچک بودن کاراکترهای رشته تصادفی:
public static string RandomString(int size, bool lowerCase) { StringBuilder builder = new StringBuilder(); Random random = new Random(); char ch; for (int i = 0; i < size; i++) { //تولید عدد و تبدیل آن به کاراکتر ch = Convert.ToChar(Convert.ToInt32(Math.Floor(26 * random.NextDouble() + 65))); builder.Append(ch); } if (lowerCase) return builder.ToString().ToLower(); return builder.ToString(); }
public string RandomPassword() { StringBuilder builder = new StringBuilder(); builder.Append(RandomString(4, true)); builder.Append(RandomNumber(1000, 9999)); builder.Append(RandomString(2, false)); return builder.ToString(); }
همانطور که در ابتدای مطلب اشاره شد، خروجی این تابع آرایهای از بایتها میباشد و هر خانهی آن عددی است تصادفی که بزرگتر و یا مساوی 0 و کوچکتر از مقدار Maximum نوع داده Byte است. کد زیر نحوه استفاده از این تابع را نشان میدهد:
byte[] b = new byte[10]; Random rnd = new Random(); rnd.NextBytes(b); for (int i = 0; i < b.Length; i++) { Console.WriteLine(b[i]); }
153 115 86 5 161 190 249 228
برای شروع ابتدا یک اینترفیس تعریف میکنیم:
public interface IFilesService { DirectoryInfo GetDirectoryInfo(string directoryPath); void DeleteFile(string fileName); void WritePersonInFile(string fileName,string name, string lastName, byte age); }
کلاس اصلی:
class FilesServices:IFilesService { public DirectoryInfo GetDirectoryInfo(string directoryPath) { return new DirectoryInfo(directoryPath); } public void DeleteFile(string fileName) { File.Delete(fileName); Console.WriteLine("the file has been deleted"); } public void WritePersonInFile(string fileName, string name, string lastName, byte age) { var text = $"my name is {name} {lastName} with {age} years old from dotnettips.info"; File.WriteAllText(fileName,text); } }
class FilesServicesProxy:IFilesService { private readonly IFilesService _filesService; public FilesServicesProxy() { _filesService=new FilesServices(); } public DirectoryInfo GetDirectoryInfo(string directoryPath) { var existing = Directory.Exists(directoryPath); if (!existing) Directory.CreateDirectory(directoryPath); return _filesService.GetDirectoryInfo(directoryPath); } public void DeleteFile(string fileName) { if(!File.Exists(fileName)) Console.WriteLine("Please enter a valid path"); else _filesService.DeleteFile(fileName); } public void WritePersonInFile(string fileName, string name, string lastName, byte age) { if (!Directory.Exists(fileName.Remove(fileName.LastIndexOf("\\")))) { Console.WriteLine("File Path is not valid"); return; } if (name.Trim().Length == 0) { Console.WriteLine("first name must enter"); return; } if (lastName.Trim().Length == 0) { Console.WriteLine("last name must enter"); return; } if (age<18) { Console.WriteLine("your age is illegal"); return; } if (name.Trim().Length < 3) { Console.WriteLine("first name must be more than 2 letters"); return; } if (lastName.Trim().Length <5) { Console.WriteLine("last name must be more than 4 letters"); return; } _filesService.WritePersonInFile(fileName,name,lastName,age); Console.WriteLine("the file has been written"); } }
در نهایت در بدنه اصلی با تست چندین حالت مختلف، همه متدها را داریم:
static void Main(string[] args) { IFilesService filesService=new FilesServicesProxy(); filesService.WritePersonInFile("c:\\myfakepath\\a.txt","ali","yeganeh",26); var directory = filesService.GetDirectoryInfo("d:\\myrightpath\\"); var fileName = Path.Combine(directory.FullName, "dotnettips.txt"); filesService.WritePersonInFile(fileName, "al", "yeganeh", 26); filesService.WritePersonInFile(fileName, "ali", "yeganeh", 12); filesService.WritePersonInFile(fileName, "ali", "yeganeh", 26); filesService.DeleteFile("c:\\myfakefile.txt"); filesService.DeleteFile(fileName); }
File Path is not valid first name must be more than 2 letters your age is illegal the file has been written Please enter a valid path the file has been deleted
namespace MvcHealthCheckTest.Controllers { public class HomeController : Controller { public IActionResult ViewDetails() { return View(); }
<a asp-controller="Home" asp-action="ViewDetails">View Details</a>
https://localhost:5001/Home/ViewDetails
الف) تمام مسیرها lowercase باشند (مناسب برای SEO؛ از این جهت که تعداد مسیرهای تکراری یکسان با حروف بزرگ و کوچک، به حداقل میرسند)
ب) ViewDetailsها تبدیل به view-details شوند. یعنی بین حروف جدا شدهی با کلمات بزرگ و کوچک، یک - قرار گیرد.
انجام یک چنین تغییرات سراسری در ASP.NET Core 2.2 با معرفی IOutboundParameterTransformer میسر شدهاست:
using System.Text.RegularExpressions; using Microsoft.AspNetCore.Routing; public class CustomUrlTransformer : IOutboundParameterTransformer { public string TransformOutbound(object value) { return value == null ? null : Regex.Replace(value.ToString(), "([a-z])([A-Z])", "$1-$2"); } }
روش معرفی آن نیز به سیستم به صورت زیر است:
ابتدا نیاز است این Transformer به صورت یک ConstraintMap جدید به سیستم مسیریابی اضافه شود. در اینجا امکان تنظیم تولید LowercaseUrls نیز وجود دارد. به همین جهت نیازی نیست تا در متد TransformOutbound فوق، در انتهای کار، متد ToLower را نیز فراخوانی کنیم:
namespace MvcTest { public class Startup { public void ConfigureServices(IServiceCollection services) { // ... services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); services.AddRouting(option => { option.ConstraintMap.Add(key: "transformer1", value: typeof(CustomUrlTransformer)); option.LowercaseUrls = true; }); }
namespace MvcTest { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller:transformer1}/{action:transformer1}/{id?}", defaults: new { controller = "Home", action = "Index" } ); }); }
app.UseMvc(routes => { routes.MapRoute( name: "default", template: "{controller:transformer1=Home}/{action:transformer1=Index}/{id?}" ); });
https://localhost:5001/home/view-details
نکته 1: برای پیاده سازی این مثالها، Castle Windsor به عنوان IOC Container انتخاب شده است. بدیهی است میتوانید از Ioc Container مورد نظر خود نیز بهره ببرید.
نکته 2: میتوانید از مقاله [هاست سرویسهای Web Api با استفاده از OWIN و TopShelf] جهت هاست سرویسهای web Api خود استفاده نمایید.
روش اول
در این روش، قدم اول، ساخت یک کلاس و پیاده سازی اینترفیس IDependencyResolver میباشد؛ به صورت زیر:
public class ApiDependencyResolver : IDependencyResolver { public ApiDependencyResolver(IWindsorContainer container) { Container = container; } public IWindsorContainer Container { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (Kernel.ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (Kernel.ComponentNotFoundException) { return Enumerable.Empty<object>(); } } public IDependencyScope BeginScope() { return new SharedDependencyResolver(Container); } public void Dispose() { Container.Dispose(); } }
کاربرد متد BeginScope چیست؟
کنترلرها به صورت (Per Request) بر اساس هر درخواست وهله سازی خواهند شد. جهت مدیریت چرخهی عمر کنترلرها و منابع در اختیار آنها، از متد BeginScope استفاده میشود. به این صورت که نمونهی اصلی DependencyResolver در هنگام شروع برنامه به GlobalConfiguration پروژه Attach خواهد شد. سپس به ازای هر درخواست، جهت وهله سازی Controllerها، متد GetService از محدوده داخلی (منظور فراخوانی متد BeginScope است) باعث ایجاد نمونه و بعد از اتمام فرآیند، متد Dispose باعث آزاد سازی منابع موجود خواهد شد.
پیاده سازی متد BeginScope وابسته به IocContainer مورد استفاده شما است. در این جا کلاس SharedDependencyResolver را به صورت زیر پیاده سازی کردم:
public class SharedDependencyResolver : IDependencyScope { public SharedDependencyResolver(IWindsorContainer container) { Container = container; Scope = Container.BeginScope(); } public IWindsorContainer Container { get; private set; } public IDisposable Scope { get; private set; } public object GetService(Type serviceType) { try { return Container.Kernel.HasComponent(serviceType) ? Container.Resolve(serviceType) : null; } catch (ComponentNotFoundException) { return null; } } public IEnumerable<object> GetServices(Type serviceType) { try { return Container.ResolveAll(serviceType).Cast<object>(); } catch (ComponentNotFoundException) { return null; } } public void Dispose() { Scope.Dispose(); } }
public IDependencyScope BeginScope() { var child = container.CreateChildContainer(); return new ApiDependencyResolver(child); }
public class KernelInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register(Classes.FromThisAssembly().BasedOn<ApiController>().LifestyleTransient()); container.Kernel.Resolver.AddSubResolver(new CollectionResolver(container.Kernel, true)); } }
در پایان در کلاس Startup نیز کافیست مراحل زیر را انجام دهید:
»ابتدا Installer نوشته شده را به WindsorContainer معرفی نمایید.
»DependencyResolver نوشته شده را به HttpConfiguration معرفی کنید.
»عملیات Routing مورد نظر را ایجاد و سپس config مورد نظر را در اختیار appBuilder قرار دهید.
public class Startup { public void Configuration( IAppBuilder appBuilder ) { var container = new WindsorContainer(); container.Install(new KernelInstaller()); var config = new HttpConfiguration { DependencyResolver = new ApiDependencyResolver(container) }; config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "Default" , routeTemplate: "{controller}/{action}/{name}" , defaults: new { name = RouteParameter.Optional } ); config.EnsureInitialized(); appBuilder.UseWebApi( config ); } }
اگر ویژگیهای پیشفرض مهیا، پاسخگوی اعتبارسنجی مدنظر نبودند، میتوان یک attribute سفارشی را تهیه کرد:
using System.ComponentModel.DataAnnotations; namespace CustomValidators { [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter)] public class EmailDomainValidator : ValidationAttribute { public string AllowedDomain { get; set; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { string[] strings = value.ToString().Split('@'); if (strings[1].ToUpper() == AllowedDomain.ToUpper()) { return null; } return new ValidationResult($"Domain must be {AllowedDomain}", new[] { validationContext.MemberName }); } } }
- کار با ارث بری از کلاس پایهی ValidationAttribute شروع میشود و باید متد IsValid آنرا بازنویسی کرد.
- اگر متد IsValid، نال برگرداند، یعنی مشکلی نیست؛ در غیراینصورت خروجی آن باید از نوع ValidationResult باشد.
- پارامتر validationContext اطلاعاتی مانند نام خاصیت در حال بررسی را ارائه میدهد.
- در اینجا متد ()ValidationContext.GetService نال را بر میگرداند؛ یعنی فعلا از تزریق وابستگیها در آن پشتیبانی نمیشود.
و در آخر روش استفادهی از آن، همانند سایر ویژگیهای اعتبارسنجی است:
public class Employee { [EmailDomainValidator(AllowedDomain = "site.com")] public string Email { get; set; } }
الگوهای طراحی، سندها و راه حلهای از پیش تعریف شده و تست شدهای برای مسائل و مشکلات روزمرهی برنامه نویسی میباشند که هر روزه ما را درگیر خودشان میکنند. هر چقدر مقیاس پروژه وسیعتر و تعداد کلاسها و اشیاء بزرگتر باشند، درگیری برنامه نویس و چالش برای مرتب سازی و خوانایی برنامه و همچنین بالا بردن کارآیی و امنیت افزونتر میشود. از همین رو استفاده از ساختارهایی تست شده برای سناریوهای یکسان، امری واجب تلقی میشود.
الگوهای طراحی از لحاظ سناریو، به سه گروه عمده تقسیم میشوند:
1- تکوینی: هر چقدر تعداد کلاسها در یک پروژه زیاد شود، به مراتب تعداد اشیاء ساخته شده از آن نیز افزوده شده و پیچیدگی و درگیری نیز افزایش مییابد. راه حلهایی از این دست، تمرکز بر روی مرکزیت دادن به کلاسها با استفاده از رابطها و کپسوله نمودن (پنهان سازی) اشیاء دارد.
2- ساختاری: گاهی در پروژهها پیش میآید که میخواهیم ارتباط بین دو کلاس را تغییر دهیم. از این رو امکان از هم پاشی اجزایِ دیگر پروژه پیش میآید. راه حلهای ساختاری، سعی در حفظ انسجام پروژه در برابر این دست از تغییرات را دارند.
3- رفتاری: گاهی بنا به مصلحت و نیاز مشتری، رفتار یک کلاس میبایستی تغییر نماید. مثلا چنانچه کلاسی برای ارائه صورتحساب داریم و در آن میزان مالیات 30% لحاظ شده است، حال این درصد باید به عددی دیگر تغییر کند و یا پایگاه داده به جای مشاهدهی تعدادِ معدودی گره از درخت، حال میبایست تمام گرهها را ارائه نماید.
الگوی فکتوری:
الگوی فکتوری در دستهء اول قرار میگیرد. من در اینجا به نمونهای از مشکلاتی که این الگو حل مینماید، اشاره میکنم:
فرض کنید یک شرکت بزرگ قصد دارد تا جزییات کامل خرید هر مشتری را با زدن دکمه چاپ ارسال نماید. چنین شرکت بزرگی بر اساس سیاستهای داخلی، بر حسب میزان خرید، مشتریان را به چند گروه مشتری معمولی و مشتری ممتاز تقسیم مینماید. در نتیجه نمایش جزییات برای آنها با احتساب میزان تخفیف و به عنوان مثال تعداد فیلدهایی که برای آنها در نظر گرفته شده است، تفاوت دارد. بنابراین برای هر نوع مشتری یک کلاس وجود دارد.
یک راه این است که با کلیک روی دکمهی چاپ، نوع مشتری تشخیص داده شود و
به ازای نوع مشتری، یک شیء از کلاس مشخص شده برای همان نوع ساخته شود.
// Get Customer Type from Customer click on Print Button int customerType = 0; // Create Object without instantiation object obj; //Instantiate obj according to customer Type if (customerType == 1) { obj = new Customer1(); } else if (customerType == 2) { obj = new Customer2(); } // Problem: // 1: Scattered New Keywords // 2: Client side is aware of Customer Type
همانگونه که مشاهده مینمایید در این سبک کدنویسی غیرحرفهای، مشکلاتی مشهود است که قابل اغماض نیستند. در ابتدا سمت کلاینت دسترسی مستقیم به کلاسها دارد و همانگونه که در شکل بالا قابل مشاهده است کلاینت مستقیما به کلاس وصل است. مشکل دوم عدم پنهان سازی کلاس از دید مشتری است.
راه حل: این مشکل با استفاده از الگوی فکتوری قابل حل است. با استناد به الگوی فکتوری، کلاینت تنها به کلاس فکتوری و یک اینترفیس دسترسی دارد و کلاسهای فکتوری و اینترفیس، حق دسترسی به کلاسهای اصلی برنامه را دارند.
گام نخست: در ابتدا یک class library به نام Interface ساخته و در آن یک کلاس با نام ICustomer می سازیم که متد Report() را معرفی مینماید.
//Interface
namespace Interface { public interface ICustomer { void Report(); } }
گام دوم: یک class library به نام MainClass ساخته و با Add Reference کلاس Interface را اضافه نموده، در آن دو کلاس با نام Customer1, Customer2 میسازیم و using Interface را Import مینماییم. هر دو کلاس از ICustomer ارث میبرند و سپس متد Report() را در هر دو کلاس Implement مینماییم.
// Customer1 using System; using Interface; namespace MainClass { public class Customer1 : ICustomer { public void Report() { Console.WriteLine("این گزارش مخصوص مشتری نوع اول است"); } } } //Customer2 using System; using Interface; namespace MainClass { public class Customer2 : ICustomer { public void Report() { Console.WriteLine("این گزارش مخصوص مشتری نوع دوم است"); } } }
گام سوم: یک class library به نام FactoryClass ساخته و با Add Reference کلاس Interface, MainClass را اضافه نموده، در آن یک کلاس با نام clsFactory می سازیم و using Interface, using MainClass را Import مینماییم. پس از آن یک متد با نام getCustomerType ساخته که ورودی آن نوع مشتری از نوع int است و خروجی آن از نوع Interface-ICustomer و بر اساس کد نوع مشتری object را از کلاس Customer1 و یا Customer2 میسازیم و آن را return می نماییم.
//Factory using System; using Interface; using MainClass; namespace FactoryClass { public class clsFactory { static public ICustomer getCustomerType(int intCustomerType) { ICustomer objCust; if (intCustomerType == 1) { objCust = new Customer1(); } else if (intCustomerType == 2) { objCust = new Customer2(); } else { return null; } return objCust; } } }
گام چهارم (آخر): در قسمت UI Client، کد نوع مشتری را از کاربر دریافت کرده و با Add Reference کلاس Interface, FactoryClass را اضافه نموده (دقت نمایید هیچ دسترسی به کلاسهای اصلی وجود ندارد)، و using Interface, using FactoryClass را Import مینماییم. از clsFactory تابع getCustomerType را فراخوانی نموده (به آن کد نوع مشتری را پاس میدهیم) و خروجی آن را که از نوع اینترفیس است به یک object از نوع ICustomer نسبت میدهیم. سپس از این object متد Report را فراخوانی مینماییم. همانطور که از شکل و کدها مشخص است، هیچ رابطه ای بین UI(Client) و کلاسهای اصلی برقرار نیست.
//UI (Client) using System; using FactoryClass; using Interface; namespace DesignPattern { class Program { static void Main(string[] args) { int intCustomerType = 0; ICustomer objCust; Console.WriteLine("نوع مشتری را وارد نمایید"); intCustomerType = Convert.ToInt16(Console.ReadLine()); objCust = clsFactory.getCustomerType(intCustomerType); objCust.Report(); Console.ReadLine(); } } }
بررسی سرعت و کارآیی AutoMapper
مدل مورد استفاده
در اینجا قصد داریم، شیء User را یک میلیون بار توسط روشهای مختلف، به خودش نگاشت کنیم و سرعت انجام اینکار را در حالتهای مختلف اندازه گیری نمائیم:
public class User { public int Id { get; set; } public string UserName { get; set; } public string Password { get; set; } public DateTime LastLogin { get; set; } }
روش بررسی سرعت انجام هر روش
برای کاهش کدهای تکراری، میتوان قسمت تکرار شونده را به صورت یک Action، در بین سایر کدهایی که هر بار نیاز است به یک شکل فراخوانی شوند، قرار داد:
public static void RunActionMeasurePerformance(Action action) { GC.Collect(); var initMemUsage = Process.GetCurrentProcess().WorkingSet64; var stopwatch = new Stopwatch(); stopwatch.Start(); action(); stopwatch.Stop(); var currentMemUsage = Process.GetCurrentProcess().WorkingSet64; var memUsage = currentMemUsage - initMemUsage; if (memUsage < 0) memUsage = 0; Console.WriteLine("Elapsed time: {0}, Memory Usage: {1:N2} KB", stopwatch.Elapsed, memUsage / 1024); }
انجام آزمایش
در مثال زیر، ابتدا یک میلیون شیء User ایجاد میشوند و سپس هربار توسط روشهای مختلفی به شیء User دیگری نگاشت میشوند:
static void Main(string[] args) { var length = 1000000; var users = new List<User>(length); for (var i = 0; i < length; i++) { var user = new User { Id = i, UserName = "User" + i, Password = "1" + i + "2" + i, LastLogin = DateTime.Now }; users.Add(user); } Console.WriteLine("Custom mapping"); RunActionMeasurePerformance(() => { var userList = users.Select( o => new User { Id = o.Id, UserName = o.UserName, Password = o.Password, LastLogin = o.LastLogin }).ToList(); }); Console.WriteLine("EmitMapper mapping"); RunActionMeasurePerformance(() => { var map = EmitMapper.ObjectMapperManager.DefaultInstance.GetMapper<User, User>(); var emitUsers = users.Select(o => map.Map(o)).ToList(); }); Console.WriteLine("ValueInjecter mapping"); RunActionMeasurePerformance(() => { var valueUsers = users.Select(o => (User)new User().InjectFrom(o)).ToList(); }); Console.WriteLine("AutoMapper mapping, DynamicMap using List"); RunActionMeasurePerformance(() => { var userMap = Mapper.DynamicMap<List<User>>(users).ToList(); }); Console.WriteLine("AutoMapper mapping, Map using List"); RunActionMeasurePerformance(() => { var userMap = Mapper.Map<List<User>>(users).ToList(); }); Console.WriteLine("AutoMapper mapping, Map using IEnumerable"); RunActionMeasurePerformance(() => { var userMap = Mapper.Map<IEnumerable<User>>(users).ToList(); }); Console.ReadKey(); }
خروجی آزمایش
در ادامه یک نمونهی خروجی نهایی را مشاهده میکنید:
Custom mapping Elapsed time: 00:00:00.4869463, Memory Usage: 58,848.00 KB EmitMapper mapping Elapsed time: 00:00:00.6068193, Memory Usage: 62,784.00 KB ValueInjecter mapping Elapsed time: 00:00:15.6935578, Memory Usage: 21,140.00 KB AutoMapper mapping, DynamicMap using List Elapsed time: 00:00:00.6028971, Memory Usage: 7,164.00 KB AutoMapper mapping, Map using List Elapsed time: 00:00:00.0106244, Memory Usage: 680.00 KB AutoMapper mapping, Map using IEnumerable Elapsed time: 00:00:01.5954456, Memory Usage: 40,248.00 KB
ValueInjecter از همه کندتر است.
EmitMapper از AutoMapper سریعتر است (البته فقط در بعضی از حالتها).
سرعت AutoMapper زمانیکه نوع آرگومان ورودی به آن به IEnumerable تنظیم شود، نسبت به حالت استفاده از List معمولی، به مقدار قابل توجهی کندتر است. زمانیکه از List استفاده شده، سرعت آن از سرعت حالت نگاشت دستی (مورد اول) هم بیشتر است.
متد DynamicMap اندکی کندتر است از متد Map.
در این بین اگر ValueInjecter را از لیست حذف کنیم، به نمودار ذیل خواهیم رسید (اعداد آن برحسب ثانیه هستند):
البته حین انتخاب یک کتابخانه، باید به آخرین تاریخ به روز شدن آن نیز دقت داشت و همچنین میزان استقبال جامعهی برنامه نویسها و از این لحاظ، AutoMapper نسبت به سایر کتابخانههای مشابه در صدر قرار میگیرد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید:
AM_Sample06.zip