بازخوردهای دوره
تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET Web forms
من برای تزریق وابستگی استفاده کردم به درستی کار میکنه
ولی در یک موضوع به بن بست خوردم

public class EfOurServiceService : IOurServiceService
    {
        readonly IUnitOfWork _uow;
        readonly IDbSet<OurService> _ourServices;
        readonly IDbSet<OurServiceCategory> _ourServiceCategories;
        public EfOurServiceService(IUnitOfWork uow)
        {
            _uow = uow;
            _ourServices = _uow.Set<OurService>();
            _ourServiceCategories = _uow.Set<OurServiceCategory>();
        }


public IList GetMasterDetailsFilterLang(string language)
        {
            var query = (_ourServiceCategories
                           .Where(c => (c.Language == language))
                           .Select(
                              c =>
                                 new
                                 {
                                     CatId=c.Id,
                                     CatName=c.Title,
                                     OurServices = c.OurServices
                                        .Select(
                                           o =>
                                              new
                                              {
                                                 ServId= o.Id,
                                                 ServName= o.Title
                                              }
                                        )
                                 }
                           ));

            var ss = query.ToList();

//در اینجا که در لایه سرویس در پروژه ای جداگانه است به ایتم‌های کوئری دسترسی کامل دارم
            foreach (var master in ss)
            {
                var s = master.CatId;
                foreach (var details in master.OurServices)
                {
                    var cc = details.ServName;
                }
            }

            return ss;
        }
}

در اینجا که در لایه سرویس در پروژه ای جداگانه است به ایتم‌های کوئری دسترسی کامل دارم
اما زمانی که در پروژه UI که وب سایت Webform قرار داره به شکل زیر از این متود استفاده میکنم :

public string CreateServiceMnu()
    {
        var ds = OurServiceService.GetMasterDetailsFilterLang(_LangSar);

        foreach (var master in ds)
        {
            var s = master.CatId;
            foreach (var details in master.OurServices)
            {
                var cc = details.ServName;
            }
        }
        return string.Empty;
    }

اما به Property‌ها دسترسی ندارم : از IList استفاده کردم (پر میشه اما قابل دسترس نیست)

Linq to Sql

نمیدونم دلیلش چیه ؟

تو لایه Service به Property‌ها دسترسی دارم اما در لایه UI که وابستگی‌ها تزریق میشه Property‌ها قابل دسترس نیست.

 
مطالب
چگونگی گزارشگیری از Business Objects مانند List توسط StimulSoft

می‌خواهیم از یک لیست در گزارش خود استفاده کنیم؛ بطور مثال وقتی در LINQ  از دستور ToList استفاده می‌کنیم و می‌خواهیم آنرا بصورت مستقیم به Stimul بفرستیم. فرض بر این است که شما DLLهای Stimul را به پروژه اضافه کرده اید و آماده گزارشگیری هستید.

مثلا مدلی در Entity FrameWork با نام base_CenterType 

public class base_CenterType
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public string Dsc { get; set; }
     }

و متدی بصورت ذیل:

public IList<base_CenterType> GetAll()
        {
            return _base_CenterType.ToList();
        }

طراحی گزارش برای این لیست به این صورت است:

1- اضافه کردن StiWebReport به فرم به نام StiWebReport1

2- با کلیک بر روی فلش سمت راست و بالای StiWebReport1 و انتخاب Design Report، وارد قسمت طراحی می‌شویم:

3- با راست کلیک بر روی Business Object و انتخاب New Business Object  پنجره مربوطه باز میشود:

4- بعد از زدن OK پنجره زیر باز خواهد شد که باید در کادر Name نام Business Object را انتخاب کنیم که برای خوانایی بهتر است همان نام کلاس را برای آن انتخاب کنیم. چون Category  نداریم پس باید کادر آن خالی بماند. 

در قسمت Columns باید ستون‌های هم نام و هم نوع با خواص کلاس base_CenterType  را ایجاد کنیم. 

و نهایتا Business Objectی به نام base_CenterType با سه ستون ایجاد خواهد شد. 

  حال می‌توانید ستون‌های مورد نظر را در گزارش بکار ببرید.

با فرض اینکه گزارش را طراحی کرده و آنرا در ریشه درایو C ذخیره کرده‌اید، از  قطعه کد زیر برای ارسال لیست به گزارش و نمایش آن استفاده میکنیم.

StiReport mainreport = new StiReport();            
mainreport.RegBusinessObject("base_CenterType", base_CenterTypeService.GetAll());
mainreport.Load("C:\\StiWebReport2.mrt");
mainreport.Show();
مطالب
فشرده سازی با فرمت 7z

جی‌میل هر ایمیلی را که به همراه آن یک فایل اجرایی پیوست شده باشد برگشت می‌زند. Zip‌ کردن آن هم فایده ندارد چون محتویات فایل‌های zip را هم بررسی می‌کند! فقط به نظر فرمت rar و همچنین 7z را بررسی نمی‌کند (احتمالا با مجوز آن مشکل دارد).
قوی‌ترین برنامه سورس بازی که این فرمت را پشتیبانی می‌کند، برنامه 7zip است و خوشبختانه محصور کننده‌هایی نیز جهت کار با کتابخانه‌های این برنامه برای دات نت فریم ورک موجود است. برای مثال:


مزیت استفاده از این کتابخانه این است که اغلب فرمت‌های پر کاربرد را نیز پشتیبانی می‌کند (شامل zip ، gz ، rar و ...).
برای استفاده از آن به فایل‌های 7z.dll و SevenZipSharp.dll نیاز خواهید داشت. 7z.dll از برنامه 7zip گرفته شده و SevenZipSharp.dll هم محصور کننده دات نتی آن است.

مثالی در مورد فشرده سازی با فرمت 7z با کمک کتابخانه‌های نامبرده شده:

