مطالب
الگوی استخر اشیاء Object Pool Pattern
الگوی استخر اشیاء، جزو الگوهای تکوینی است و کار آن جلوگیری از ایجاد اشیاء تکراری و محافظت از به هدر رفتن حافظه است. نحوه کار این الگو بدین شکل است که وقتی کاربر درخواست نمونه‌ای از یک شیء را می‌دهد، بعد از اتمام کار، شیء نابود نمی‌شود؛ بلکه به استخر بازگشت داده می‌شود تا در درخواست‌های آینده، مجددا مورد استفاده قرار گیرد. این کار موجب عدم هدر رفتن حافظه و همچنین بالا رفتن کارآیی برنامه می‌گردد. این الگو به خصوص برای اشیایی که مدت کوتاهی مورد استفاده قرار میگیرند و آماده سازی آن‌ها هزینه بر است بسیار کارآمد می‌باشد. از آنجا که هزینه‌ی ساخت و نابود سازی در حد بالایی قرار دارد و آن شیء به طور مکرر مورد استفاده قرار می‌گیرد، زمان زیادی صرفه جویی می‌شود.

در این الگو ابتدا شیء از استخر درخواست می‌شود. اگر قبلا ایجاد شده باشد، مجددا مورد استفاده قرار می‌گیرد. در غیر این صورت نمونه جدیدی ایجاد می‌شود و وقتی که کار آن پایان یافت به استخر بازگشت داده شده و نگهداری می‌شود، تا زمانی که مجددا درخواست استفاده از آن برسد.
 یکی از نکات مهم و کلیدی این است که وقتی کار شیء مربوطه تمام شد، باید اطلاعات شیء قبلی و داده‌های آن نیز پاکسازی شوند؛ در غیر این صورت احتمال آسیب و خطا در برنامه بالا می‌رود و این الگو را به یک ضد الگو تبدیل خواهد کرد.
نمونه‌ای از پیاده سازی این الگو را در دات نت، می‌توانید در data provider‌های مخصوص SQL Server نیز ببینید. از آنجا که ایجاد اتصال به دیتابیس، هزینه بر بوده و دستورات در کوتاه‌ترین زمان ممکن اجرا می‌شوند، این کانکشن‌ها یا اتصالات بعد از بسته شدن از بین نمی‌روند، بلکه در استخر مانده تا زمانیکه مجددا نمونه مشابهی از آن‌ها درخواست شود تا دوباره مورد استفاده قرار بگیرند تا از هزینه اولیه برای ایجاد آن‌ها پرهیز شود. استفاده از این الگو در برنامه نویسی با سوکت ها، تردها و کار با اشیاء گرافیکی بزرگ مثل فونت‌ها و تصاویر Bitmap کمک شایانی می‌کند. ولی در عوض برای اشیای ساده می‌تواند کارآیی را به شدت کاهش دهد.

الگوی بالا را در سی شارپ بررسی می‌کنیم:
ابتدا کلاس PooledObject را به عنوان یک شیء بزرگ و پر استفاده ایجاد می‌کنیم:
 public class PooledObject
    {
        DateTime _createdAt = DateTime.Now;

        public DateTime CreatedAt
        {
            get { return _createdAt; }
        }

        public string TempData { get; set; }

        public void DoSomething(string name)
        {
            Console.WriteLine($"{name} : {TempData} is written on {CreatedAt}");
        }
    }
tempdata ویژگی است که باید برای هر شیء، مجزا باشد. پس باید دقت داشت برای استفاده‌های بعدی نیاز است پاک سازی شود.
بعد از آن کلاس پول را پیاده سازی می‌کنیم:
public static class Pool
{
    private static List<PooledObject> _available = new List<PooledObject>();
    private static List<PooledObject> _inUse = new List<PooledObject>();
 
    public static PooledObject GetObject()
    {
        lock(_available)
        {
            if (_available.Count != 0)
            {
                PooledObject po = _available[0];
                _inUse.Add(po);
                _available.RemoveAt(0);
                return po;
            }
            else
            {
                PooledObject po = new PooledObject();
                _inUse.Add(po);
                return po;
            }
        }
    }
 
    public static void ReleaseObject(PooledObject po)
    {
        CleanUp(po);
 
        lock (_available)
        {
            _available.Add(po);
            _inUse.Remove(po);
        }
    }
 
