نظرات مطالب
پیاده سازی Option یا Maybe در #C
ممنون از شما ...
هدف بنده از این مطلب آشنایی با این ایده (maybe or optional )که شما را به دنیای برنامه نویسی تابعی در#C هدایت کند اون هم با ساده‌ترین روش و مثال بود نه پیاده سازی خاصی چون در طول زمان پیاده سازی‌ها متفاوت خواهند شد ولی مفاهیم ثابت می‌مونن.
این  کتابخانه  و کتابش    هم خوبه اگر دوست داشتید. 
مطالب
امکان تعریف توابع خاص بانک‌های اطلاعاتی در EF Core
یکی از اهداف کار با ORMها، رسیدن به کدی قابل ترجمه و استفاده‌ی توسط تمام بانک‌های اطلاعاتی ممکن است و یکی از الزامات رسیدن به این هدف، صرفنظر کردن از قابلیت‌های بومی بانک‌های اطلاعاتی است که در سایر بانک‌های اطلاعاتی دیگر معادلی ندارند. برای مثال SQL Server به همراه توابع توکاری مانند datediff و datepart برای کار با زمان و تاریخ است؛ اما این توابع را به صورت مستقیم نمی‌توان در ORMها استفاده کرد. چون به محض استفاده‌ی از آن‌ها، کد تهیه شده دیگر قابلیت انتقال به سایر بانک‌های اطلاعاتی را نخواهد داشت. اما ... اگر این هدف را نداشته باشیم، چطور؟ آیا می‌توان یک تابع DateDiff سفارشی را برای EF Core تهیه نمود و از تمام قابلیت‌های بومی آن در کوئری‌های LINQ استفاده کرد؟ بله! یک چنین قابلیتی تحت عنوان DbFunctions در EF Core پشتیبانی می‌شود که روش تهیه‌ی آن‌ها را در این مطلب بررسی خواهیم کرد.


معرفی موجودیت Person

در مثال این مطلب قصد داریم، معادل توابع بومی مخصوص SQL Server را که امکان کار با DateTime را مهیا می‌کنند، در EF Core تعریف کنیم. به همین جهت نیاز به موجودیتی داریم که دارای خاصیتی از این نوع باشد:
using System;

namespace EFCoreDbFunctionsSample.Entities
{
    public class Person
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public DateTime AddDate { get; set; }
    }
}


گزارشگیری بر اساس تعداد روز گذشته‌ی از ثبت نام

اکنون فرض کنید می‌خواهیم گزارشی را از تمام کاربرانی که در طی 10 روز قبل ثبت نام کرده‌اند، تهیه کنیم. اگر کوئری زیر را برای این منظور تهیه کنیم:
var usersInfo = context.People.Where(person => (DateTime.Now - person.AddDate).Days <= 10).ToList();
با استثنای زیر متوقف خواهیم شد:
'The LINQ expression 'DbSet<Person>.Where(p => (DateTime.Now - p.AddDate).Days <= 10)'
could not be translated. Either rewrite the query in a form that can be translated,
or switch to client evaluation explicitly by inserting a call to either
AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync().
See https://go.microsoft.com/fwlink/?linkid=2101038 for more information.'
عنوان می‌کند که یک چنین کوئری LINQ ای قابلیت ترجمه‌ی به SQL را ندارد. اما ... نکته‌ی مهم اینجا است که خود SQL Server یک چنین توانمندی را به صورت توکار دارا است:
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= 10
برای انجام کوئری مدنظر فقط کافی است از تابع DATEDIFF توکار آن با پارامتر Day، استفاده کنیم تا لیست تمام کاربران ثبت نام کرده‌ی در طی 10 روز قبل را بازگشت دهد. اکنون سؤال اینجا است که آیا می‌توان چنین تابعی را به EF Core معرفی کرد؟


روش تعریف تابع DATEDIFF سفارشی در EF Core