using SevenZip;
using System.Windows.Forms;
using System;

class C7Z
{
public static void Compress7Z(string filePath, string outPath)
{
SevenZipCompressor.SetLibraryPath(String.Format(@"{0}\7z.dll", Application.StartupPath));
SevenZipCompressor cmp = new SevenZipCompressor
{
ArchiveFormat = OutArchiveFormat.SevenZip,
CompressionMethod = CompressionMethod.Lzma,
CompressionMode = CompressionMode.Create,
CompressionLevel = CompressionLevel.High,
VolumeSize = 0
};
cmp.CompressFiles(outPath, filePath);
}

}

C7Z.Compress7Z(@"C:\test\test.txt", @"C:\test\test.7z");
مثال‌های بیشتری را با دریافت سورس SevenZipSharp می‌توانید مشاهده کنید.

مطالب
امکان معرفی نوع‌های محدود به یک فایل در C# 11
در C# 11 ارائه‌ی شده‌ی به همراه NET 7.0.، واژه‌ی کلیدی جدید file، جهت تعریف نوع‌های محدود به یک فایل «File Scoped Types» ارائه شده‌است. این واژه‌ی کلیدی را می‌توان به تعریف هر نوع دلخواهی مانند class, interface, record, struct, enum, delegate اضافه کرد (منهای خواص، فیلدها و رخدادها؛ البته تا C# 11)، تا آن نوع، دیگر در سایر کلاس‌های فایل‌های برنامه، قابل دسترسی نباشد و سطح دید استفاده‌ی از آن، تنها محدود به فایل جاری محل قرار گیری آن شود. به این ترتیب می‌توان در یک فضای نام مشخص، چندین کلاس هم‌نام را تعریف کرد؛ کاری که در نگارش‌های پیشین #C، میسر نبود. بدیهی دیگر نمی‌توان یک چنین نوع‌هایی را با سطوح دسترسی متداول internal و یا  public، تعریف و ترکیب کرد.


یک مثال: نمونه‌ای از نحوه‌ی تعریف و استفاده‌ی از File Scoped Types

فرض کنید دو فایل جدید را به نام‌های File1.cs و File2.cs به پروژه‌ی جاری اضافه کرده‌ایم.
محتوای فایل File1.cs به صورت زیر است:
namespace CS11Tests;

file static class Post
{
    public static string GetTitle() => "Title from File1.cs";
}

internal static class InternalClassFromFile1
{
    public static string GetTitle() => Post.GetTitle();
}
و محتوای فایل File2.cs به نحو زیر تعریف شده‌است:
namespace CS11Tests;

file static class Post
{
    public static string GetTitle() => "Title from File2.cs";
}

internal static class InternalClassFromFile2
{
    public static string GetTitle() => Post.GetTitle();
}
اگر دقت کنید، ذیل فضای نام مشخص و ثابت CS11Tests، دو کلاس هم نام Post را داریم که اینبار با واژه‌ی کلیدی file، شروع شده‌اند و میدان دید دسترسی به آن‌ها، محدود به همان فایل دربرگیرنده‌ی آن‌ها است و در سایر قسمت‌های برنامه قابل دسترسی نیستند. اگر خواستیم به‌نحوی از آن‌ها در سایر قسمت‌های برنامه نیز استفاده کنیم، مانند فایل Program.cs، می‌توان یک تعریف متداول internal/public را مانند کلاس‌های internal تعریف شده، ایجاد کرد و سپس به صورت «غیرمستقیمی» به آن‌ها دسترسی یافت:
using System.Security.AccessControl;
using CS11Tests;
using static System.Console;

WriteLine(InternalClassFromFile1.GetTitle());
WriteLine(InternalClassFromFile2.GetTitle());

امکان partial تعریف کردن نوع‌های محدود به یک فایل در C# 11

در اینجا می‌توان نوع‌های محدود به یک فایل را partial نیز تعریف کرد؛ به شرطی که تمام تعاریف آن‌ها داخل همان فایل قرار گیرند:
namespace CS11Tests;

file static partial class Post
{
    internal static string GetFileScopeTitle() => "Title from File3.cs";
}

file static partial class Post
{
    internal static string AnotherGetFileScopeTitle() => "Another Title from File3.cs";
}

یک سؤال: اگر در یک فایل، file class Post و در فایلی دیگر، کلاس هم نام داخلی internal class Post را تعریف کردیم، آیا می‌توان از نمونه‌ی هم‌نام internal، در کلاس file دار استفاده کرد؟
پاسخ:
خیر!
فرض کنید در File4.cs چنین تعریفی را داریم:
namespace CS11Tests;

internal static class Post
{
    public static string GetTitle() => "Title from File4.cs";
}
در اینجا در فضای نام مشخصی، کلاس Post، به صورت internal تعریف شده‌است. اکنون در File3.cs، مجدد تعریف کلاس هم‌نام Post را اینبار به صورت file داریم:
namespace CS11Tests;

file static class Post
{
    internal static string GetFileScopeTitle() => CS11Tests.Post.GetTitle() + "Title from File3.cs";
}
این قطعه کد کامپایل نمی‌شود. چون Post ای که در اینجا قابل استفاده‌است، دقیقا همان کلاس Post جاری این فایل است و نه نمونه‌ی هم‌نام internal در فایلی دیگر.


خروجی کامپایلر C# 11 در مورد سطح دسترسی file