    private static void CleanUp(PooledObject po)
    {
        po.TempData = null;
    }
}
در کلاس بالا، کاربر با متد GetObject، درخواست شی‌ءایی را می‌کند و متد نگاه می‌کند که اگر لیست موجودی، پر باشد، اولین نمونه‌ی در دسترس را ارسال می‌کند. ولی در صورتیکه نمونه‌ای از آن نباشد، یک نمونه‌ی جدید را ایجاد کرده و آن را در لیست مورد استفاده‌ها قرار می‌دهد و به کاربر باز می‌گرداند.
متد Release Object وظیفه‌ی بازگرداندن شیء را به استخر، دارد که از لیست در حال استفاده‌ها آن را حذف کرده و به لیست موجودی اضافه می‌کند. متد Cleanup در این بین وظیفه ریست کردن شیء را دارد تا مشکلی که در بالا بیان کردیم رخ ندهد.
کد زیر را اجرا می‌کنیم:
            var obj1 = Pool.GetObject();
            obj1.DoSomething("obj1");

            Thread.Sleep(2000);
            var obj2 = Pool.GetObject();
            obj2.DoSomething("obj2");
            Pool.ReleaseObject(obj1);

            Thread.Sleep(2000);
            var obj3 = Pool.GetObject();
            obj3.DoSomething("obj3");
نتیجه به شکل زیر است:
obj1 :  is written on 4/21/2016 11:25:26 AM
obj2 :  is written on 4/21/2016 11:25:28 AM
obj3 :  is written on 4/21/2016 11:25:26 AM
ابتدا شیء obj1 ایجاد می‌شود و سپس obj2 و بعد از آن obj1 به لیست موجودی‌ها بازگشته و برای obj3 همان شیء را بازگشت می‌هد که برای obj1 ساخته بود. توقف تردها در این مثال برای مشاهده‌ی بهتر زمان است.
اشتراک‌ها
NET 8 Preview 2. منتشر شد
Microsoft's second preview of .NET 8 is just "a quick follow-up" to Preview 1, highlighted by a couple of improvements to Blazor, for creating web apps in C# instead of JavaScript.
NET 8 Preview 2. منتشر شد
مطالب
الگوی بازدیدکننده Visitor Pattern
این الگو یکی دیگر از الگوهای رفتاری است که به قاعده OCP یا Open Closed Principle کمک بسیاری می‌کند. این الگو برای زمانی مناسب است که ما سعی بر این داریم تا یک سری الگوریتم‌های متفاوت را بر روی یک سری از اشیاء پیاده سازی کنیم. به عنوان مثال تصور کنید که ما در یک سازمان افراد مختلفی را از مدیریت اصلی گرفته، تا ساده‌ترین کارمندان، داریم و برای محاسبه حقوق و مالیات و ... نیاز است تا برای هر کدام دستور العمل‌هایی را اجرا کنیم  و ممکن است در آینده تعداد این دستور العمل‌ها بالاتر هم برود.
در این مثال ما سه گروه Manager,Employee و Worker را داریم که می‌خواهیم با استفاده از این الگو برای هر کدام به طور جداگانه، حقوق و دستمزد و اضافه کاری را محاسبه کنیم. با توجه به اینکه فرمول هر یک جداست و این احتمال نیز وجود دارد که هر کدام خواص مخصوص به خود را داشته باشند که در دیگری وجود ندارد و در آینده این احتمال می‌رود که سمت جدید یا دستورالعمل‌های جدیدی اضافه شود، بهترین راه حل استفاده از الگوی Visitor است.

الگوی visitor دو بخش مهم دارد؛ یکی Element که قرار است کار روی آن انجام شود. مثل سمت‌های مختلف و دیگری Visitor هست که همان دستورالعمل‌هایی چون محاسبه حقوق و دستمزد و ... است که روی المان‌ها صورت می‌گیرد.
ابتدا برای هر کدام یک اینترفیس را با مشخصات زیر می‌سازیم:
 public interface IElement
    {
        void Accept(IElementVisitor visitor);
    }

    public interface IElementVisitor
    {
        void Visit(Manager manager);
        void Visit(Employee manager);
        void Visit(Worker manager);
    }