برای تعریف متد DateDiff مخصوص EF Core، ابتدا باید یک کلاس static را تعریف کرد و سپس تنها امضای این متد را، معادل امضای تابع توکار SQL Server تعریف کرد. این متد نیازی نیست تا پیاده سازی را داشته باشد. به همین جهت بدنه‌ی آن‌را صرفا با یک throw new InvalidOperationException مقدار دهی می‌کنیم. هدف از این متد، استفاده‌ی از آن در LINQ Expressions است و قرار نیست به صورت مستقیمی بکار گرفته شود:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public enum SqlDateDiff
    {
        Year,
        Quarter,
        Month,
        DayOfYear,
        Day,
        Week,
        Hour,
        Minute,
        Second,
        MilliSecond,
        MicroSecond,
        NanoSecond
    }

    public static class SqlDbFunctionsExtensions
    {
        public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end)
            => throw new InvalidOperationException($"{nameof(SqlDateDiff)} method cannot be called from the client side.");
        public static readonly MethodInfo SqlDateDiffMethodInfo = typeof(SqlDbFunctionsExtensions)
            .GetRuntimeMethod(
                nameof(SqlDbFunctionsExtensions.SqlDateDiff),
                new[] { typeof(SqlDateDiff), typeof(DateTime), typeof(DateTime) }
            );
    }
}
در اینجا علاوه بر تعریف امضای متد DateDiff که در اینجا SqlDateDiff نام گرفته‌است، فیلد SqlDateDiffMethodInfo را نیز مشاهده می‌کنید. در حین تعریف و معرفی DbFunctions سفارشی به EF Core، متدهایی که اینکار را انجام می‌دهند، پارامترهای ورودی از نوع MethodInfo دارند. به همین جهت یک چنین تعریفی انجام شده‌است.


روش معرفی تابع DATEDIFF سفارشی به EF Core

پس از تعریف امضای متد معادل DateDiff، اکنون نوبت به معرفی آن به EF Core است:
namespace EFCoreDbFunctionsSample.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.HasDbFunction(SqlDbFunctionsExtensions.SqlDateDiffMethodInfo)
                .HasTranslation(args =>
                {
                    var parameters = args.ToArray();
                    var param0 = ((SqlConstantExpression)parameters[0]).Value.ToString();
                    return SqlFunctionExpression.Create("DATEDIFF",
                        new[]
                        {
                            new SqlFragmentExpression(param0), // It should be written as DateDiff(day, ...) and not DateDiff(N'day', ...) .
                            parameters[1],
                            parameters[2]
                        },
                        SqlDbFunctionsExtensions.SqlDateDiffMethodInfo.ReturnType,
                        typeMapping: null);
                });
        }
    }
}
کار تعریف DbFunctions سفارشی توسط متد HasDbFunction صورت می‌گیرد. پارامتر این متد، همان MethodInfo معادل امضای تابع توکار مدنظر است.
سپس توسط متد HasTranslation، مشخص می‌کنیم که این متد به چه نحوی قرار است به یک عبارت SQL ترجمه شود. پارامتر args ای که در اینجا در اختیار ما قرار می‌گیرد، دقیقا همان پارامترهای متد public static int SqlDateDiff(SqlDateDiff interval, DateTime initial, DateTime end) هستند که در این مثال خاص، شامل سه پارامتر می‌شوند. پارامترهای دوم و سوم آن‌را به همان نحوی که دریافت می‌کنیم، به SqlFunctionExpression.Create ارسال خواهیم کرد. اما پارامتر اول را از نوع enum تعریف کرده‌ایم و همچنین قرار نیست به صورت 'N'day و رشته‌ای به سمت بانک اطلاعاتی ارسال شود، بلکه باید به همان نحو اصلی آن (یعنی day)، در کوئری نهایی درج گردد، به همین جهت ابتدا Value آن‌را استخراج کرده و سپس توسط SqlFragmentExpression عنوان می‌کنیم آن‌را باید به همین نحو درج کرد.
پارامتر اول متد SqlFunctionExpression.Create، باید دقیقا معادل نام متد توکار مدنظر باشد. پارامتر دوم آن، لیست پارامترهای این تابع است. پارامتر سوم آن، نوع خروجی این تابع است که از طریق MethodInfo معادل، قابل استخراج است.


استفاده‌ی از DbFunction سفارشی جدید در برنامه

پس از این تعاریف و معرفی‌ها، اکنون می‌توان متد سفارشی SqlDateDiff تهیه شده را به صورت مستقیمی در کوئری‌های LINQ استفاده کرد تا قابلیت ترجمه‌ی به SQL را پیدا کنند:
var sinceDays = 10;
users = context.People.Where(person =>
      SqlDbFunctionsExtensions.SqlDateDiff(SqlDateDiff.Day, person.AddDate, DateTime.Now) <= sinceDays).ToList();