کامپایلر C# 11 جهت جلوگیری از تداخل نام‌های حاصل از تعریف کلاس‌های با سطح دسترسی file، از قالب زیر:
<SourceFileNameWithoutExtension>F$index$_TypeName
برای نامگذاری نهایی اینگونه نوع‌ها استفاده می‌کند؛ مانند مثال زیر که مرتبط با کلاس Post تعریف شده‌ی در فایل File1.cs است:
internal static class <File1>F3A5590C89B71B2DB20A548228781187A11D076C0CC91E851A4EE796FFE808F8F__Post
{
    public static string GetTitle()
    {
        return "Title from File1.cs";
    }
}
Index منحصربفرد استفاده شده، مشکل تداخل نام‌ها را برطرف می‌کند و به علت وجود <> در تعریف این نام‌های ویژه، امکان استفاده‌ی از آن‌ها در سایر قسمت‌ها و فایل‌های برنامه وجود ندارد.
تاکنون از این روش نامگذاری ویژه، در موارد دیگری مانند async/await , lambda, anonymous method, anonymous types نیز استفاده شده‌است.

چرا قابلیت «File Scoped Types» به زبان C# 11 اضافه شده‌است؟

- جهت کدهای تولیدی توسط ابزارها: گاهی از اوقات، تولید کننده‌های کد، از یک نام مشخص مانند DataSet، بارها و بارها استفاده می‌کنند. برای جلوگیری از تداخل این‌ها، عموما از تعریف تو در توی کلاس‌ها استفاده می‌شود و یا نام آن‌ها را با ایندکس‌هایی مانند DateSet1، DateSet2 و امثال آن‌ها مشخص می‌کنند. وجود واژه‌ی کلیدی file، کار ابزارهای تولید کننده‌ی کد را ساده‌تر می‌کند.
- برای ساده سازی تعریف متدهای الحاقی: با استفاده از سطح دسترسی فایل می‌توان از تداخل متدهای الحاقی هم نام و همچنین شلوغ شدن intellisense جلوگیری کرد. به این ترتیب می‌توان کلاس‌های حاوی Extension method مختص به یک فایل را ایجاد کرد که در سایر قسمت‌های برنامه قابل دسترسی نباشند.
- کاهش تعریف کلاس‌های تو در تو: همانطور که عنوان شد، یکی از روش‌های مقابله‌ی با مشکل تعریف کلاس‌های هم نام در یک فضای نام مشخص، تعریف nested classes است. با ارائه‌ی واژه‌ی کلیدی file، می‌توان یک سطح فرو رفتگی تعریف کلاس‌ها را کاهش داد و به کدهای تمیزتری رسید.
- امکان کپسوله سازی‌های بهتر: عموما کامپوننت‌ها و ماژول‌ها، از چند کلاس تشکیل می‌شوند. با وجود واژه‌ی کلیدی file، می‌توان به سطح بالاتری از خصوصی سازی نوع‌ها، بدون نیاز به تعریف نوع‌های private و یا nested private رسید.
- سهولت نوشتن کلاس‌های آزمون‌های واحد: عموما هر کلاس آزمون، از نوع‌ها و داده‌های خاص خودش استفاده می‌کنند و در اینجا می‌توان سطح دسترسی این تعاریف را بسیار محدود و مختص به همان فایل Test کرد.
مطالب
روش ذخیره‌ی لاگ‌های ILogger در پایگاه داده در Blazor

مقدمه

همانطور که می‌دانید، Blazor دارای یک سیستم لاگ گیری توکار است که می‌توان از آن توسط تزریق ILogger در کامپوننت‌ها بهره برد. این سیستم لاگ گیری در زمان توسعه‌ی نرم افزار، در قالب یک کنسول، لاگ‌ها را به توسعه دهنده نشان می‌دهد. اما پس از تولید و پابلیش اپلیکیشن، دیگر این کنسول وجود ندارد. برای ذخیره‌ی لاگ‌ها در یک فایل متنی بر روی سرور هاست، می‌توان از Serilog بهره برد که روش آن در اینجا  توضیح داده شده است. حال اگر بخواهیم این لاگ‌ها را در یک پایگاه داده ذخیره کنیم چطور؟

ایجاد کلاس لاگ

برای این منظور ابتدا کلاسی را برای ذخیره‌ی لاگ‌ها در پایگاه داده به شکل زیر ایجاد می‌نماییم:
public class DBLog
    {
        public int  DBLogId { get; set; }
        public string? LogLevel { get; set; }
        public string? EventName { get; set; }
        public string? Message { get; set; }
        public string? StackTrace { get; set; }
        public DateTime CreatedDate { get; set; }=DateTime.Now;
    }


ایجاد دیتابیس لاگر

کلاس DBLogger از اینترفیس ILogger ارث بری می‌کند و دارای سه متد است که مهمترین آنها متد Log می‌باشد که درحقیقت با هر بار فراخوانی Logger در برنامه فراخوانی می‌شود. برای مطالعه‌ی بیشتر در رابطه با دو متد دیگر می‌توانید به اینجا مراجعه نمایید.
public class DBLogger:ILogger

    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DBLogger(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }


        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
        {
            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = exception?.Message,
                StackTrace=exception?.StackTrace                
            }; 
            _dbContext.DBLogs.Add(dblLogItem);
            _dbContext.SaveChanges();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return true;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return null;
        }
    }


ایجاد یک لاگ پروایدر سفارشی

حال باید یک لاگ پروایدر سفارشی را ایجاد کنیم تا بتوان یک نمونه از دیتابیس لاگر سفارشی بالا (DBLogger) را ایجاد کرد.

public class DbLoggerProvider:ILoggerProvider
    {
        private bool _isDisposed;
        private readonly ApplicationDbContext _dbContext;
        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
        }

        public ILogger CreateLogger(string categoryName)
        {
            return new D‌BLogger(_dbContext);
        }

        public void Dispose()
        {
        }
    }  

همانطور که ملاحظه می‌نمایید، این لاگ پروایدر، از اینترفیس ILoggerProvider ارث بری کرده‌است که دارای متد CreateLogger می‌باشد ئ این متد با شروع برنامه، یک نمونه از دیتابیس لاگر سفارشی ما را ایجاد می‌کند. در سازنده‌ی این کلاس، DatabaseContext را مقدار دهی نموده‌ایم تا آنرا به کلاس DBLogger ارسال نماییم.