همانطور که می‌بینید در کلاس Visitor سه متد هستند که سه کلاس مدیر، کارمند و کارگر را که مشتق شده از اینترفیس Element هستند، به صورت آرگومان می‌پذیرند. توصیف هر کلاس المان به شرح زیر است:
 public class  Manager: IElement
    {
        public int WorkingHour = 8;
        public int Wife = 1;
        public int Children = 3;
        public int OffDays = 6;
        public int OverHours = 12;

        public void Accept(IElementVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

public class Employee: IElement
    {
        public int WorkingHour = 8;
        public int Wife = 1;
        public int Children = 3;
        public int OffDays = 6;
        public int OverHours = 12;

        public void Accept(IElementVisitor visitor)
        {
            visitor.Visit(this);
        }
    }

public class Worker:IElement
    {
        public int WorkingHour = 8;
        public int Wife = 1;
        public int Children = 3;
        public int OffDays = 6;
        public int OverHours = 12;

        public void Accept(IElementVisitor visitor)
        {
            visitor.Visit(this);
        }
    }
ما اطلاعات هر کلاس را در این مثال، مشابه گذاشته‌ایم تا نتیجه فرمول را ببینیم. ولی هیچ الزامی به رعایت آن نیست.
حال وقت آن رسیده تا از روی کلاس Visitor، برای حقوق، دستمزد و اضافه کاری، کلاس‌های جدیدی را بسازیم:
 class SalaryCalculator:IElementVisitor
    {
        public void Visit(Manager manager)
        {
            var salary = manager.WorkingHour*10000;
            salary += manager.Wife*25000;
            salary += manager.Children*20000;
            salary -= manager.OffDays*5000;
            Console.WriteLine("Manager's Salary is " + salary);
        }

        public void Visit(Employee employee)
        {
            var salary = employee.WorkingHour * 7000;
            salary += employee.Wife * 15000;
            salary += employee.Children * 10000;
            salary -= employee.OffDays * 6000;
            Console.WriteLine("Employee's Salary is " + salary);
        }

        public void Visit(Worker worker)
        {
            var salary = worker.WorkingHour * 6000;
            salary += worker.Wife * 5000;
            salary += worker.Children * 2000;
            salary -= worker.OffDays * 7000;
            Console.WriteLine("Worker's Salary is " + salary);
        }
    }

    class WageCalculator:IElementVisitor
    {
        public void Visit(Manager manager)
        {
            var wage = manager.OverHours*30000;
            Console.WriteLine("Employee's wage is " + wage);
        }

        public void Visit(Employee employee)
        {
            var wage = employee.OverHours * 20000;
            Console.WriteLine("Employee's wage is " + wage);
        }

        public void Visit(Worker worker)
        {
            var wage = worker.OverHours * 15000;
            Console.WriteLine("Employee's wage is " + wage);
        }
    }
اکنون نیاز است تا ارتباط بین المان‌ها و بازدید کننده‌ها را طوری برقرار کنیم که برای تغییر آن‌ها در آینده، مشکلی نداشته باشیم. به همین جهت یک کلاس جدید به نام سیستم مالی ایجاد می‌کنیم:
class FinancialSystem
    {
        private readonly IList<IElement> _elements;

        public FinanceSystem()
        {
            _elements=new List<IElement>();
        }

        public void Attach(IElement element)
        {
            _elements.Add(element);
        }

        public void Detach(IElement element)
        {
            _elements.Remove(element);
        }

        public void Accept(IElementVisitor visitor)
        {
            foreach (var element in _elements)
            {
                element.Accept(visitor);
            }
        }
    }
در این روش تمام المان‌ها را داخل یک لیست قرار داده و سپس با استفاده از متد Accept، یکی از کلاس‌های مشتق شده از Visitor را به آن نسبت می‌دهیم که وظیفه آن صدا زدن متد Accept درون المان هاست. وقتی متد Accept المان‌ها صدا زده شد، شیء، المان را به متد Visit در Visitor داده و فرمول را روی آن اجرا می‌کند.
بدنه اصلی:
IElement manager=new Manager();
IElement employee=new Employee();
IElement worker=new Worker();

var fine=new FinancialSystem();
fine.Attach(manager);
fine.Attach(employee);
fine.Attach(worker);

fine.Accept(new SalaryCalculator());
fine.Accept(new WageCalculator());
نتیجه خروجی:
Manager's Salary is 135000
Employee's Salary is 65000
Worker's Salary is 17000
Manager's wage is 360000
Employee's wage is 240000
Worker's wage is 180000
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 6 - Implementation Factory

در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفاده‌ی از Implementation Type‌ها را  به ما ارائه می‌دهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار می‌دهد، ما یک Delegate را برای فراخوانی سرویس استفاده می‌کنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده می‌کنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح می‌دهند یک Config را مستقیما از IOptionMonitor   بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:

services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);

پیاده سازیComposite Pattern 

یک کاربرد بالقوه‌ی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمی‌کند، ولی ما می‌توانیم آن‌را پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را می‌فرستادیم؛ ولی حالا مالک محصول می‌آید و می‌گوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض می‌کنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلی‌تر به نام INotificationService می‌سازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری می‌کنند:

public interface INotificationService
{
      void SendMessage(string msg, int userId);
}
حالا CompositeNotificationService را به این صورت تعریف می‌کنیم:
    public class CompositeNotificationService : INotificationService
    {
        private readonly IEnumerable<INotificationService> _notificationServices;
        public CompositeNotificationService(IEnumerable<INotificationService> notificationServices)
        {
            _notificationServices = notificationServices;
        }

        public void SendMessage(string msg, int userId)
        {
            foreach (var notificationServicei in _notificationServices) 
            {
                notificationServicei.SendMessage(msg, userId);
            }
        }
    }
و این سرویس‌ها را به این صورت در DI Container ثبت می‌کنیم: 
services.AddScoped<IEmailNotificationService, EmailNotificationService>();
services.AddScoped<ISMSNotificationService, SMSNotificationService>();

services.AddSingleton<INotificationService>(sp => new CompositeNotificationService(
      new INotificationService[] { 
          sp.GetRequiredService<IEmailNotificationService>() , 
          sp.GetRequiredService<ISMSNotificationService>()
      }
));
حالا هر زمانیکه بخواهیم همزمان، هم ایمیل و هم پیامک بفرستیم، کافی است که سرویس INotificationService را در سازنده‌ی کلاس مورد نظر تزریق کرده و از آن در مکان‌ها و شرایط مختلفی استفاده کنیم. اگر هر کدام از سرویس‌های ارسال ایمیل و سرویس‌های پیامک را به صورت جداگانه بخواهیم، می‌توانیم آنها را به صورت تکی ثبت و استفاده کنیم.  


وهله سازی سفارشی

در مثال بعدی نشان می‌دهیم که چطور می‌توانیم از Implementation Factory برای برگرداندن پیاده‌سازی سرویس‌هایی که Service Provider امکان ساخت خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری می‌کند و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام می‌دهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل می‌کنیم: 

services.AddTransient<IAccountBuilder, AccountBuilder>();

services.AddScoped<IAccount>(sp => {
    var builder = sp.GetService<IAccountBuilder>();
    builder.WithAccountType(AccountType.Guest);
    return builder.Build();
});


ثبت یک نمونه برای چندین اینترفیس

ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت نمونه‌ی شیء ساخته شده‌، توسط هر کدام از اینترفیس‌ها قابل استفاده است. فرض کنید یک سرویس Greeting داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی می‌کرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی جامع‌تر را با نیازمندی‌های دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند: 

public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }

احتمالا اولین چیزی که به ذهنمان می‌رسد این است: 

services.TryAddSingleton<IHomeGreetingService, GreetingService>();
services.TryAddSingleton<IGreetingService, GreetingService>();

مشکل روش بالا این است که دو نمونه از GreetingService ساخته می‌شود و درون حافظه باقی می‌ماند و در حقیقت برای هر اینترفیس، یک نوع جداگانه از GreetingService ثبت می‌شود؛ در حالیکه ما می‌خواهیم هر دو اینترفیس فقط از یک نمونه از شیء GreetingService استفاده کنند.  دو راه حل برای این مشکل وجود دارد:

var greetingService = new GreetingService(Environment);
services.TryAddSingleton<IHomeGreetingService>(greetingService);
services.TryAddSingleton<IGreetingService>(greetingService);

در اینجا سازنده‌ی کلاس GreetingService فقط به  environment نیاز داشت که یکی از سرویس‌های پایه‌ی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویس‌ها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آن‌ها را نداریم؛ علاوه بر این نمی‌توان از روش‌های بالای برای حالت‌های Scoped یا Transient  استفاده کرد.

روش بعدی همان روش استفاده از Implementation Factory است که در ادامه آن را می‌بینید: 

services.TryAddSingleton<GreetingService>();
services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>());
services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());

در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این می‌توان با استفاده از روش فوق از طول حیات‌های Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.

 

Open Generics Service