/*
SELECT [p].[Id], [p].[AddDate], [p].[Name]
FROM [People] AS [p]
WHERE DATEDIFF(Day, [p].[AddDate], GETDATE()) <= @__sinceDays_0
*/


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: EFCoreDbFunctionsSample.zip
این کدها به همراه چند تابع سفارشی دیگر نیز هستند.
مسیرراه‌ها
NHibernate
      مطالب دوره‌ها
      انتقال خودکار Data Annotations از مدل‌ها به ViewModelهای ASP.NET MVC به کمک AutoMapper
      عموما مدل‌های ASP.NET MVC یک چنین شکلی را دارند:
      public class UserModel
      {
          public int Id { get; set; }
       
          [Required(ErrorMessage = "(*)")]
          [Display(Name = "نام")]
          [StringLength(maximumLength: 10, MinimumLength = 3, ErrorMessage = "نام باید حداقل 3 و حداکثر 10 حرف باشد")]
          public string FirstName { get; set; }
       
          [Required(ErrorMessage = "(*)")]
          [Display(Name = "نام خانوادگی")]
          [StringLength(maximumLength: 10, MinimumLength = 3, ErrorMessage = "نام خانوادگی باید حداقل 3 و حداکثر 10 حرف باشد")]
          public string LastName { get; set; }
      }
       و ViewModel مورد استفاده برای نمونه چنین ساختاری را دارد:
      public class UserViewModel
      {
            public string FirstName { get; set; }
            public string LastName { get; set; }
      }
      مشکلی که در اینجا وجود دارد، نیاز به کپی و تکرار تک تک ویژگی‌های (Data Annotations/Attributes) خاصیت‌های مدل، به خواص مشابه آن‌ها در ViewModel است؛ از این جهت که می‌خواهیم برچسب خواص ViewModel، از ویژگی Display دریافت شوند و همچنین اعتبارسنجی‌های فیلدهای اجباری و بررسی حداقل و حداکثر طول فیلدها نیز حتما اعمال شوند (هم در سمت کاربر و هم در سمت سرور).
      در ادامه قصد داریم راه حلی را به کمک جایگزین سازی Provider‌های توکار ASP.NET MVC با نمونه‌ی سازگار با AutoMapper، ارائه دهیم، به نحوی که دیگر نیازی نباشد تا این ویژگی‌ها را در ViewModelها تکرار کرد.


      قسمت‌هایی از ASP.NET MVC که باید جهت انتقال خودکار ویژگی‌ها تعویض شوند

      ASP.NET MVC به صورت توکار دارای یک ModelMetadataProviders.Current است که از آن جهت دریافت ویژگی‌های هر خاصیت استفاده می‌کند. می‌توان این تامین کننده‌ی ویژگی‌ها را به نحو ذیل سفارشی سازی نمود.
      در اینجا IConfigurationProvider همان Mapper.Engine.ConfigurationProvider مربوط به AutoMapper است. از آن جهت استخراج اطلاعات نگاشت‌های AutoMapper استفاده می‌کنیم. برای مثال کدام خاصیت Model به کدام خاصیت ViewModel نگاشت شده‌است. این‌کارها توسط متد الحاقی GetMappedAttributes انجام می‌شوند که در ادامه‌ی مطلب معرفی خواهد شد.
      public class MappedMetadataProvider : DataAnnotationsModelMetadataProvider
      {
          private readonly IConfigurationProvider _mapper;
       
          public MappedMetadataProvider(IConfigurationProvider mapper)
          {
              _mapper = mapper;
          }
       
          protected override ModelMetadata CreateMetadata(
              IEnumerable<Attribute> attributes,
              Type containerType,
              Func<object> modelAccessor,
              Type modelType,
              string propertyName)
          {
              var mappedAttributes =
                  containerType == null ?
                  attributes :
                  _mapper.GetMappedAttributes(containerType, propertyName, attributes.ToList());
              return base.CreateMetadata(mappedAttributes, containerType, modelAccessor, modelType, propertyName);
          }
      }

      شبیه به همین کار را باید برای ModelValidatorProviders.Providers نیز انجام داد. در اینجا یکی از تامین کننده‌های ModelValidator، از نوع DataAnnotationsModelValidatorProvider است که حتما نیاز است این مورد را نیز به نحو ذیل سفارشی سازی نمود. در غیراینصورت error messages موجود در ویژگی‌های تعریف شده، به صورت خودکار منتقل نخواهند شد.
      public class MappedValidatorProvider : DataAnnotationsModelValidatorProvider
      {
          private readonly IConfigurationProvider _mapper;
       
          public MappedValidatorProvider(IConfigurationProvider mapper)
          {
              _mapper = mapper;
          }
       
          protected override IEnumerable<ModelValidator> GetValidators(
              ModelMetadata metadata,
              ControllerContext context,
              IEnumerable<Attribute> attributes)
          {
       
              var mappedAttributes =
                  metadata.ContainerType == null ?
                  attributes :
                  _mapper.GetMappedAttributes(metadata.ContainerType, metadata.PropertyName, attributes.ToList());
              return base.GetValidators(metadata, context, mappedAttributes);
          }
      }

      و در اینجا پیاده سازی متد GetMappedAttributes را ملاحظه می‌کنید.
      ASP.NET MVC هر زمانیکه قرار است توسط متدهای توکار خود مانند Html.TextBoxFor, Html.ValidationMessageFor، اطلاعات خاصیت‌ها را تبدیل به المان‌های HTML کند، از تامین کننده‌های فوق جهت دریافت اطلاعات ویژگی‌های مرتبط با هر خاصیت استفاده می‌کند. در اینجا فرصت داریم تا ویژگی‌های مدل را از تنظیمات AutoMapper دریافت کرده و سپس بجای ویژگی‌های خاصیت معادل ViewModel درخواست شده، بازگشت دهیم. به این ترتیب ASP.NET MVC تصور خواهد کرد که ViewModel ما نیز دقیقا دارای همان ویژگی‌های Model است.
      public static class AutoMapperExtensions
      {
          public static IEnumerable<Attribute> GetMappedAttributes(
              this IConfigurationProvider mapper,
              Type viewModelType,
              string viewModelPropertyName,
              IList<Attribute> existingAttributes)
          {
              if (viewModelType != null)
              {
                  foreach (var typeMap in mapper.GetAllTypeMaps().Where(i => i.DestinationType == viewModelType))
                  {
                      var propertyMaps = typeMap.GetPropertyMaps()
                          .Where(propertyMap => !propertyMap.IsIgnored() && propertyMap.SourceMember != null)
                          .Where(propertyMap => propertyMap.DestinationProperty.Name == viewModelPropertyName);
       
                      foreach (var propertyMap in propertyMaps)
                      {
                          foreach (Attribute attribute in propertyMap.SourceMember.GetCustomAttributes(true))
                          {
                              if (existingAttributes.All(i => i.GetType() != attribute.GetType()))
                              {
                                  yield return attribute;
                              }
                          }
                      }
                  }
              }
       
              if (existingAttributes == null)
              {
                  yield break;
              }
       
              foreach (var attribute in existingAttributes)
              {
                  yield return attribute;
              }
          }
      }


      ثبت تامین کننده‌های سفارشی سازی شده توسط AutoMapper

      پس از تهیه‌ی تامین کننده‌های انتقال ویژگی‌ها، اکنون نیاز است آن‌ها را به ASP.NET MVC معرفی کنیم:
      protected void Application_Start()
      {
          AreaRegistration.RegisterAllAreas();
          WebApiConfig.Register(GlobalConfiguration.Configuration);
          FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
          RouteConfig.RegisterRoutes(RouteTable.Routes); 
       
          Mappings.RegisterMappings();
          ModelMetadataProviders.Current = new MappedMetadataProvider(Mapper.Engine.ConfigurationProvider);
       
          var modelValidatorProvider = ModelValidatorProviders.Providers
              .Single(provider => provider is DataAnnotationsModelValidatorProvider);
          ModelValidatorProviders.Providers.Remove(modelValidatorProvider);
          ModelValidatorProviders.Providers.Add(new MappedValidatorProvider(Mapper.Engine.ConfigurationProvider));
      }
      در اینجا ModelMetadataProviders.Current با MappedMetadataProvider جایگزین شده‌است.
      در قسمت کار با ModelValidatorProviders.Providers، ابتدا صرفا همان تامین کننده‌ی از نوع DataAnnotationsModelValidatorProvider پیش فرض، یافت شده و حذف می‌شود. سپس تامین کننده‌ی سفارشی سازی شده‌ی خود را معرفی می‌کنیم تا جایگزین آن شود.


      مثالی جهت آزمایش انتقال خودکار ویژگی‌های مدل به ViewModel

      کنترلر مثال برنامه به شرح زیر است. در اینجا از متد Mapper.Map جهت تبدیل خودکار مدل کاربر به ViewModel آن استفاده شده‌است:
      public class HomeController : Controller
      {
          public ActionResult Index()
          {
              var model = new UserModel { FirstName = "و", Id = 1, LastName = "ن" };
              var viewModel = Mapper.Map<UserViewModel>(model);
              return View(viewModel);
          }
       
          [HttpPost]
          public ActionResult Index(UserViewModel data)
          {
              return View(data);
          }
      }
      با این View که جهت ثبت اطلاعات مورد استفاده قرار می‌گیرد. این View، اطلاعات مدل خود را از ViewModel معرفی شده‌ی در ابتدای بحث دریافت می‌کند:
      @model Sample12.ViewModels.UserViewModel
       
      @using (Html.BeginForm("Index", "Home", FormMethod.Post, htmlAttributes: new { @class = "form-horizontal", role = "form" }))
      {
          <div class="row">
              <div class="form-group">
                  @Html.LabelFor(d => d.FirstName, htmlAttributes: new { @class = "col-md-2 control-label" })
                  <div class="col-md-10">
                      @Html.TextBoxFor(d => d.FirstName)
                      @Html.ValidationMessageFor(d => d.FirstName)
                  </div>
              </div>
              <div class="form-group">
                  @Html.LabelFor(d => d.LastName, htmlAttributes: new { @class = "col-md-2 control-label" })
                  <div class="col-md-10">
                      @Html.TextBoxFor(d => d.LastName)
                      @Html.ValidationMessageFor(d => d.LastName)
                  </div>
              </div>
              <div class="form-group">
                  <div class="col-md-offset-2 col-md-10">
                      <input type="submit" value="ارسال" class="btn btn-default" />
                  </div>
              </div>
          </div>
      }
      در این حالت اگر برنامه را اجرا کنیم به شکل زیر خواهیم رسید:


      در این شکل هر چند نوع مدل View مورد استفاده از ViewModel ایی تامین شده‌است که دارای هیچ ویژگی و Data Annotations/Attributes نیست، اما برچسب هر فیلد از ویژگی Display دریافت شده‌‌است. همچنین اعتبارسنجی سمت کاربر فعال بوده و برچسب‌های آن‌ها نیز به درستی دریافت شده‌اند.


      کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
      مطالب
      C# 7 - Generalized Async Return Types
      از زمان ارائه‌ی C# 5 و معرفی الگوهای async/await، تنها نوع‌های خروجی پشتیبانی شده، <Task، Task<T و void (در موارد خاص) بودند. مشکل همراه با این روش، اجبار به وهله سازی رسمی یک Task است؛ حتی اگر نوع خروجی کاملا مشخص باشد.
      برای نمونه در متد ذیل، میزان حجم مصرفی در یک پوشه بازگشت داده می‌شود:
      public async Task<long> GetDirectorySize(string path, string searchPattern)
      {
          if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any())
              return 0;
          else
              return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern,
              SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length)));
      }
      اگر پوشه‌ای خالی باشد، حجم آن صفر است و در این حالت نیازی به ایجاد یک ترد مخصوص آن نیست. اما با توجه به اینکه خروجی متد، <Task<long است، هنوز هم باید این Task وهله سازی شود. برای نمونه اگر به کدهای IL آن دقت کنیم، return 0 آن به صورت ذیل ترجمه می‌شود:
       AsyncTaskMethodBuilder<long>.Create()

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


      امکان تعریف خروجی‌های سفارشی متدهای async در C# 7.0

      در C# 7 می‌توان خروجی‌های سفارشی را جهت متدهای async تعریف کرد و پیشنیاز اصلی آن پیاده سازی متد GetAwater است. برای مثال <System.Threading.Tasks.ValueTask<T یک چنین نوع سفارشی را ارائه می‌دهد. در این حالت، متد ابتدای بحث را می‌توان به صورت ذیل بازنویسی کرد:
      public async ValueTask<long> GetDirectorySize(string path, string searchPattern)
      {
          if (!Directory.EnumerateFileSystemEntries(path, searchPattern).Any())
              return 0;
          else
              return await Task.Run<long>(() => Directory.GetFiles(path, searchPattern,
              SearchOption.AllDirectories).Sum(t => (new FileInfo(t).Length)));
      }
      اگر دقت کنید بجز تغییر نوع خروجی متد، تغییر دیگری نیاز نبوده‌است.
      همانطور که از نام  ValueTask نیز مشخص است، یک struct است؛ برخلاف Task و تخصیص حافظه‌ی آن بر روی stack بجای heap صورت می‌گیرد. به این ترتیب با کاهش فشار بر روی GC، در حلقه‌هایی که خروجی value type دارند، با اندازه گیری‌های انجام شده، کارآیی تا 50 درصد هم می‌تواند بهبود یابد.

      برای کامپایل قطعه کد فوق و تامین نوع جدید ValueTask، نیاز به نصب بسته‌ی نیوگت ذیل نیز می‌باشد:
       PM> install-package System.Threading.Tasks.Extensions
      اشتراک‌ها
      Angular Language Service چیست؟

      تیم Angular در حال تهیه‌ی پروژه‌ای است به نام Angular Language Service که به عنوان افزونه‌ای برای ادیتورها جهت درک بهتر قالب‌های کامپوننت‌ها بکار می‌رود.

      Angular Language Service چیست؟
      نظرات مطالب
      شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرت‌ها به یک اسمبلی دیگر
      کتابخانه استاندارد تبدیل اسامی، به جمع آن‌ها در دات نت، کتابخانه‌ی معروف Humanizer است. بهتر است برای این نوع کارها از آن استفاده کنید چون تهیه‌ی حالت جمع اسامی، استثناء زیاد دارد.
      نظرات مطالب
      تبدیل html به pdf با کیفیت بالا
      نمونه‌ی دیگری هم که از موتور کروم برای تهیه‌ی PDF و Screenshot استفاده می‌کند، « puppeteer-sharp » است که معادل کتابخانه‌ی بسیار معروف و موفق « puppeteer » نودجی‌اس است.
      نظرات مطالب
      امکان تغییر شکل سراسری URLهای تولیدی توسط برنامه‌های ASP.NET Core 2.2
      به نظر شما استفاده از یک TagHelper  سفارشی راه درستی هست برای حل این مورد
      public class AnchorTagHelper : TagHelper {
              /// <summary>
              /// The name of the action method.
              /// </summary>
              [HtmlAttributeName ("asp-action")]
              public string Action { get; set; }
      
              /// <summary>
              /// The name of the controller.
              /// </summary>
              [HtmlAttributeName ("asp-controller")]
              public string Controller { get; set; }
      
              /// <summary>
              /// The name of the area.
              /// </summary>
              [HtmlAttributeName ("asp-area")]
              public string Area { get; set; }
      
              [HtmlAttributeName ("asp-route")]
              public string Route { get; set; }
      
              // Can be async Task
              public override void Process (TagHelperContext context, TagHelperOutput output) {
                  output.TagName = "a";
      
                  string result = string.Empty;
                  if (!string.IsNullOrWhiteSpace (Area)) {
                      result += "/" + Area;
                  }
      
                  if (!string.IsNullOrWhiteSpace (Controller)) {
                      result += "/" + Controller;
                  }
      
                  if (!string.IsNullOrWhiteSpace (Action)) {
                      result += "/" + Action;
                  }
                  if (!string.IsNullOrWhiteSpace (Route)) {
                      Route = ToFriendlyHref (Route);
                      result += "/" + Route;
                  }
      
                  output.Attributes.SetAttribute ("href", result.ToLowerInvariant ());
                  //output.Content.SetContent (currentAttribute.ToString ());
              }
      
              private string ToFriendlyHref (object value) {
                  string text = value.ToString ();
                  List<char> illegalChars = new List<char> () { ' ', '.', '#', '%', '&', '*', '{', '}', '\\', ':', '<', '>', '?', ';', '@', '=', '+', '$', ',' };
                  illegalChars.ForEach (c => {
                      text = text.Replace (c.ToString (), "-");
                  });
                  return text;
              }
          }