در انتها کافیست در کلاس Startup.cs این لاگ پروایدر سفارشی (DbLoggerProvider  ) را صدا بزنیم.

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
        {
            .
            .
            .
            #region CustomLogProvider
            var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
            var appDbContext = serviceProvider.GetRequiredService<ApplicationDbContext>();
            loggerFactory.AddProvider(new DbLoggerProvider(appDbContext));
            #endregion
            .
            .
            .
در اینجا ابتدا DBContext پروژه را گرفته سپس DbLoggerProvider   را صدا زده و مقدار DBContext را به آن پاس می‌دهیم.


مشکل!

منطق کدهای بالا کاملا صحیح می‌باشد، اما با اجرای یک اپلیکیشن واقعی، در ابتدای کار اینقدر تعداد فراخوانی ثبت لاگ‌ها در پایگاه داده بالا می‌رود که اپلیکیشن هنگ می‌کند. برای حل این مشکل باید یک صف همزمانی برای ثبت لاگ‌ها تشکیل شود. برای این منظور من از این مطلب پروژه‌ی DNTIdentity بهره بردم. بنابراین باید پروایدر را به شکل زیر تصحیح کنیم:
public class DbLoggerProvider:ILoggerProvider
    {
        private readonly CancellationTokenSource _cancellationTokenSource = new();
        private readonly IList<DBLog> _currentBatch = new List<DBLog>();
        private readonly TimeSpan _interval = TimeSpan.FromSeconds(2);

        private readonly BlockingCollection<DBLog> _messageQueue = new(new ConcurrentQueue<DBLog>());

        private readonly Task _outputTask;
        private readonly ApplicationDbContext _dbContext;
        private bool _isDisposed;

        public DbLoggerProvider(ApplicationDbContext dbContext)
        {
            _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
            _outputTask = Task.Run(ProcessLogQueue);
        }
        public ILogger CreateLogger(string categoryName)
        {
            return new DBLogger(this,categoryName);
        }
        private async Task ProcessLogQueue()
        {
            while (!_cancellationTokenSource.IsCancellationRequested)
            {
                while (_messageQueue.TryTake(out var message))
                {
                    try
                    {
                        _currentBatch.Add(message);
                    }
                    catch
                    {
                        //cancellation token canceled or CompleteAdding called
                    }
                }

                await SaveLogItemsAsync(_currentBatch, _cancellationTokenSource.Token);
                _currentBatch.Clear();

                await Task.Delay(_interval, _cancellationTokenSource.Token);
            }
        }
        internal void AddLogItem(DBLog appLogItem)
        {
            if (!_messageQueue.IsAddingCompleted)
            {
                _messageQueue.Add(appLogItem, _cancellationTokenSource.Token);
            }
        }
        private async Task SaveLogItemsAsync(IList<DBLog> items, CancellationToken cancellationToken)
        {
            try
            {
                if (!items.Any())
                {
                    return;
                }

                // We need a separate context for the logger to call its SaveChanges several times,
                // without using the current request's context and changing its internal state.
                foreach (var item in items)
                {
                    var addedEntry = _dbContext.DbLogs.Add(item);
                }

                await _dbContext.SaveChangesAsync(cancellationToken);

            }
            catch
            {
                // don't throw exceptions from logger
            }
        }

        [SuppressMessage("Microsoft.Usage", "CA1031:catch a more specific allowed exception type, or rethrow the exception",
            Justification = "don't throw exceptions from logger")]
        private void Stop()
        {
            _cancellationTokenSource.Cancel();
            _messageQueue.CompleteAdding();

            try
            {
                _outputTask.Wait(_interval);
            }
            catch
            {
                // don't throw exceptions from logger
            }
        }
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        Stop();
                        _messageQueue.Dispose();
                        _cancellationTokenSource.Dispose();
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }  
همچنین دیتابیس لاگر نیز به شکل زیر تغییر خواهد کرد:
public class DBLogger:ILogger
    {
        private readonly LogLevel _minLevel;
        private readonly DbLoggerProvider _loggerProvider;
        private readonly string _categoryName;

        public DBLogger(
            DbLoggerProvider loggerProvider,
            string categoryName
        )
        {
            _loggerProvider= loggerProvider ?? throw new ArgumentNullException(nameof(loggerProvider));
            _categoryName= categoryName;
        }

        public IDisposable BeginScope<TState>(TState state)
        {
            return new NoopDisposable();
        }

        public bool IsEnabled(LogLevel logLevel)
        {
            return logLevel >= _minLevel;
        }

        public void Log<TState>(
            LogLevel logLevel,
            EventId eventId,
            TState state,
            Exception exception,
            Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel))
            {
                return;
            }

            if (formatter == null)
            {
                throw new ArgumentNullException(nameof(formatter));
            }

            var message = formatter(state, exception);

            if (exception != null)
            {
                message = $"{message}{Environment.NewLine}{exception}";
            }

            if (string.IsNullOrEmpty(message))
            {
                return;
            }

            var dblLogItem = new DBLog()
            {
                EventName = eventId.Name,
                LogLevel = logLevel.ToString(),
                Message = $"{_categoryName}{Environment.NewLine}{message}",
                StackTrace=exception?.StackTrace
            };
            _loggerProvider.AddLogItem(dblLogItem);
        }

      

        private class NoopDisposable : IDisposable
        {
            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }

            protected virtual void Dispose(bool disposing)
            {
            }
        }
    }  