گاهی از اوقات می‌خواهید سرویس‌هایی را ثبت کنید که از اینترفیسی جنریک ارث بری می‌کنند. هر نوع جنریک در زمان اجرا، نوع مخصوص به خود را واکشی می‌کند. ثبت کردن دستی این سرویس‌ها می‌تواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویس‌های جنریک را نیز در اختیار ما گذاشته‌است. بیایید نگاهی به سرویس ILogger<T>  بیندازیم. این یک سرویس درونی فریمورک است و می‌تواند به ازای هر نوع، کارهای مربوط به ثبت log را انجام بدهد و در پروژه‌ها معمولا از این اینترفیس برای ثبت لاگ‌ها در سطح کنترلر و سرویس‌ها استفاده می‌شود: 

public interface ILogger<out TCategoryName> : ILogger
{
}

در حالت عادی اگر سرویسی مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن، مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم: 

services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
این کاری طاقت فرساست. به همین جهت مایکروسافت قابلیت Open Generics Service را در اختیار ما گذاشته تا بتوانیم اینگونه سرویس‌ها را فقط و فقط یکبار ثبت کنیم: 
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
و اینگونه می‌توانیم نمونه‌های مختلف از ILogger<T> را به هر جایی که خواستیم، تزریق کنیم.

 

دسته بندی سرویس‌ها درون متدهای مختلف و پاکسازی  متد ConfigurationService

 تا اینجای کار ما سرویس‌های مختلفی را به روش‌های مختلفی ثبت کرده‌ایم. حتی در همین آموزش ساده، تعداد زیاد سرویس‌های ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما می‌شوند که خوانایی و در ادامه اشکال زدایی و توسعه‌ی کدها را برای ما سخت‌تر می‌کنند.  ساده‌ترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعه‌ای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعه‌ای را آورده‌ام:
namespace AspNetCoreDependencyInjection.Extensions
{
    public static class DICRegisterationExetnsion
    {
        /// <summary>
        /// مثال ثبت برای اپن جنریت
        /// </summary>
        /// <param name="services"></param>
        public static void OpenGenericRegisterationExample(this IServiceCollection services)
        {
            services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>();
            services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>));
        }


        /// <summary>
        /// ثبت تنظیمات به روش‌های مختلف 
        /// </summary>
        public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton(services => new AppConfig
            {
                ApplicationName = configuration["ApplicationName"],
                GreetingMessage = configuration["GreetingMessage"],
                AllowedHosts = configuration["AllowedHosts"]
            });

            services.AddSingleton(services => new AccountTypeBalanceConfig(
                    new List<(AccountType, long)> {
                        (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) ,
                        (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) ,
                        (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) ,
                        (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) ,
                        (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) ,
                    })
            );

            services.AddSingleton(services => new LiteDbConfig
            {
                ConnectionString = configuration["LiteDbConfig:ConnectionString"],
            });

            services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig"));
        }
    }
}

حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعه‌ای استفاده می‌کنیم:
services.RegisterConfiguration(this.Configuration);
services.OpenGenericRegisterationExample();

می‌توانید کد منبع این آموزش را در اینجا  ببینید.
اشتراک‌ها
AutoMapper 4.0 منتشر شد

There’s a ton of small bug fixes in this release, quite a few enhancements and a few larger new features. Configuration performance went up quite a bit, and I’ve laid the groundwork to make in-memory mapping a lot faster in the future. LINQ projection has gotten to the point where you can do anything that the major query providers support.

AutoMapper 4.0 منتشر شد
اشتراک‌ها
معرفی NET MAUI Preview 4.

Today we are pleased to announce the availability of .NET Multi-platform App UI (.NET MAUI) Preview 4. Each preview introduces more controls and features to this multi-platform toolkit on our way to general availability this November at .NET Conf. .NET MAUI now has enough building blocks to build functional applications for all supported platforms, new capabilities to support running Blazor on the desktop, and exciting progress in Visual Studio to support .NET MAUI.


معرفی NET MAUI Preview 4.
اشتراک‌ها
Bootstrap 5.1.0 منتشر شد

The first minor release of Bootstrap 5 is here! v5.1.0 has arrived and is packed with exciting new features and improvements. There’s experimental support for CSS Grid, offcanvas in the navbar, a new placeholders component, horizontal collapse support, new helpers, new CSS variables in our utilities, refactored JavaScript, and more. 

Bootstrap 5.1.0 منتشر شد