Curry and Partial methods
public static class CurryMethodExtensions { public static Func< A, Func< B, Func< C, R > > > Curry< A, B, C, R >( this Func< A, B, C, R > f ) { return a => b => c => f( a, b, c ); } }
Func< int, int, int, int > addNumbers = ( x, y, z ) => x + y + z; var f1 = addNumbers.Curry(); Func< int, Func< int, int > > f2 = f1( 3 ); Func< int, int > f3 = f2( 4 ); Console.WriteLine( f3( 5 ) );
public static class CurryMethodExtensions { public static Func< C, R > Partial< A, B, C, R >( this Func< A, B, C, R > f, A a, B b ) { return c => f( a, b, c ); } }
Func< int, int, int, int > sumNumbers = ( x, y, z ) => x + y + z; Func< int, int > f4 = sumNumbers.Partial( 3, 4 ); Console.WriteLine( f4( 5 ) );
WeakReference
var obj = new WeakReferenceTest { FirstName = "Vahid" }; var w = new WeakReference(obj); obj = null; GC.Collect(); var weakReferenceTest = w.Target as WeakReferenceTest; if ( weakReferenceTest != null ) Console.WriteLine( weakReferenceTest.FirstName );
Lazy<T>
public abstract class ThreadSafeLazyBaseSingleton< T > where T : new() { static readonly Lazy< T > lazy = new Lazy< T >( () => new T() ); public static T Instance => lazy.Value; }
BigInteger
var positiveString = "91389681247993671255433422114345532000000"; var negativeString = "-9031583741089631207100208803453423537140000"; var posBigInt = BigInteger.Parse( positiveString ); Console.WriteLine( posBigInt ); var negBigInt = BigInteger.Parse( negativeString ); Console.WriteLine( negBigInt );
using System.Data.Entity.Core.Metadata.Edm;
public class SoftDeleteAttribute : Attribute { public string ColumnName { get; set; } public SoftDeleteAttribute(string column) { ColumnName = column; } public static string GetSoftDeleteColumnName(EdmType type) { MetadataProperty column = type.MetadataProperties.Where(x => x.Name.EndsWith("customannotation:SoftDeleteColumnName")).SingleOrDefault(); return column == null ? null : (string)column.Value; } }
سؤال: متادیتای "customannotation:SoftDeleteColumnName" از کجا آمد؟ برای پاسخ به این سوال کافیست ادامهی مطلب را کامل مطالعه نمایید.
[SoftDelete("IsDeleted")] public class TblUser { [Key] public int TblUserID { get; set; } [MaxLength(30)] public string Name { get; set; } public bool IsDeleted { get; set; } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { var Conv = new AttributeToTableAnnotationConvention<SoftDeleteAttribute, string>( "SoftDeleteColumnName", (type, attribute) => attribute.Single().ColumnName); modelBuilder.Conventions.Add(Conv); }
در پنجره باز شده به قسمت سوم یعنی <StorageModels> مراجعه نمایید و بایستی گزینه زیر را مشاهده نمایید .
<EntityType Name="TblUser" customannotation:SoftDeleteColumnName="IsDeleted">
تا اینجای کار ما توانستیم یک Annotation جدید را به Ef اضافه نماییم .
در مرحله بعد بایستی به Ef دستور دهیم که در تولید Query بر روی این Entity، این مورد را نیز لحاظ کند.
برای این کار کلاسی را ایجاد مینماییم که از اینترفیس IDbCommandTreeInterceptor ارث بری مینماید. مانند کد زیر :
public class SoftDeleteInterceptor : IDbCommandTreeInterceptor { public void TreeCreated(DbCommandTreeInterceptionContext interceptionContext) { if (interceptionContext.OriginalResult.DataSpace == System.Data.Entity.Core.Metadata.Edm.DataSpace.SSpace) { var QueryCommand = interceptionContext.Result as DbQueryCommandTree; if (QueryCommand != null) { var newQuery = QueryCommand.Query.Accept(new SoftDeleteQueryVisitor()); interceptionContext.Result = new DbQueryCommandTree(QueryCommand.MetadataWorkspace, QueryCommand.DataSpace, newQuery); } } } }
در ابتدا تشخیص داده میشود که نوع خروجی Query آیا از نوع Storage Model است . ( برای توضیحات بیشتر ) سپس پرس و جوی تولید شده را با استفاده از الگوی visitor تغییر داده و Query جدید را تولید نموده و در انتها Query جدیدی را به جای Query قبلی جایگزین مینماییم.
در اینجا ما نیاز به داشتن کلاس SoftDeleteQueryVisitor برای تغییر دادن Query و اضافه نمودن IsDeleted <>1 به Query میباشیم.
یک کلاس دیگری با نام SoftDeleteQueryVisitor به شکل زیر به برنامه اضافه مینماییم.
public class SoftDeleteQueryVisitor : DefaultExpressionVisitor { public override DbExpression Visit(DbScanExpression expression) { var column = SoftDeleteAttribute.GetSoftDeleteColumnName(expression.Target.ElementType); if (column!=null) { var Binding = DbExpressionBuilder.Bind(expression); return DbExpressionBuilder.Filter(Binding, DbExpressionBuilder.NotEqual(DbExpressionBuilder.Property(DbExpressionBuilder.Variable(Binding.VariableType, Binding.VariableName), column), DbExpression.FromBoolean(true))); } else { return base.Visit(expression); } } }
در نهایت برای اینکه EF تشخیص دهد که یکچنین Interceptor ایی وجود دارد، بایستی در کلاس DbContextConfig، کلاس SoftDeleteInterceptor را اضافه نماییم؛ همانند کد زیر:
public class DbContextConfig : DbConfiguration { public DbContextConfig() { AddInterceptor(new SoftDeleteInterceptor()); } }
تا اینجا در تمام Queryهای تولید شده بر روی Entity که با خاصیت SoftDelete مزین شده است، مقدار IsDeleted <> 1 را به صورت اتوماتیک اعمال مینماید. حتی به صورت هوشمند چنانچه این موجودیت در یک Join استفاده شده باشد این شرط را قبل از Join به Query تولید شده اضافه مینماید.
در مقاله بعدی در مورد تغییر کد Remove به کد Update توضیح داده خواهد شد.
برای مطالعه بیشتر
Entity Framework: Building Applications with Entity Framework 6
تهیه سرویسی برای رندر کردن Razor Viewها به صورت یک رشته
در ادامه کدهای کامل سرویسی را که توسط RazorViewEngine کار رندر کردن یک View و تبدیل آنرا به رشته انجام میدهد، ملاحظه میکنید:
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection; using System.IO; using System.Threading.Tasks; using System; namespace WebToolkit { public static class RazorViewToStringRendererExtensions { public static IServiceCollection AddRazorViewRenderer(this IServiceCollection services) { services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddScoped<IViewRendererService, ViewRendererService>(); return services; } } public interface IViewRendererService { Task<string> RenderViewToStringAsync(string viewNameOrPath); Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model); } /// <summary> /// Modified version of: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.RenderViewToString/RazorViewToStringRenderer.cs /// </summary> public class ViewRendererService : IViewRendererService { private readonly IRazorViewEngine _viewEngine; private readonly ITempDataProvider _tempDataProvider; private readonly IServiceProvider _serviceProvider; private readonly IHttpContextAccessor _httpContextAccessor; public ViewRendererService( IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider, IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; _serviceProvider = serviceProvider; _httpContextAccessor = httpContextAccessor; } public Task<string> RenderViewToStringAsync(string viewNameOrPath) { return RenderViewToStringAsync(viewNameOrPath, string.Empty); } public async Task<string> RenderViewToStringAsync<TModel>(string viewNameOrPath, TModel model) { var actionContext = getActionContext(); var viewEngineResult = _viewEngine.FindView(actionContext, viewNameOrPath, isMainPage: false); if (!viewEngineResult.Success) { viewEngineResult = _viewEngine.GetView("~/", viewNameOrPath, isMainPage: false); if (!viewEngineResult.Success) { throw new FileNotFoundException($"Couldn't find '{viewNameOrPath}'"); } } var view = viewEngineResult.View; using (var output = new StringWriter()) { var viewDataDictionary = new ViewDataDictionary<TModel>(new EmptyModelMetadataProvider(), new ModelStateDictionary()) { Model = model }; var viewContext = new ViewContext( actionContext, view, viewDataDictionary, new TempDataDictionary(actionContext.HttpContext, _tempDataProvider), output, new HtmlHelperOptions()); await view.RenderAsync(viewContext).ConfigureAwait(false); return output.ToString(); } } private ActionContext getActionContext() { var httpContext = _httpContextAccessor?.HttpContext; if (httpContext != null) { return new ActionContext(httpContext, httpContext.GetRouteData(), new ActionDescriptor()); } httpContext = new DefaultHttpContext { RequestServices = _serviceProvider }; return new ActionContext(httpContext, new RouteData(), new ActionDescriptor()); } } }
اصل این کد متعلق است به مایکروسافت در اینجا. اما در کدهای فوق سه قسمت آن بهبود یافتهاست:
الف) به سازندهی کلاس، سرویس IHttpContextAccessor نیز تزریق شدهاست تا بتوان به HttpContext و اطلاعات آن دسترسی یافت. حالت پیش فرض آن، استفاده از new DefaultHttpContext است. در این حالت اگر در قالبهای ایمیلهای خود از Url.Action استفاده کنید، استثنای index out of range مربوط به یافت نشدن مسیریابیها را دریافت خواهید کرد. علت اینجا است که new DefaultHttpContext حاوی اطلاعات مسیریابی درخواست جاری سیستم نیست. به همین جهت توسط IHttpContextAccessor در متد getActionContext، کار مقدار دهی قسمت مسیریابی صورت گرفتهاست.
ب) در کدهای مثال اصلی، فقط viewEngine.FindView ذکر شدهاست. این متد حالتهای یافتن Viewهایی را به صورت FolderName/ViewName، پشتیبانی میکند. اگر بخواهیم یک مسیر کامل را مانند "Areas/Identity/Views/EmailTemplates/_RegisterEmailConfirmation.cshtml/~" ذکر کنیم، کار نمیکند. به همین جهت در ادامه، بررسی viewEngine.GetView نیز اضافه شدهاست تا این نقصان را پوشش دهد.
ج) یک overload اضافهتر هم جهت رندر یک View بدون مدل نیز به آن اضافه شدهاست.
روش استفادهی از سرویس ViewRenderer
اسمبلی که این سرویس در آن تعریف میشود باید دارای وابستگیهای ذیل باشد:
{ "dependencies": { "Microsoft.AspNetCore.Mvc.ViewFeatures": "1.1.0", "Microsoft.AspNetCore.Mvc.Razor": "1.1.0" } }
public void ConfigureServices(IServiceCollection services) { services.AddRazorViewRenderer(); }
پس از ثبت سرویسهای مورد نیاز، اکنون میتوان سرویس IViewRendererService را به سازندهی کنترلرها و یا کلاسهای برنامه تزریق و از متدهای RenderViewToStringAsync آن استفاده کرد:
public class RenderController : Controller { private readonly IViewRendererService _viewRenderService; public RenderController(IViewRendererService viewRenderService) { _viewRenderService = viewRenderService; } public async Task<IActionResult> RenderInviteView() { var viewModel = new InviteViewModel { UserId = "1", UserName = "Vahid" }; var emailBody = await _viewRenderService.RenderViewToStringAsync("EmailTemplates/Invite", viewModel).ConfigureAwait(false); //todo: send emailBody return Content(emailBody); } }
چند نکتهی تکمیلی در مورد قالبهای ایمیل
- پیش فرض این سرویس، یافتن Viewها در پوشهی Views است؛ مانند: Views\EmailTemplates\_EmailsLayout.cshtml
مگر اینکه مسیر آنرا به صورت کامل توسط filename.cshtml/.../~ ذکر کنید و در این حالت ذکر پسوند فایل الزامی است.
- ایمیلها میتوانند دارای Layout هم باشند. برای مثال فایل Views\EmailTemplates\_EmailsLayout.cshtml را با محتوای ذیل ایجاد کنید:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Language" content="fa" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <style type='text/css'> .main { font-size: 9pt; font-family: Tahoma; } </style> </head> <body bgcolor="whitesmoke" style="font-size: 9pt; font-family: Tahoma; background-color: whitesmoke; direction: rtl;"> <div class="main">@RenderBody()</div> </body> </html>
به علاوه در اینجا جهت ارسال ایمیلها باید هر نوع شیوه نامهای، به صورت صریح قید شود (inline css) و نباید فایل css ایی را لینک کنید.
- پس از اینکه فایل layout خاص ارسال ایمیلهای خودتان را طراحی کردید، اکنون قالب یکی از ایمیلهای برنامه، یک چنین فرمتی را پیدا میکند که Layout در ابتدای آن ذکر شدهاست:
@using Sample.ViewModels @model RegisterEmailConfirmationViewModel @{ Layout = "~/Views/EmailTemplates/_EmailsLayout.cshtml"; } با سلام <br /> اکانت شما با مشخصات ذیل ایجاد گردید: ....
<a style="direction:ltr" href="@Url.Action("Index", "Home", values: new { area = "" }, protocol: this.Context.Request.Scheme)">@Model.EmailSignature</a>
به تصویر زیر توجه بفرمایید. ابتدا دادهها به دو دسته ششتایی تقسیم شدن(ستون ntl)، سپس هر کدام از این دستهها نیز به سه دسته دوتایی تقسیم شدن(ستون grp) هدف ما تولید دو ستون ntl و grp توسط query میباشد.
برای بدست آوردن مقادیر دو ستون مذکور روشهای متنوعی وجود دارد که برخی از آنها را در انیجا پوشش میدم.
قبل از هر چیزی ابتدا جدول را ایجاد و 12 سطر زیر را در آن انتشار دهید:
CREATE TABLE T (nbr INT NOT NULL); INSERT T VALUES (1), (2), (3), (4), (5), (6), (7), (8), (9), (10), (11), (12);
روش اول:
این روش، تعمیم پذیری و پویایی ندارد و برای هر سناریویی مناسب نخواهد بود. ولی از آنجایی که دیدم کوئری زیر میتواند یک نمونه از کاربرد Ntile باشه آن را مطرح کردم.
SELECT nbr, ntl, NTILE(3) OVER(PARTITION BY ntl ORDER BY nbr) AS grp FROM ( SELECT nbr, NTILE(2) OVER(ORDER BY nbr) ntl FROM T ) AS D;
تابع ntile بیرونی بر اساس دو عدد 1و 2 گروه بندی انجام داده و هر گروه را به 3 قسمت تقسیم میکند. قسمت اول 1، دوم 2 و سوم 3 خواهد بود.
لازم به ذکر است که باید خارج قسمت تقسیم تعدادسطرها بر عدد ntile یک عدد صحیح باشد تا خروجی مناسب داشته باشیم. و همچنین بایستی بدانیم که تعداد سطرهای جدول چنتاست تا آن را به گونه ای تقسیم کنیم که خارج قسمت برابر شود با عدد مورد نظر ما یعنی 6.
روش دوم:
در این روش بر خلاف روش قبل که همه چیز توسط تابع بدست میآمد باید خودمان دست به کار شویم و فرمولی را بدست آوریم که نتیجه مورد نظر را تولید کند.
برای حل این مساله ابتدا باید سطرهای جدول را 6 تا 6 تا سوا کنیم و عناصر هر دسته را شماره گذاری کنیم (از 1 تا 6 بر اساس ترتیب مقدار nbr) سپس با کمک سایر فرمولها دستهها را دوتا دوتا شماره گذاری میکنیم.
به تصویر زیر توجه بفرمایید:
در کادر نارنجی رنگ همانطور که اشاره شد ما سطرهای شمارگذاری شده ای داریم که در رنج 1 تا 6 هستند. و در کادر بنفش ستون مورد نظر ما قرار دارد. ستون بنفش با کمک ستون نارنجی بدست آمده است.
اگر تقسیم صحیح را div و باقیمانده صحیح را mod بگیریم فرولهای مورد نظر به این شرح خواهد بود:
طبق کوئری زیر ستون نارنجی(rnk1/rnk2) را به دو طریق میتوان ایجاد نمود و ستون بنفش(grp1/grp2) را نیز به دو طریق میتوان ایجاد نمود.
SELECT nbr, rnk1, rnk2, (rnk1 + 1) / 2 AS grp2, (rnk1 - 1) / 2 + 1 AS grp2 FROM ( SELECT nbr, ROW_NUMBER() OVER(PARTITION BY (nbr + 5) / 6 ORDER BY nbr) rnk1, (nbr - 1) % 6 + 1 AS rnk2 FROM t )d
تنظیم minheight برای سطرهای جدول
سطح اول کش در NHibernate در یک تراکنش معنا پیدا میکند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنشهای جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.
الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریمها امکان دریافت فایلهای مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، میخواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامههای وب و دسکتاپ بدون مشکل کار میکند).
ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate میباشد. اینکار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...
ج) تعریف نوع کش در هنگام ایجاد نگاشتها
اگر از ClassMapها برای تعریف نگاشتها استفاده میکنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده میکنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;
namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
د) اعمال متد Cacheable به کوئریها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان میکنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنشهای جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();
ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینهتر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).
Value
// Behavior 1 object obj = null; bool objValueEqual = obj.Equals(null); // Behavior 2 object obj = null; Type objType = obj.GetType(); // Behavior 3 string str = (string)null; bool strType = str is string; // Behavior 4 int num = 5; Nullable<int> nullableNum = 5; bool typeEqual = num.GetType() == nullableNum.GetType(); // Behavior 5 Type inType = typeof(int); Type nullableIntType = typeof(Nullable<int>); bool typeEqual = inType == nullableIntType;
- در رفتار اول هرچند که متد Equals از شی null در دسترس است و با مقدار null مقایسه شده اما در زمان اجرا پیغام خطای NullReferenceException را خواهیم داشت.
- در رفتار دوم هم پیغام خطا را خواهیم داشت. شئ با مقدار null، در زمان اجرا هیچ نوعی را برنمیگرداند.
- در رفتار سوم هر چند که مقدار null صریحا به رشته تبدیل شده و برای چاپ متغیر str پیام خطایی را نخواهیم داشت، اما متغیر strType در خروجی، false خواهد بود. همانطور که در رفتار دوم گفته شد، شیء با مقدار null هیچ نوعی را برنمیگرداند.
- خروجی رفتار چهارم true خواهد بود. به این صورت که هر دو از نوع System.int32 خواهند بود.
- در رفتار پنجم اگر از نوعها، خروجی جداگانه بگیریم، خواهیم دیدکه نوع int از System.int32 و <Nullable<int از نوع System.Nullable`1[System.Int32] میباشند، در نتیجه خروجی false است. اشیای nullable بعد از اینکه مقداری مشخص را دریافت کردند، به صورت یک شیء غیر nullable رفتار خواهند کرد.
مدیریت مقادیر null در سربارگذاری متدها
static void Main(string[] args) { Console.WriteLine(Method(null)); Console.ReadLine(); } private static string Method(object obj) { return "Object parameter"; } private static string Method(string str) { return "String parameter"; }
رفتارهای ()Math.Round
var rounded = Math.Round(1.5); // 2 var rounded = Math.Round(2.5); // 2 var rounded = Math.Round(2.5, MidpointRounding.ToEven); // 2 var rounded = Math.Round(2.5, MidpointRounding.AwayFromZero); // 3 var value = 1.4f; var rounded = Math.Round(value + 0.1f); // 1
مقدار دهی اولیه کلاسها
- فیلدهای استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
- سازنده استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
- فیلدهایی از کلاس که در نمونه ساخته شده در دسترس قرار میگیرند.
- سازنده کلاس که در زمان ایجاد یک نمونه از کلاس در دسترس قرار میگیرد.
public static class Config { public static bool ThrowException { get; set; } = true; } public class FailingClass { static FailingClass() { if (Config.ThrowException) { throw new InvalidOperationException(); } } }
try { var failedInstance = new FailingClass(); } catch (TypeInitializationException) { } Config.ThrowException = false; var instance = new FailingClass();
public class BaseClass { { public BaseClass() { VirtualMethod(1); } public virtual int VirtualMethod(int dividend) { return dividend / 1; } } public class DerivedClass : BaseClass { int divisor; public DerivedClass() { divisor = 1; } public override int VirtualMethod(int dividend) { return base.VirtualMethod(dividend / divisor); } }
چند ریختی
class Program { static void Main(string[] args) { var instance = new DerivedClass(); var result = instance.Method(); result = ((BaseClass)instance).Method(); Console.WriteLine(instance + " -> " + result); // Derived Class ... -> Method in BaseClass Console.ReadLine(); } } public class BaseClass { public virtual string Method() { return "Method in BaseClass"; } } public class DerivedClass : BaseClass { public override string ToString() { return "Derived Class ... "; } public new string Method() { return "Method in DerivedClass"; } }
class Program { static void Main(string[] args) { var instance = new DerivedClass(); var result = instance.Method(); // -> Method in DerivedClass result = ((IInterface)instance).Method(); // -> Method belonging to IInterface Console.WriteLine(result); Console.ReadLine(); } } public interface IInterface { string Method(); } public class DerivedClass : IInterface { public string Method() { return "Method in DerivedClass"; } string IInterface.Method() { return "Method belonging to IInterface"; } }
Iterators
private IEnumerable<int> GetEnumerable(StringBuilder log) { using (var test = new Test(log)) { return Enumerable.Range(1, 5); } }
var log = new StringBuilder(); foreach (var number in GetEnumerable(log)) { log.AppendLine($"{number}"); }
Created Disposed 1 2 3 4 5
using (var test = new Test(log)) { foreach (var i in Enumerable.Range(1,5)) { yield return i; } }
Created 1 2 3 4 5 Disposed
public class ProductQuery { public dynamic GetProductsByCustomerId(int id) { return new ExpandoObject(); } } public class CustomerQuery { private readonly ProductQuery _productQuery; public CustomerQuery(ProductQuery productQuery) { _productQuery = productQuery; } public dynamic GetProducts(int customerId) { return _productQuery.GetProductsByCustomerId(customerId); } } public static class Programm { static void Main(string[] args) { var query = new CustomerQuery(new ProductQuery()); var products = query.GetProducts(1); } }
چرا چنین بویی به راه میافتد
- انتقال مسئولیتهای یک کلاس به کلاسی دیگر به مرور زمان و تبدیل متدهای آن به هدایت کنندگان فراخوانی.
- عدم تشخیص درست مسئولیتهای یک کلاس و اجبار به افزودن هدایت کنندگان فراخوانی در کلاسهای دیگر. این حالت معمولا زمانی اتفاق میافتد که یک کلاس، مسئولیتهای زیادی داشته باشد و کلاسهای مختلف، صرفا نیاز به هدایت فراخوانی به این کلاس را داشته باشند.
- عدم استفاده مناسب از الگوهای طراحی.
روشهای اصلاح این کد بد بو
چه کدهایی دلال نیستند
- Chain of responsibility
- Decorator
- Proxy
- Adapter
Please open an issue in the library repository to alert its author and ask them to package the library using the Angular Package Format (https://goo.gl/jB3GVv).
مراحل ایجاد یک پروژهی «کتابخانه» توسط Angular CLI 6.0
مرحلهی اول ایجاد یک پروژهی کتابخانه، مانند قبل، توسط دستور ng new و ایجاد یک پروژهی دلخواه جدید است:
ng new my-lib-test
پس از ایجاد پروژهی my-lib-test توسط دستور فوق و وارد شدن به پوشهی اصلی آن توسط خط فرمان، میتوان با اجرای دستور زیر، پروژههای دیگری را به پروژهی جاری افزود:
ng generate application my-app-name
ng generate library my-lib
همچنین یک پوشهی جدید به نام projects نیز ایجاد شده و پروژهی my-lib داخل آن قرار گرفتهاست.
فایل جدید public_api.ts
پس از ایجاد کتابخانهی جدید «my-lib»، فایل جدیدی به نام projects\my-lib\src\public_api.ts نیز به آن اضافه شدهاست:
با این محتوا:
/* * Public API Surface of my-lib */ export * from './lib/my-lib.service'; export * from './lib/my-lib.component'; export * from './lib/my-lib.module';
برای مثال اگر فایل جدید projects\my-lib\src\lib\my-lib.models.ts را به این کتابخانه اضافه کنیم که شامل تعدادی مدل و اینترفیس قابل دسترسی توسط استفاده کنندگان باشد، باید یک سطر زیر را به انتهای فایل public_api.ts اضافه کنیم:
export * from './lib/my-lib.models';
این پروژهی کتابخانه حتی به همراه فایلهای package.json, tsconfig.json, tslint.json مخصوص به خود نیز میباشد تا بتوان آنها را صرفا جهت این پروژه سفارشی سازی کرد.
ساختار my-lib.service پیشفرض یک پروژهی کتابخانه
اگر به فایل projects\my-lib\src\lib\my-lib.service.ts دقت کنیم:
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root' }) export class MyLibService { constructor() { } }
شاید بپرسید چرا؟ هدف اصلی از آن، بهبود فرآیند tree-shaking یا حذف کدهای مرده و استفاده نشدهاست. ممکن است سرویسی را تعریف کنید، اما در برنامه استفاده نشود. این حالت خصوصا در پروژههای کتابخانههای ثالث ممکن است زیاد رخ دهد. به همین جهت با ارائهی این قابلیت، امکان حذف سادهتر سرویسهایی که در برنامه استفاده نشدهاند از خروجی نهایی کامپایل شده، وجود خواهد داشت.
چگونه به پروژهی کتابخانهی جدید، یک کامپوننت جدید را اضافه کنیم؟
تمام دستورات Angular CLI، در اینجا نیز کار میکنند. تنها تفاوت آنها، ذکر صریح نام پروژهی مورد استفاده است:
ng generate component show-data --project=my-lib
البته در اینجا باید فایل my-lib.module.ts را اندکی ویرایش کرد و ShowDataComponent را به قسمت exports نیز افزود:
@NgModule({ imports: [ CommonModule, HttpClientModule ], declarations: [MyLibComponent, ShowDataComponent], exports: [MyLibComponent, ShowDataComponent] }) export class MyLibModule { }
همچنین قسمت imports آن نیز به صورت پیشفرض خالی است. اگر نیاز است با ngIf کار کنید، باید CommonModule را در اینجا قید کنید و اگر نیاز است تبادلات HTTP وجود داشته باشد، ذکر HttpClientModule نیز ضروری است.
مرحلهی ساخت پروژه
پیش از استفادهی از این پروژهی کتابخانه، باید آنرا build کرد:
ng build my-lib
پس از اجرای این دستور، خروجی ذیل مشاهده میشود:
Building Angular Package Building entry point 'my-lib' Rendering Stylesheets Rendering Templates Compiling TypeScript sources through ngc Downleveling ESM2015 sources through tsc Bundling to FESM2015 Bundling to FESM5 Bundling to UMD Minifying UMD bundle Remap source maps Relocating source maps Copying declaration files Writing package metadata Removing scripts section in package.json as it's considered a potential security vulnerability. Built my-lib Built Angular Package! - from: D:\my-lib-test\projects\my-lib - to: D:\my-lib-test\dist\my-lib
استفادهی از کتابخانهی تولید شده
پس از پایان موفقیت آمیز مرحلهی Build، اکنون نوبت به استفادهی از این کتابخانه است. استفادهی از آن نیز همانند تمام کتابخانهها و وابستگیهای ثالثی است که تا پیش از این از آنها استفاده کردهایم. برای مثال ماژول آنرا در قسمت imports مربوط به NgModule کلاس AppModule معرفی میکنیم. برای این منظور به فایل src\app\app.module.ts مراجعه کرده و MyLibModule را به نحو ذیل اضافه میکنیم:
import { MyLibModule } from "my-lib"; @NgModule({ imports: [ BrowserModule, MyLibModule ] }) export class AppModule { }
اما سؤال اینجا است که آیا این پوشه پس از build، داخل پوشهی node_modules نیز کپی شدهاست؟ پاسخ آن خیر است و برای مدیریت خودکار آن، به صورت زیر عمل شدهاست:
اگر به فایل tsconfig.json اصلی و واقع در ریشهی workspace دقت کنید، پس از اجرای دستور «ng generate library my-lib»، قسمت paths آن نیز به صورت خودکار ویرایش شدهاست:
{ "compilerOptions": { "paths": { "my-lib": [ "dist/my-lib" ] } } }
برای نمونه اگر شارهگر ماوس را بر روی my-lib قرار دهید، به درستی مسیر خوانده شدن آن، تشخیص داده میشود.
به این ترتیب مسیر این import، چه در این پروژهی محلی و چه برای کسانیکه پوشهی dist/my-lib را به صورت یک بستهی npm جدید دریافت کردهاند، یکی خواهد بود.
در ادامه اگر به فایل app.component.html مراجعه کرده و selector کامپوننت show-data را به آن اضافه کنیم:
<lib-show-data></lib-show-data>
توزیع کتابخانهی ایجاد شده برای عموم
برای اینکه این کتابخانهی تولیدی را در اختیار عموم، در سایت npm قرار دهیم، ابتدا باید کتابخانه را در حالت production build تولید و سپس آنرا publish کرد:
ng build my-lib --prod cd dist/my-lib npm publish
البته دستور آخر نیاز به ایجاد یک اکانت در سایت npm و وارد شدن به آنرا دارد. جزئیات بیشتر آن در اینجا.