خلاصه تغییرات اینگونه‌است که بجای ذخیره لاگ‌ها در دیتابیس در داخل کلاس DBLogger، آنها را در یک صف همزمانی اضافه می‌کنیم و پس از اتمام پروسه، ذخیره سازی لاگ‌ها را در لاگ پروایدر انجام می‌دهیم.
توسط ترکیب روش توضیح داده شده در این مقاله با مدیریت استثناءها در Blazor Server - قسمت دوم، علاوه برلاگ‌های معمولی، می‌توان تمامی استثناءهای یک اپلیکیشن را نیز به راحتی در پایگاه داده ذخیره نمود.
مطالب
Attribute Routing در ASP.NET MVC 5
Routing مکانیزم مسیریابی ASP.NET MVC است، که یک URI را به یک اکشن متد نگاشت می‌کند. MVC 5 نوع جدیدی از مسیر یابی را پشتیبانی میکند که Attribute Routing یا مسیریابی نشانه ای نام دارد. همانطور که از نامش پیداست، مسیریابی نشانه ای از Attribute‌ها برای این امر استفاده میکند. این روش به شما کنترل بیشتری روی URI‌های اپلیکیشن تان می‌دهد.
مدل قبلی مسیریابی (conventional-routing) هنوز کاملا پشتیبانی می‌شود. در واقع می‌توانید هر دو تکنیک را بعنوان مکمل یکدیگر در یک پروژه استفاده کنید.
در این پست قابلیت‌ها و گزینه‌های اساسی مسیریابی نشانه ای را بررسی میکنیم.
  • چرا مسیریابی نشانه ای؟
  • فعال سازی مسیریابی نشانه ای
  • پارامتر‌های اختیاری URI و مقادیر پیش فرض
  • پیشوند مسیر ها
  • مسیر پیش فرض
  • محدودیت‌های مسیر ها
      • محدودیت‌های سفارشی
  • نام مسیر ها
  • ناحیه‌ها (Areas)


چرا مسیریابی نشانه ای

برای مثال یک وب سایت تجارت آنلاین بهینه شده اجتماعی، می‌تواند مسیرهایی مانند لیست زیر داشته باشد:
  • {productId:int}/{productTitle}
نگاشت می‌شود به: (ProductsController.Show(int id
  • {username}
نگاشت می‌شود به: (ProfilesController.Show(string username
  • {username}/catalogs/{catalogId:int}/{catalogTitle}
نگاشت می‌شود به: (CatalogsController.Show(string username, int catalogId
در نسخه قبلی ASP.NET MVC، قوانین مسیریابی در فایل RouteConfig.cs تعریف می‌شدند، و اشاره به اکشن‌های کنترلرها به نحو زیر انجام می‌شد:
routes.MapRoute(
    name: "ProductPage",
    url: "{productId}/{productTitle}",
    defaults: new { controller = "Products", action = "Show" },
    constraints: new { productId = "\\d+" }
);
هنگامی که قوانین مسیریابی در کنار اکشن متدها تعریف می‌شوند، یعنی در یک فایل سورس و نه در یک کلاس پیکربندی خارجی، درک و فهم نگاشت URI‌ها به اکشن‌ها واضح‌تر و راحت می‌شود. تعریف مسیر قبلی، می‌تواند توسط یک attribute ساده بدین صورت نگاشت شود:
[Route("{productId:int}/{productTitle}")]
public ActionResult Show(int productId) { ... }

فعال سازی Attribute Routing

برای فعال سازی مسیریابی نشانه ای، متد MapMvcAttributeRoutes را هنگام پیکربندی فراخوانی کنید.
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        routes.MapMvcAttributeRoutes();
    }
}
همچنین می‌توانید مدل قبلی مسیریابی را با تکنیک جدید تلفیق کنید.
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

پارامترهای اختیاری URI و مقادیر پیش فرض

می توانید با اضافه کردن یک علامت سوال به پارامترهای مسیریابی، آنها را optional یا اختیاری کنید. برای تعیین مقدار پیش فرض هم از فرمت parameter=value استفاده می‌کنید.
public class BooksController : Controller
{
    // eg: /books
    // eg: /books/1430210079
    [Route("books/{isbn?}")]
    public ActionResult View(string isbn)
    {
        if (!String.IsNullOrEmpty(isbn))
        {
            return View("OneBook", GetBook(isbn));
        }
        return View("AllBooks", GetBooks());
    }
 
    // eg: /books/lang
    // eg: /books/lang/en
    // eg: /books/lang/he
    [Route("books/lang/{lang=en}")]
    public ActionResult ViewByLanguage(string lang)
    {
        return View("OneBook", GetBooksByLanguage(lang));
    }
}
در این مثال، هر دو مسیر books/ و books/1430210079/ به اکشن متد "View" نگاشت می‌شوند، مسیر اول تمام کتاب‌ها را لیست میکند، و مسیر دوم جزئیات کتابی مشخص را لیست می‌کند. هر دو مسیر books/lang/ و books/lang/en/ به یک شکل نگاشت می‌شوند، چرا که مقدار پیش فرض این پارامتر en تعریف شده.



پیشوند مسیرها (Route Prefixes)

برخی اوقات، تمام مسیرها در یک کنترلر با یک پیشوند شروع می‌شوند. بعنوان مثال:
public class ReviewsController : Controller
{
    // eg: /reviews
    [Route("reviews")]
    public ActionResult Index() { ... }
    // eg: /reviews/5
    [Route("reviews/{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg: /reviews/5/edit
    [Route("reviews/{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
همچنین می‌توانید با استفاده از خاصیت [RoutePrefix] یک پیشوند عمومی برای کل کنترلر تعریف کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /reviews
    [Route]
    public ActionResult Index() { ... }
    // eg.: /reviews/5
    [Route("{reviewId}")]
    public ActionResult Show(int reviewId) { ... }
    // eg.: /reviews/5/edit
    [Route("{reviewId}/edit")]
    public ActionResult Edit(int reviewId) { ... }
}
در صورت لزوم، می‌توانید برای بازنویسی (override) پیشوند مسیرها از کاراکتر ~ استفاده کنید:
[RoutePrefix("reviews")]
public class ReviewsController : Controller
{
    // eg.: /spotlight-review
    [Route("~/spotlight-review")]
    public ActionResult ShowSpotlight() { ... }
 
    ...
}

مسیر پیش فرض

می توانید خاصیت [Route] را روی کنترلر اعمال کنید، تا اکشن متد را بعنوان یک پارامتر بگیرید. این مسیر سپس روی تمام اکشن متدهای این کنترلر اعمال می‌شود، مگر آنکه یک [Route] بخصوص روی اکشن‌ها تعریف شده باشد.
[RoutePrefix("promotions")]
[Route("{action=index}")]
public class ReviewsController : Controller
{
    // eg.: /promotions
    public ActionResult Index() { ... }
 
    // eg.: /promotions/archive
    public ActionResult Archive() { ... }
 
    // eg.: /promotions/new
    public ActionResult New() { ... }
 
    // eg.: /promotions/edit/5
    [Route("edit/{promoId:int}")]
    public ActionResult Edit(int promoId) { ... }
}

محدودیت‌های مسیر ها

با استفاده از Route Constraints می‌توانید نحوه جفت شدن پارامتر‌ها در قالب مسیریابی را محدود و کنترل کنید. فرمت کلی {parameter:constraint} است. بعنوان مثال:
// eg: /users/5
[Route("users/{id:int}"]
public ActionResult GetUserById(int id) { ... }
 
// eg: users/ken
[Route("users/{name}"]
public ActionResult GetUserByName(string name) { ... }
در اینجا، مسیر اول تنها در صورتی انتخاب می‌شود که قسمت id در URI یک مقدار integer باشد. در غیر اینصورت مسیر دوم انتخاب خواهد شد.
جدول زیر constraint‌ها یا محدودیت هایی که پشتیبانی می‌شوند را لیست می‌کند.
 مثال  توضیحات  محدودیت
 {x:alpha}  کاراکترهای الفبای لاتین را تطبیق (match) می‌دهد (a-z, A-Z).  alpha
 {x:bool}  یک مقدار منطقی را تطبیق می‌دهد.  bool
 {x:datetime}  یک مقدار DateTime را تطبیق می‌دهد.  datetime
 {x:decimal}  یک مقدار پولی را تطبیق می‌دهد.  decimal
 {x:double}  یک مقدار اعشاری 64 بیتی را تطبیق می‌دهد.  double
 {x:float}  یک مقدار اعشاری 32 بیتی را تطبیق می‌دهد.  float
 {x:guid}  یک مقدار GUID را تطبیق می‌دهد.  guid
 {x:int}  یک مقدار 32 بیتی integer را تطبیق می‌دهد.  int
 {(x:length(6}
{(x:length(1,20}
 رشته ای با طول تعیین شده را تطبیق می‌دهد.  length
 {x:long}  یک مقدار 64 بیتی integer را تطبیق می‌دهد.  long
 {(x:max(10}  یک مقدار integer با حداکثر مجاز را تطبیق می‌دهد.  max
 {(x:maxlength(10}  رشته ای با حداکثر طول تعیین شده را تطبیق می‌دهد.  maxlength
 {(x:min(10}  مقداری integer با حداقل مقدار تعیین شده را تطبیق می‌دهد.  min
 {(x:minlength(10}  رشته ای با حداقل طول تعیین شده را تطبیق می‌دهد.  minlength
 {(x:range(10,50}  مقداری integer در بازه تعریف شده را تطبیق می‌دهد.  range
 {(${x:regex(^\d{3}-\d{3}-\d{4}  یک عبارت با قاعده را تطبیق می‌دهد.  regex

توجه کنید که بعضی از constraint ها، مانند "min" آرگومان‌ها را در پرانتز دریافت می‌کنند.
می توانید محدودیت‌های متعددی روی یک پارامتر تعریف کنید، که باید با دونقطه جدا شوند. بعنوان مثال:
// eg: /users/5
// but not /users/10000000000 because it is larger than int.MaxValue,
// and not /users/0 because of the min(1) constraint.
[Route("users/{id:int:min(1)}")]
public ActionResult GetUserById(int id) { ... }
مشخص کردن اختیاری بودن پارامتر ها، باید در آخر لیست constraints تعریف شود:
// eg: /greetings/bye
// and /greetings because of the Optional modifier,
// but not /greetings/see-you-tomorrow because of the maxlength(3) constraint.
[Route("greetings/{message:maxlength(3)?}")]
public ActionResult Greet(string message) { ... }

محدودیت‌های سفارشی

با پیاده سازی قرارداد IRouteConstraint می‌توانید محدودیت‌های سفارشی بسازید. بعنوان مثال، constraint زیر یک پارامتر را به لیستی از مقادیر قابل قبول محدود می‌کند:
public class ValuesConstraint : IRouteConstraint
{
    private readonly string[] validOptions;
    public ValuesConstraint(string options)
    {
        validOptions = options.Split('|');
    }
 
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
        }
        return false;
    }
}
قطعه کد زیر نحوه رجیستر کردن این constraint را نشان می‌دهد:
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        var constraintsResolver = new DefaultInlineConstraintResolver();
 
        constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
 
        routes.MapMvcAttributeRoutes(constraintsResolver);
    }
}
حالا می‌توانید این محدودیت سفارشی را روی مسیرها اعمال کنید:
public class TemperatureController : Controller
{
    // eg: temp/celsius and /temp/fahrenheit but not /temp/kelvin
    [Route("temp/{scale:values(celsius|fahrenheit)}")]
    public ActionResult Show(string scale)
    {
        return Content("scale is " + scale);
    }
}

نام مسیر ها

می توانید به مسیرها یک نام اختصاص دهید، با این کار تولید URI‌ها هم راحت‌تر می‌شوند. بعنوان مثال برای مسیر زیر:
[Route("menu", Name = "mainmenu")]
public ActionResult MainMenu() { ... }
می‌توانید لینکی با استفاده از Url.RouteUrl تولید کنید:
<a href="@Url.RouteUrl("mainmenu")">Main menu</a>

ناحیه‌ها (Areas)

برای مشخص کردن ناحیه ای که کنترلر به آن تعلق دارد می‌توانید از خاصیت [RouteArea] استفاده کنید. هنگام استفاده از این خاصیت، می‌توانید با خیال راحت کلاس AreaRegistration را از ناحیه مورد نظر حذف کنید.
[RouteArea("Admin")]
[RoutePrefix("menu")]
[Route("{action}")]
public class MenuController : Controller
{
    // eg: /admin/menu/login
    public ActionResult Login() { ... }
 
    // eg: /admin/menu/show-options
    [Route("show-options")]
    public ActionResult Options() { ... }
 
    // eg: /stats
    [Route("~/stats")]
    public ActionResult Stats() { ... }
}
با این کنترلر، فراخوانی تولید لینک زیر، رشته "Admin/menu/show-options/" را بدست میدهد:
Url.Action("Options", "Menu", new { Area = "Admin" })
به منظور تعریف یک پیشوند سفارشی برای یک ناحیه، که با نام خود ناحیه مورد نظر متفاوت است می‌توانید از پارامتر AreaPrefix استفاده کنید. بعنوان مثال:
[RouteArea("BackOffice", AreaPrefix = "back-office")]
اگر از ناحیه‌ها هم بصورت مسیریابی نشانه ای، و هم بصورت متداول (که با کلاس‌های AreaRegistration پیکربندی می‌شوند) استفاده می‌کنید باید مطمئن شوید که رجیستر کردن نواحی اپلیکیشن پس از مسیریابی نشانه ای پیکربندی می‌شود. به هر حال رجیستر کردن ناحیه‌ها پیش از تنظیم مسیرها بصورت متداول باید صورت گیرد. دلیل آن هم مشخص است، برای اینکه درخواست‌های ورودی بدرستی با مسیرهای تعریف شده تطبیق داده شوند، باید ابتدا attribute routes، سپس area registration و در آخر default route رجیستر شوند. بعنوان مثال:
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    AreaRegistration.RegisterAllAreas();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

اشتراک‌ها
پلتفرم خودکار یک پلتفرم رایگان و متن باز (Open Source) برای تولید نرم افزار تحت وب با تکنولوژی مایکروسافت
پلتفرم خودکار یه پروژه متن بازه و ایده اصلی اش تولید یک نرم افزار تحت وب از صفر تا صد به صورت آنلاین و بدون نیاز به ابزار هایی مثل ویژوال استدیو Sql Management Studio و TFS  و غیره و تنها با یک مرورگر ساده مثل Chrome .


پلتفرم خودکار  همانند ویژوال استدیو دو حالت Release و دیباگ دارد که این امکان را به برنامه نویسان می‌دهد که کدهای اجرایی در سمت سرور و کلاینت در دو حالت دیباگ و Release کاملا مختلف و مجزا باشند.

یعنی شما برای ساخت یک وب سایت می‌تونید تمام کدهای سمت سرور و کلاینت و دیتابیس رو از طریق یک مرورگر وب بنویسید و اگه نیاز به کامپایل باشه , خود پلتفرم این کار رو انجام میده.
تعدادی از این کدها : C# VB.Net SQL CSS JavaScript SASS LESS Coffee و غیره

اما غیر از اینها پلتفرم خودکار شامل یه سورس کنترل و مدیریت ورژن و Build System و Load Module  اختصاصی هم میشه .یعتی عملا شما نیازی به سورس کنترل‌های آنلاین
مثل GitHub , ... را ندارید و می‌تونید دسترسی ویرایش و یا Build و یا Test و یا اجرا رو در سطح یک خط کد تا یک پروژه به طور کامل به سایر برنامه نویسان بدهید.

برای طراحی و ساخت دیتابیس تون و مدیریت Migration‌‌ها هم یک ابزار انلاین داره که پشت صحنه از Entity FrameWork استفاده می‌کنه و تمامی امکانات Entity FrameWork   برای ساخت و پشتیبانی دیتابیس رو به صورت آنلاین در اختیار شما می‌زاره.

وابسته به فریم ورک خاصی در سمت کلاینت نیست .ولی پیش فرض اش JQUERY و بوت استرپ استفاده می‌کنه. شما می‌تونید N تا فریم ورک و قالب متفاوت تعریف کنید , به طوری که برای مثال یه صفحه وبسایتتون با React و صفحه دیگه با انگولار باشه .

برای دیباگ تحت وب هم امکاناتی در اختیار برنامه نویس قرار می‌ده مثل ریموت دیباگ و یا دیباگ در ویژوال استدیو.این‌ها بخشی از امکانات پلتفرم خودکاره برای آشنایی بیشتر با امکانات پلتفرم از لینک‌های زیر استفاده کنید.

حدود 70 ساعت آموزش در آپارات  :


خودم تا حالا دو تا وبسایت باهاش نوشتم و البته چند تا دیگه هم در دست توسعه است. وبسایت های کارشناسان.نت با قالب بوت استرپ و jquery  نوشته شده و داستان موفقیت با قالب Material و jquer نوشته شده است.

دوستان عزیز قصد دارم ورژن core پلتفرم خودکار  رو بنویسم و یه سری امکانات جدید مثل کامپایلر و BuildSystem ری اکت رو اضافه کنم و هوش مصنوعی IDE رو هم افزایش بدم. اگر علاقه مند به همکاری هستید برام پیام بذارید.

با توجه به متن باز بودن پلتفرم , سود مالی وجود نداره ولی می‌تونه رزومه خوبی براتون بشه و از اون مهمتره می‌تونه پروژه فوق العاده ارزشمند و کاربردی باشه. 

پلتفرم خودکار یک پلتفرم رایگان و متن باز (Open Source) برای تولید نرم افزار تحت وب با تکنولوژی مایکروسافت
مطالب
تعیین اعتبار یک checkBoxList با کمک jQuery

checkBoxList جزو کنترل‌هایی در ASP.Net است که نمی‌توان RequiredFieldValidator استاندارد را بر آن اعمال کرد. به عبارتی اگر نیاز بود حداقل یک آیتم چک باکس لیست حتما توسط کاربر انتخاب شود، راه حل آماده‌ای برای آن وجود ندارد. پیاده سازی این‌کار با استفاده از jQuery به سادگی میسر است که در ادامه آن‌را مرور خواهیم کرد.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="CheckBoxListValidator._Default" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="jquery.min.js" type="text/javascript"></script>
<script type="text/javascript">
//<![CDATA[
function CheckItems(sender, args) {
//Get the total nuumber of selected CheckBoxes
var num = jQuery("table#<%=CheckBoxList1.ClientID%> input:checked").length;
args.IsValid = num > 0;
}
//]]>
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:CheckBoxList ID="CheckBoxList1" runat="server">
<asp:ListItem>item1</asp:ListItem>
<asp:ListItem>item2</asp:ListItem>
</asp:CheckBoxList>
<asp:CustomValidator ClientValidationFunction="CheckItems" ID="ValidateIt"
runat="server" ErrorMessage="(*)"> </asp:CustomValidator>
<asp:Button ID="Button1" runat="server" />
</div>
</form>
</body>
</html>

توضیحات:
یک CustomValidator استاندارد را به فرم اضافه کرده‌ایم تا توسط تابعی که به ClientValidationFunction آن معرفی می‌شود، کار اعتبار سنجی سمت کاربر را انجام دهد. این تابع یا همان CheckItems مثال فوق، امضای استاندارد و آشنایی دارد. اگر تعیین اعتبار صورت گرفته باشد، باید args.IsValid در آن به true تنظیم شود یا بر عکس.
اصل قضیه هم، همین یک سطر کد زیر است:

var num = jQuery("table#<%=CheckBoxList1.ClientID%> input:checked").length;
کار این سطر که از جی‌کوئری استفاده می‌کند، پیدا کردن جدولی است که ID آن مساوی آی دی سمت کلاینت چک باکس لیست ما است (ASP.Net یک چک باکس لیست را به صورت یک جدول حاوی چک باکس‌ها رندر می‌کند). سپس در همان ناحیه مشغول به جستجوی چک باکس‌هایی می‌شود که تیک خورده‌اند. نهایتا تعداد آن‌ها را بر می‌گرداند.

مطالب
خواندنی‌های 16 تیر

اس کیوال سرور

توسعه وب

دات نت فریم ورک

دبلیو پی اف و سیلور لایت

سی و مشتقات

شیرپوینت

کتاب‌های رایگان

مای اس کیوال

متفرقه

وب سرورها

پی اچ پی

نظرات مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
یک نکته‌ی تکمیلی: همیشه بهتر است در سمت کلاینت هم تاریخ انقضای JWT پیش از استفاده بررسی شود
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;

namespace BlazorWasm.Client.Utils
{
    public class JwtInfo
    {
        public IEnumerable<Claim> Claims { set; get; }

        public DateTime? ExpirationDateUtc { set; get; }

        public bool IsExpired { set; get; }

        public IEnumerable<string> Roles { set; get; }
    }

    /// <summary>
    /// From the Steve Sanderson’s Mission Control project:
    /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs
    /// </summary>
    public static class JwtParser
    {
        public static JwtInfo ParseClaimsFromJwt(string jwt)
        {
            var claims = new List<Claim>();
            var payload = jwt.Split('.')[1];

            var jsonBytes = getBase64WithoutPadding(payload);

            foreach (var keyValue in JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes))
            {
                if (keyValue.Value is JsonElement element && element.ValueKind == JsonValueKind.Array)
                {
                    foreach (var itemValue in element.EnumerateArray())
                    {
                        claims.Add(new Claim(keyValue.Key, itemValue.ToString()));
                    }
                }
                else
                {
                    claims.Add(new Claim(keyValue.Key, keyValue.Value.ToString()));
                }
            }

            var roles = getRoles(claims);
            var expirationDateUtc = getDateUtc(claims, "exp");
            var isExpired = getIsExpired(expirationDateUtc);
            return new JwtInfo
            {
                Claims = claims,
                Roles = roles,
                ExpirationDateUtc = expirationDateUtc,
                IsExpired = isExpired
            };
        }

        private static IList<string> getRoles(IList<Claim> claims) =>
            claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();

        private static byte[] getBase64WithoutPadding(string base64)
        {
            switch (base64.Length % 4)
            {
                case 2: base64 += "=="; break;
                case 3: base64 += "="; break;
            }
            return Convert.FromBase64String(base64);
        }

        private static bool getIsExpired(DateTime? expirationDateUtc) =>
            !expirationDateUtc.HasValue || !(expirationDateUtc.Value > DateTime.UtcNow);

        private static DateTime? getDateUtc(IList<Claim> claims, string type)
        {
            var exp = claims.SingleOrDefault(claim => claim.Type == type);
            if (exp == null)
            {
                return null;
            }

            var expValue = getTimeValue(exp.Value);
            if (expValue == null)
            {
                return null;
            }

            var dateTimeEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            return dateTimeEpoch.AddSeconds(expValue.Value);
        }

        private static long? getTimeValue(string claimValue)
        {
            if (long.TryParse(claimValue, out long resultLong))
                return resultLong;

            if (float.TryParse(claimValue, out float resultFloat))
                return (long)resultFloat;

            if (double.TryParse(claimValue, out double resultDouble))
                return (long)resultDouble;

            return null;
        }
    }
}